Merge pull request #929 from Elv13/geometry_overhaul_p3.03

Geometry overhaul part 3.03 (final part for 3.6)
This commit is contained in:
Emmanuel Lepage Vallée 2016-08-20 16:40:36 -04:00 committed by GitHub
commit ba2750cf91
4 changed files with 825 additions and 86 deletions

View File

@ -284,7 +284,6 @@ end
-- @treturn table The list of widgets.The first element is the biggest
-- container while the last is the topmost widget. The table contains *x*, *y*,
-- *width*, *height* and *widget*.
-- @treturn table The list of geometries.
-- @see wibox.find_widgets
function mouse.object.get_current_widgets()
@ -308,8 +307,8 @@ end
-- @property current_widget
-- @tparam widget|nil widget The widget
-- @treturn ?widget The widget
-- @treturn ?table The geometry.
-- @see wibox.find_widgets
-- @see current_widget_geometry
function mouse.object.get_current_widget()
local wdgs, geos = mouse.object.get_current_widgets()
@ -319,6 +318,28 @@ function mouse.object.get_current_widget()
end
end
--- Get the current widget geometry.
-- @property current_widget_geometry
-- @tparam ?table The geometry.
-- @see current_widget
function mouse.object.get_current_widget_geometry()
local _, ret = mouse.object.get_current_widget()
return ret
end
--- Get the current widget geometries.
-- @property current_widget_geometries
-- @tparam ?table A list of geometry tables.
-- @see current_widgets
function mouse.object.get_current_widget_geometries()
local _, ret = mouse.object.get_current_widgets()
return ret
end
--- True if the left mouse button is pressed.
-- @property is_left_mouse_button_pressed
-- @param boolean

View File

@ -95,7 +95,8 @@ local layout = require("awful.layout")
local a_screen = require("awful.screen")
local grect = require("gears.geometry").rectangle
local util = require("awful.util")
local dpi = require("beautiful").xresources.apply_dpi
local cairo = require( "lgi" ).cairo
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local function get_screen(s)
return s and capi.screen[s]
@ -260,6 +261,19 @@ local resize_to_point_map = {
bottom = {p1={0,0} , p2= nil , x_only=false, y_only=true , align="top_left" },
}
-- Outer position matrix
-- 1=best case, 2=fallback
local outer_positions = {
left1 = function(r, w, _) return {x=r.x-w , y=r.y }, "down" end,
left2 = function(r, w, h) return {x=r.x-w , y=r.y-h+r.height }, "up" end,
right1 = function(r, _, _) return {x=r.x , y=r.y }, "down" end,
right2 = function(r, _, h) return {x=r.x , y=r.y-h+r.height }, "up" end,
top1 = function(r, _, h) return {x=r.x , y=r.y-h }, "right" end,
top2 = function(r, w, h) return {x=r.x-w+r.width, y=r.y-h }, "left" end,
bottom1 = function(r, _, _) return {x=r.x , y=r.y }, "right" end,
bottom2 = function(r, w, _) return {x=r.x-w+r.width, y=r.y }, "left" end,
}
--- Add a context to the arguments.
-- This function extend the argument table. The context is used by some
-- internal helper methods. If there already is a context, it has priority and
@ -553,8 +567,9 @@ attach = function(d, position_f, args)
end
end
-- If there is a parent drawable, screen or mouse, also track it
if parent then
-- If there is a parent drawable, screen, also track it.
-- Note that tracking the mouse is not supported
if parent and parent.connect_signal then
parent:connect_signal("property::geometry" , tracker)
end
@ -603,6 +618,158 @@ local function rect_to_point(rect, corner_i, corner_j)
}
end
-- Create a pair of rectangles used to set the relative areas.
-- v=vertical, h=horizontal
local function get_cross_sections(abs_geo, mode)
if not mode or mode == "cursor" then
-- A 1px cross section centered around the mouse position
local coords = capi.mouse.coords()
return {
h = {
x = abs_geo.drawable_geo.x ,
y = coords.y ,
width = abs_geo.drawable_geo.width ,
height = 1 ,
},
v = {
x = coords.x ,
y = abs_geo.drawable_geo.y ,
width = 1 ,
height = abs_geo.drawable_geo.height,
}
}
elseif mode == "geometry" then
-- The widget geometry extended to reach the end of the drawable
return {
h = {
x = abs_geo.drawable_geo.x ,
y = abs_geo.y ,
width = abs_geo.drawable_geo.width ,
height = abs_geo.height ,
},
v = {
x = abs_geo.x ,
y = abs_geo.drawable_geo.y ,
width = abs_geo.width ,
height = abs_geo.drawable_geo.height,
}
}
elseif mode == "cursor_inside" then
-- A 1x1 rectangle centered around the mouse position
local coords = capi.mouse.coords()
coords.width,coords.height = 1,1
return {h=coords, v=coords}
elseif mode == "geometry_inside" then
-- The widget absolute geometry, unchanged
return {h=abs_geo, v=abs_geo}
end
end
-- When a rectangle is embedded into a bigger one, get the regions around
-- the outline of the bigger rectangle closest to the smaller one (on each side)
local function get_relative_regions(geo, mode, is_absolute)
-- Use the mouse position and the wibox/client under it
if not geo then
local draw = capi.mouse.current_wibox
geo = draw and draw:geometry() or capi.mouse.coords()
geo.drawable = draw
elseif is_absolute then
-- Some signals are a bit inconsistent in their arguments convention.
-- This little hack tries to mitigate the issue.
geo.drawable = geo -- is a wibox or client, geometry and object are one
-- and the same.
elseif (not geo.drawable) and geo.x and geo.width then
local coords = capi.mouse.coords()
-- Check if the mouse is in the rect
if coords.x > geo.x and coords.x < geo.x+geo.width and
coords.y > geo.y and coords.y < geo.y+geo.height then
geo.drawable = capi.mouse.current_wibox
end
-- Maybe there is a client
if (not geo.drawable) and capi.mouse.current_client then
geo.drawable = capi.mouse.current_client
end
end
-- Get the drawable geometry
local dpos = geo.drawable and (
geo.drawable.drawable and
geo.drawable.drawable:geometry()
or geo.drawable:geometry()
) or {x=0, y=0}
-- Compute the absolute widget geometry
local abs_widget_geo = is_absolute and geo or {
x = dpos.x + geo.x ,
y = dpos.y + geo.y ,
width = geo.width ,
height = geo.height ,
drawable = geo.drawable ,
}
abs_widget_geo.drawable_geo = geo.drawable and dpos or geo
-- Get the point for comparison.
local center_point = mode:match("cursor") and capi.mouse.coords() or {
x = abs_widget_geo.x + abs_widget_geo.width / 2,
y = abs_widget_geo.y + abs_widget_geo.height / 2,
}
-- Get widget regions for both axis
local cs = get_cross_sections(abs_widget_geo, mode)
-- Get the 4 closest points from `center_point` around the wibox
local regions = {
left = {x = cs.h.x , y = cs.h.y },
right = {x = cs.h.x+cs.h.width, y = cs.h.y },
top = {x = cs.v.x , y = cs.v.y },
bottom = {x = cs.v.x , y = cs.v.y+cs.v.height},
}
-- Assume the section is part of a single screen until someone complains.
-- It is much faster to compute and getting it wrong probably has no side
-- effects.
local s = geo.drawable and geo.drawable.screen or a_screen.getbycoord(
center_point.x,
center_point.y
)
-- Compute the distance (dp) between the `center_point` and the sides.
-- This is only relevant for "cursor" and "cursor_inside" modes.
for _, v in pairs(regions) do
local dx, dy = v.x - center_point.x, v.y - center_point.y
v.distance = math.sqrt(dx*dx + dy*dy)
v.width = cs.v.width
v.height = cs.h.height
v.screen = capi.screen[s]
end
return regions
end
-- Check if the proposed geometry fits the screen
local function fit_in_bounding(obj, geo, args)
local sgeo = get_parent_geometry(obj, args)
local region = cairo.Region.create_rectangle(cairo.RectangleInt(sgeo))
region:intersect(cairo.Region.create_rectangle(
cairo.RectangleInt(geo)
))
local geo2 = region:get_rectangle(0)
-- If the geometry is the same then it fits, otherwise it will be cropped.
return geo2.width == geo.width and geo2.height == geo.height
end
--- Move a drawable to the closest corner of the parent geometry (such as the
-- screen).
--
@ -778,38 +945,44 @@ end
-- It will place `c` next to the mouse pointer, trying the following positions
-- in this order: right, left, above and below.
--@DOC_awful_placement_next_to_mouse_EXAMPLE@
-- @client[opt=focused] c The client.
-- @tparam[opt=apply_dpi(5)] integer offset The offset from the mouse position.
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.next_to_mouse(c, offset)
c = c or capi.client.focus
offset = offset or dpi(5)
local c_geometry = area_common(c)
local c_width = c_geometry.width
local c_height = c_geometry.height
local m_coords = capi.mouse.coords()
local screen_geometry = capi.screen[capi.mouse.screen].workarea
local x, y
-- Prefer it to be on the right.
x = m_coords.x + offset
if x + c_width > screen_geometry.width then
-- Then to the left.
x = m_coords.x - c_width - offset
function placement.next_to_mouse(d, args)
if type(args) == "number" then
util.deprecate(
"awful.placement.next_to_mouse offset argument is deprecated"..
" use awful.placement.next_to_mouse(c, {offset={x=...}})"
)
args = nil
end
if x < screen_geometry.x then
-- Then above.
x = m_coords.x - math.ceil(c_width / 2)
y = m_coords.y - c_height - offset
if y < screen_geometry.y then
-- Finally below.
y = m_coords.y + offset
end
local old_args = args or {}
args = add_context(args, "next_to_mouse")
d = d or capi.client.focus
local sgeo = get_parent_geometry(d, args)
args.pretend = true
args.parent = capi.mouse
local ngeo = placement.left(d, args)
if ngeo.x + ngeo.width > sgeo.x+sgeo.width then
ngeo = placement.right(d, args)
else
y = m_coords.y - math.ceil(c_height / 2)
-- It is _next_ to mouse, not under_mouse
ngeo.x = ngeo.x+1
end
return c:geometry({ x = x, y = y })
args.pretend = old_args.pretend
geometry_common(d, args, ngeo)
attach(d, placement.next_to_mouse, old_args)
return fix_new_geometry(ngeo, args, true)
end
--- Resize the drawable to the cursor.
@ -1087,6 +1260,10 @@ for _, v in ipairs {"vertically", "horizontally"} do
end
end
---@DOC_awful_placement_maximize_vertically_EXAMPLE@
---@DOC_awful_placement_maximize_horizontally_EXAMPLE@
--- Scale the drawable by either a relative or absolute percent.
--
-- Valid args:
@ -1143,9 +1320,94 @@ function placement.scale(d, args)
return fix_new_geometry(ngeo, args, true)
end
---@DOC_awful_placement_maximize_vertically_EXAMPLE@
--- Move a drawable to a relative position next to another one.
--
-- The `args.preferred_positions` look like this:
--
-- {"top", "right", "left", "bottom"}
--
-- In that case, if there is room on the top of the geomtry, then it will have
-- priority, followed by all the others, in order.
--
-- @tparam drawable d A wibox or client
-- @tparam table args
-- @tparam string args.mode The mode
-- @tparam string args.preferred_positions The preferred positions (in order)
-- @tparam string args.geometry A geometry inside the other drawable
-- @treturn table The new geometry
-- @treturn string The choosen position
-- @treturn string The choosen direction
function placement.next_to(d, args)
args = add_context(args, "next_to")
d = d or capi.client.focus
---@DOC_awful_placement_maximize_horizontally_EXAMPLE@
local preferred_positions = {}
if #(args.preferred_positions or {}) then
for k, v in ipairs(args.preferred_positions) do
preferred_positions[v] = k
end
end
local dgeo = geometry_common(d, args)
local pref_idx, pref_name = 99, nil
local mode,wgeo = args.mode
if args.geometry then
mode = "geometry"
wgeo = args.geometry
else
local pos = capi.mouse.current_widget_geometry
if pos then
wgeo, mode = pos, "cursor"
elseif capi.mouse.current_client then
wgeo, mode = capi.mouse.current_client:geometry(), "cursor"
end
end
if not wgeo then return end
-- See get_relative_regions comments
local is_absolute = wgeo.ontop ~= nil
local regions = get_relative_regions(wgeo, mode, is_absolute)
-- Check each possible slot around the drawable (8 total), see what fits
-- and order them by preferred_positions
local does_fit = {}
for k,v in pairs(regions) do
local geo, dir = outer_positions[k.."1"](v, dgeo.width, dgeo.height)
geo.width, geo.height = dgeo.width, dgeo.height
local fit = fit_in_bounding(v.screen, geo, args)
-- Try the other compatible geometry
if not fit then
geo, dir = outer_positions[k.."2"](v, dgeo.width, dgeo.height)
geo.width, geo.height = dgeo.width, dgeo.height
fit = fit_in_bounding(v.screen, geo, args)
end
does_fit[k] = fit and {geo, dir} or nil
if fit and preferred_positions[k] and preferred_positions[k] < pref_idx then
pref_idx = preferred_positions[k]
pref_name = k
end
-- No need to continue
if fit and preferred_positions[k] == 1 then break end
end
local pos_name = pref_name or next(does_fit)
local ngeo, dir = unpack(does_fit[pos_name] or {}) --FIXME why does this happen
geometry_common(d, args, ngeo)
attach(d, placement.next_to, args)
return fix_new_geometry(ngeo, args, true), pos_name, dir
end
--- Restore the geometry.
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)

View File

@ -37,41 +37,172 @@
-- @author Sébastien Gross &lt;seb•ɱɩɲʋʃ•awesome•ɑƬ•chezwam•ɖɵʈ•org&gt;
-- @copyright 2009 Sébastien Gross
-- @release @AWESOME_VERSION@
-- @module awful.tooltip
-- @classmod awful.tooltip
-------------------------------------------------------------------------
local mouse = mouse
local timer = require("gears.timer")
local util = require("awful.util")
local object = require("gears.object")
local color = require("gears.color")
local wibox = require("wibox")
local a_placement = require("awful.placement")
local abutton = require("awful.button")
local shape = require("gears.shape")
local beautiful = require("beautiful")
local textbox = require("wibox.widget.textbox")
local background = require("wibox.container.background")
local dpi = require("beautiful").xresources.apply_dpi
local cairo = require("lgi").cairo
local setmetatable = setmetatable
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local ipairs = ipairs
local capi = {mouse=mouse, awesome=awesome}
--- Tooltip object definition.
-- @table tooltip
-- @tfield wibox wibox The wibox displaying the tooltip.
-- @tfield boolean visible True if tooltip is visible.
local tooltip = { mt = {} }
local instance_mt = {}
-- The mouse point is 1x1, so anything aligned based on it as parent
-- geometry will go out of bound. To get the desired placement, it is
-- necessary to swap left with right and top with bottom
local align_convert = {
top_left = "bottom_right",
left = "right",
bottom_left = "top_right",
right = "left",
top_right = "bottom_left",
bottom_right = "top_left",
top = "bottom",
bottom = "top",
}
function instance_mt:__index(key)
if key == "wibox" then
local wb = wibox(self.wibox_properties)
wb:set_widget(self.marginbox)
-- If the wibox is under the cursor, it will trigger a mouse::leave
local offset = {
top_left = {x = 0, y = 0 },
left = {x = 0, y = 0 },
bottom_left = {x = 0, y = 0 },
right = {x = 1, y = 0 },
top_right = {x = 0, y = 0 },
bottom_right = {x = 1, y = 1 },
top = {x = 0, y = 0 },
bottom = {x = 0, y = 1 },
}
-- Close the tooltip when clicking it. This gets done on release, to not
-- emit the release event on an underlying object, e.g. the titlebar icon.
wb:buttons(abutton({}, 1, nil, self.hide))
rawset(self, "wibox", wb)
return wb
--- The tooltip border color.
-- @beautiful beautiful.tooltip_border_color
--- The tooltip background color.
-- @beautiful beautiful.tooltip_bg
--- The tooltip foregound (text) color.
-- @beautiful beautiful.tooltip_fg
--- The tooltip font.
-- @beautiful beautiful.tooltip_font
--- The tooltip border width.
-- @beautiful beautiful.tooltip_border_width
--- The tooltip opacity.
-- @beautiful beautiful.tooltip_opacity
--- The default tooltip shape.
-- By default, all tooltips are rectangles, however, by setting this variables,
-- they can default to rounded rectangle or stretched octogons.
-- @beautiful beautiful.tooltip_shape
-- @tparam[opt=gears.shape.rectangle] function shape A `gears.shape` compatible function
-- @see shape
-- @see gears.shape
local function apply_shape(self)
local s = self._private.shape
local wb = self.wibox
if not s then
-- Clear the shape
if wb.shape_bounding then
wb.shape_bounding = nil
wb:set_bgimage(nil)
end
return
end
local w, h = wb.width, wb.height
-- First, create a A1 mask for the shape bounding itself
local img = cairo.ImageSurface(cairo.Format.A1, w, h)
local cr = cairo.Context(img)
cr:set_source_rgba(1,1,1,1)
s(cr, w, h, unpack(self._private.shape_args or {}))
cr:fill()
wb.shape_bounding = img._native
-- The wibox background uses ARGB32 border so tooltip anti-aliasing works
-- when an external compositor is used. This will look better than
-- the capi.drawin's own border support.
img = cairo.ImageSurface(cairo.Format.ARGB32, w, h)
cr = cairo.Context(img)
-- Draw the border (multiply by 2, then mask the inner part to save a path)
local bw = (self._private.border_width
or beautiful.tooltip_border_width
or beautiful.border_width or 0) * 2
-- Fix anti-aliasing
if bw > 2 and awesome.composite_manager_running then
bw = bw - 1
end
local bc = self._private.border_color
or beautiful.tooltip_border_color
or beautiful.border_normal
or "#ffcb60"
cr:translate(bw, bw)
s(cr, w-2*bw, h-2*bw, unpack(self._private.shape_args or {}))
cr:set_line_width(bw)
cr:set_source(color(bc))
cr:stroke_preserve()
cr:clip()
local bg = self._private.bg
or beautiful.tooltip_bg
or beautiful.bg_focus or "#ffcb60"
cr:set_source(color(bg))
cr:paint()
wb:set_bgimage(img)
end
local function apply_mouse_mode(self)
local w = self:get_wibox()
local align = self._private.align
local real_placement = align_convert[align]
a_placement[real_placement](w, {
parent = capi.mouse,
offset = offset[align]
})
end
local function apply_outside_mode(self)
local w = self:get_wibox()
local _, position = a_placement.next_to(w, {
geometry = self._private.widget_geometry,
preferred_positions = self.preferred_positions,
honor_workarea = true,
})
if position ~= self.current_position then
-- Re-apply the shape.
apply_shape(self)
end
self.current_position = position
end
-- Place the tooltip under the mouse.
@ -82,9 +213,23 @@ local function set_geometry(self)
local n_w, n_h = self.textbox:get_preferred_size(mouse.screen)
n_w = n_w + self.marginbox.left + self.marginbox.right
n_h = n_h + self.marginbox.top + self.marginbox.bottom
self.wibox:geometry({ width = n_w, height = n_h })
a_placement.next_to_mouse(self.wibox)
a_placement.no_offscreen(self.wibox, mouse.screen)
local w = self:get_wibox()
w:geometry({ width = n_w, height = n_h })
if self._private.shape then
apply_shape(self)
end
local mode = self.mode
if mode == "outside" and self._private.widget_geometry then
apply_outside_mode(self)
else
apply_mouse_mode(self)
end
a_placement.no_offscreen(w)
end
-- Show a tooltip.
@ -92,7 +237,7 @@ end
-- @tparam tooltip self The tooltip to show.
local function show(self)
-- do nothing if the tooltip is already shown
if self.visible then return end
if self._private.visible then return end
if self.timer then
if not self.timer.started then
self:timer_function()
@ -101,7 +246,7 @@ local function show(self)
end
set_geometry(self)
self.wibox.visible = true
self.visible = true
self._private.visible = true
self:emit_signal("property::visible")
end
@ -110,46 +255,192 @@ end
-- @tparam tooltip self The tooltip to hide.
local function hide(self)
-- do nothing if the tooltip is already hidden
if not self.visible then return end
if not self._private.visible then return end
if self.timer then
if self.timer.started then
self.timer:stop()
end
end
self.wibox.visible = false
self.visible = false
self._private.visible = false
self:emit_signal("property::visible")
end
--- The wibox.
-- @property wibox
-- @param `wibox`
function tooltip:get_wibox()
if self._private.wibox then
return self._private.wibox
end
local wb = wibox(self.wibox_properties)
wb:set_widget(self.marginbox)
-- Close the tooltip when clicking it. This gets done on release, to not
-- emit the release event on an underlying object, e.g. the titlebar icon.
wb:buttons(abutton({}, 1, nil, self.hide))
self._private.wibox = wb
return wb
end
--- Is the tooltip visible?
-- @property visible
-- @param boolean
function tooltip:get_visible()
return self._private.visible
end
function tooltip:set_visible(value)
if self._private.visible == value then return end
if value then
show(self)
else
hide(self)
end
end
--- The horizontal alignment.
--
-- The following values are valid:
--
-- * top_left
-- * left
-- * bottom_left
-- * right
-- * top_right
-- * bottom_right
-- * bottom
-- * top
--
-- @property align
-- @see beautiful.tooltip_align
--- The default tooltip alignment.
-- @beautiful beautiful.tooltip_align
-- @param string
-- @see align
function tooltip:get_align()
return self._private.align
end
function tooltip:set_align(value)
if not align_convert[value] then
return
end
self._private.align = value
set_geometry(self)
self:emit_signal("property::align")
end
--- The shape of the tooltip window.
-- If the shape require some parameters, use `set_shape`.
-- @property shape
-- @see gears.shape
-- @see set_shape
-- @see beautiful.tooltip_shape
--- Set the tooltip shape.
-- All other arguments will be passed to the shape function.
-- @tparam gears.shape s The shape
-- @see shape
-- @see gears.shape
function tooltip:set_shape(s, ...)
self._private.shape = s
self._private.shape_args = {...}
apply_shape(self)
end
--- Set the tooltip positioning mode.
-- This affects how the tooltip is placed. By default, the tooltip is `align`ed
-- close to the mouse cursor. It is also possible to place the tooltip relative
-- to the widget geometry.
--
-- Valid modes are:
--
-- * "mouse": Next to the mouse cursor
-- * "outside": Outside of the widget
--
-- @property mode
-- @param string
function tooltip:set_mode(mode)
self._private.mode = mode
set_geometry(self)
self:emit_signal("property::mode")
end
function tooltip:get_mode()
return self._private.mode or "mouse"
end
--- The preferred positions when in `outside` mode.
--
-- If the tooltip fits on multiple sides of the drawable, then this defines the
-- priority
--
-- The default is:
--
-- {"top", "right", "left", "bottom"}
--
-- @property preferred_positions
-- @tparam table preferred_positions The position, ordered by priorities
function tooltip:get_preferred_positions()
return self._private.preferred_positions or
{"top", "right", "left", "bottom"}
end
function tooltip:set_preferred_positions(value)
self._private.preferred_positions = value
set_geometry(self)
end
--- Change displayed text.
--
-- @property text
-- @tparam tooltip self The tooltip object.
-- @tparam string text New tooltip text, passed to
-- `wibox.widget.textbox.set_text`.
tooltip.set_text = function(self, text)
function tooltip:set_text(text)
self.textbox:set_text(text)
if self.visible then
if self._private.visible then
set_geometry(self)
end
end
--- Change displayed markup.
--
-- @property markup
-- @tparam tooltip self The tooltip object.
-- @tparam string text New tooltip markup, passed to
-- `wibox.widget.textbox.set_markup`.
tooltip.set_markup = function(self, text)
function tooltip:set_markup(text)
self.textbox:set_markup(text)
if self.visible then
if self._private.visible then
set_geometry(self)
end
end
--- Change the tooltip's update interval.
--
-- @property timeout
-- @tparam tooltip self A tooltip object.
-- @tparam number timeout The timeout value.
tooltip.set_timeout = function(self, timeout)
function tooltip:set_timeout(timeout)
if self.timer then
self.timer.timeout = timeout
end
@ -160,7 +451,10 @@ end
-- @tparam tooltip self The tooltip.
-- @tparam gears.object obj An object with `mouse::enter` and
-- `mouse::leave` signals.
tooltip.add_to_object = function(self, obj)
-- @function add_to_object
function tooltip:add_to_object(obj)
if not obj then return end
obj:connect_signal("mouse::enter", self.show)
obj:connect_signal("mouse::leave", self.hide)
end
@ -170,11 +464,21 @@ end
-- @tparam tooltip self The tooltip.
-- @tparam gears.object obj An object with `mouse::enter` and
-- `mouse::leave` signals.
tooltip.remove_from_object = function(self, obj)
-- @function remove_from_object
function tooltip:remove_from_object(obj)
obj:disconnect_signal("mouse::enter", self.show)
obj:disconnect_signal("mouse::leave", self.hide)
end
-- Tooltip can be applied to both widgets, wibox and client, their geometry
-- works differently.
local function get_parent_geometry(arg1, arg2)
if type(arg2) == "table" and arg2.width then
return arg2
elseif type(arg1) == "table" and arg1.width then
return arg1
end
end
--- Create a new tooltip and link it to a widget.
-- Tooltips emit `property::visible` when their visibility changes.
@ -189,14 +493,24 @@ end
-- seconds.
-- @tparam[opt=apply_dpi(5)] integer args.margin_leftright The left/right margin for the text.
-- @tparam[opt=apply_dpi(3)] integer args.margin_topbottom The top/bottom margin for the text.
-- @tparam[opt=nil] gears.shape args.shape The shape
-- @treturn awful.tooltip The created tooltip.
-- @see add_to_object
-- @see set_timeout
-- @see set_text
-- @see set_markup
tooltip.new = function(args)
local self = setmetatable(object(), instance_mt)
self.visible = false
-- @see timeout
-- @see text
-- @see markup
-- @function awful.tooltip
function tooltip.new(args)
local self = object {
enable_properties = true,
}
rawset(self,"_private", {})
self._private.visible = false
self._private.align = args.align or beautiful.tooltip_align or "right"
self._private.shape = args.shape or beautiful.tooltip_shape
or shape.rectangle
-- private data
if args.delay_show then
@ -208,7 +522,15 @@ tooltip.new = function(args)
delay_timeout:stop()
end)
function self.show()
function self.show(other, geo)
-- Auto detect clients and wiboxes
if other.drawable or other.pid then
geo = other:geometry()
end
-- Cache the geometry in case it is needed later
self._private.widget_geometry = get_parent_geometry(other, geo)
if not delay_timeout.started then
delay_timeout:start()
end
@ -220,7 +542,15 @@ tooltip.new = function(args)
hide(self)
end
else
function self.show()
function self.show(other, geo)
-- Auto detect clients and wiboxes
if other.drawable or other.pid then
geo = other:geometry()
end
-- Cache the geometry in case it is needed later
self._private.widget_geometry = get_parent_geometry(other, geo)
show(self)
end
function self.hide()
@ -229,11 +559,7 @@ tooltip.new = function(args)
end
-- export functions
self.set_text = tooltip.set_text
self.set_markup = tooltip.set_markup
self.set_timeout = tooltip.set_timeout
self.add_to_object = tooltip.add_to_object
self.remove_from_object = tooltip.remove_from_object
util.table.crush(self, tooltip, true)
-- setup the timer action only if needed
if args.timer_function then
@ -244,27 +570,26 @@ tooltip.new = function(args)
self.timer:connect_signal("timeout", self.timer_function)
end
local fg = beautiful.tooltip_fg or beautiful.fg_focus or "#000000"
local font = beautiful.tooltip_font or beautiful.font or "terminus 6"
-- Set default properties
self.wibox_properties = {
visible = false,
ontop = true,
border_width = beautiful.tooltip_border_width or beautiful.border_width or 1,
border_color = beautiful.tooltip_border_color or beautiful.border_normal or "#ffcb60",
border_width = 0,
fg = fg,
bg = color.transparent,
opacity = beautiful.tooltip_opacity or 1,
bg = beautiful.tooltip_bg_color or beautiful.bg_focus or "#ffcb60"
}
local fg = beautiful.tooltip_fg_color or beautiful.fg_focus or "#000000"
local font = beautiful.tooltip_font or beautiful.font or "terminus 6"
self.textbox = textbox()
self.textbox:set_font(font)
self.background = background(self.textbox)
self.background:set_fg(fg)
-- Add margin.
local m_lr = args.margin_leftright or dpi(5)
local m_tb = args.margin_topbottom or dpi(3)
self.marginbox = wibox.container.margin(self.background, m_lr, m_lr, m_tb, m_tb)
self.marginbox = wibox.container.margin(self.textbox, m_lr, m_lr, m_tb, m_tb)
-- Add tooltip to objects
if args.objects then
@ -273,6 +598,13 @@ tooltip.new = function(args)
end
end
-- Apply the properties
for k, v in pairs(args) do
if tooltip["set_"..k] then
self[k] = v
end
end
return self
end

124
tests/test-tooltip.lua Normal file
View File

@ -0,0 +1,124 @@
local runner = require("_runner")
local place = require("awful.placement")
local wibox = require("wibox")
local beautiful = require("beautiful")
local tooltip = require("awful.tooltip")
local gears = require("gears")
local steps = {}
local w = wibox {
width = 250,
height = 250,
visible = true,
ontop = true
}
w:setup{
image = beautiful.awesome_icon,
widget = wibox.widget.imagebox
}
-- Center eveything
place.centered(w)
place.centered(mouse)
local tt = nil
table.insert(steps, function()
tt = tooltip {text = "A long tooltip", visible = true}
return true
end)
local align_pos = {
"top_left", "left", "bottom_left", "right",
"top_right", "bottom_right", "bottom", "top",
}
-- Test the various alignment code paths
for _, v in ipairs(align_pos) do
table.insert(steps, function()
tt.align = v
return true
end)
end
-- Set a parent object
table.insert(steps, function()
tt:add_to_object(w)
return true
end)
-- Test the other mode
table.insert(steps, function()
tt.mode = "outside"
-- This only work when there is a mouse::enter event, create one
place.top_left(mouse)
place.centered(mouse)
return true
end)
--- Reset the wibox content and use a widget geometry.
table.insert(steps, function()
tt:remove_from_object(w)
tt.visible = false
w:setup{
{
image = beautiful.awesome_icon,
widget = wibox.widget.imagebox,
id = "myimagebox"
},
top = 125,
bottom = 100,
left = 205,
right = 20 ,
layout = wibox.container.margin
}
local imb = w:get_children_by_id("myimagebox")[1]
assert(imb)
tt:add_to_object(imb)
-- Move the mouse over the widget
place.top_left(mouse)
mouse.coords {
x = w.x + w.width - 20 - 12.5,
y = w.y + 125 + 12.5,
}
assert(tt.current_position == "top")
return true
end)
-- Try the preferred positions
table.insert(steps, function()
tt.visible = false
tt.preferred_positions = {"right"}
tt.visible = true
assert(tt.current_position == "right")
return true
end)
-- Add a shape.
table.insert(steps, function()
tt.shape = gears.shape.octogon
return true
end)
runner.run_steps(steps)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80