From ce5cdb49ed0e0b98348988db0f187c9ee6de1cea Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 28 May 2016 22:08:47 -0400 Subject: [PATCH 01/10] mouse: Add the current_widget_geometry property Apparently, __index cannot return multiple values, so the previous code wasn't working. --- lib/awful/mouse/init.lua | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/awful/mouse/init.lua b/lib/awful/mouse/init.lua index 822fec31f..22ada8b70 100644 --- a/lib/awful/mouse/init.lua +++ b/lib/awful/mouse/init.lua @@ -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 From 211907def2ed9af2ec3b8ae802dc50c047ad2a58 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 28 May 2016 22:38:40 -0400 Subject: [PATCH 02/10] placement: Add `next_to` This commit add the last placement function imported from the Radical module. It allows to place a wibox/client next to another object. It tries to find the best fit. It also support wibox widgets. This is intended for tooltips and menus, but can also be used in `awful.rules` to place the new client as close as possible to the focused one without overlap. --- lib/awful/placement.lua | 265 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 261 insertions(+), 4 deletions(-) diff --git a/lib/awful/placement.lua b/lib/awful/placement.lua index 778977e34..621a31c71 100644 --- a/lib/awful/placement.lua +++ b/lib/awful/placement.lua @@ -96,6 +96,8 @@ 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 +262,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 +568,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 +619,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). -- @@ -1087,6 +1255,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 +1315,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`) From 2910a007df40cbe6f06a6b71af03127abb512405 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 28 May 2016 23:26:33 -0400 Subject: [PATCH 03/10] tooltip: Turn into a class As done with everything else --- lib/awful/tooltip.lua | 94 ++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/lib/awful/tooltip.lua b/lib/awful/tooltip.lua index 3a2fec5e2..2da3c5e6a 100644 --- a/lib/awful/tooltip.lua +++ b/lib/awful/tooltip.lua @@ -37,11 +37,12 @@ -- @author Sébastien Gross <seb•ɱɩɲʋʃ•awesome•ɑƬ•chezwam•ɖɵʈ•org> -- @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 wibox = require("wibox") local a_placement = require("awful.placement") @@ -53,27 +54,8 @@ local dpi = require("beautiful").xresources.apply_dpi local setmetatable = setmetatable local ipairs = ipairs ---- 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 = {} - -function instance_mt:__index(key) - if key == "wibox" then - 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)) - rawset(self, "wibox", wb) - return wb - end -end - -- Place the tooltip under the mouse. -- -- @tparam tooltip self A tooltip object. @@ -82,9 +64,10 @@ 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) + + self:get_wibox():geometry({ width = n_w, height = n_h }) + a_placement.next_to_mouse(self:get_wibox()) + a_placement.no_offscreen(self:get_wibox(), mouse.screen) end -- Show a tooltip. @@ -121,12 +104,39 @@ local function hide(self) 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 + --- 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 set_geometry(self) @@ -135,10 +145,12 @@ 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 set_geometry(self) @@ -147,9 +159,11 @@ 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 +174,8 @@ 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) obj:connect_signal("mouse::enter", self.show) obj:connect_signal("mouse::leave", self.hide) end @@ -170,7 +185,8 @@ 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 @@ -191,11 +207,17 @@ end -- @tparam[opt=apply_dpi(3)] integer args.margin_topbottom The top/bottom margin for the text. -- @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) +-- @see timeout +-- @see text +-- @see markup +-- @function awful.tooltip +function tooltip.new(args) + local self = object { + enable_properties = true, + } + + rawset(self,"_private", {}) + self.visible = false -- private data @@ -229,11 +251,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 From ade3fabaa9131c49512e75b37c5826a132e83b21 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 May 2016 15:22:07 -0400 Subject: [PATCH 04/10] tooltip: Make the 'visible' property dynamic --- lib/awful/tooltip.lua | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/awful/tooltip.lua b/lib/awful/tooltip.lua index 2da3c5e6a..482bbf05d 100644 --- a/lib/awful/tooltip.lua +++ b/lib/awful/tooltip.lua @@ -75,7 +75,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() @@ -84,7 +84,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 @@ -93,14 +93,14 @@ 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 @@ -129,6 +129,20 @@ end -- @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 + --- Change displayed text. -- -- @property text @@ -138,7 +152,7 @@ end function tooltip:set_text(text) self.textbox:set_text(text) - if self.visible then + if self._private.visible then set_geometry(self) end end @@ -152,7 +166,7 @@ end function tooltip:set_markup(text) self.textbox:set_markup(text) - if self.visible then + if self._private.visible then set_geometry(self) end end @@ -218,7 +232,7 @@ function tooltip.new(args) rawset(self,"_private", {}) - self.visible = false + self._private.visible = false -- private data if args.delay_show then From ea52b199c60ee3680aba8d26d3e88e5af2d39185 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 May 2016 17:40:39 -0400 Subject: [PATCH 05/10] tooltip: Add align support --- lib/awful/tooltip.lua | 80 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/lib/awful/tooltip.lua b/lib/awful/tooltip.lua index 482bbf05d..7907185c3 100644 --- a/lib/awful/tooltip.lua +++ b/lib/awful/tooltip.lua @@ -53,9 +53,36 @@ local background = require("wibox.container.background") local dpi = require("beautiful").xresources.apply_dpi local setmetatable = setmetatable local ipairs = ipairs +local capi = {mouse=mouse} local tooltip = { 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", +} + +-- 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 }, +} + -- Place the tooltip under the mouse. -- -- @tparam tooltip self A tooltip object. @@ -65,9 +92,19 @@ local function set_geometry(self) n_w = n_w + self.marginbox.left + self.marginbox.right n_h = n_h + self.marginbox.top + self.marginbox.bottom - self:get_wibox():geometry({ width = n_w, height = n_h }) - a_placement.next_to_mouse(self:get_wibox()) - a_placement.no_offscreen(self:get_wibox(), mouse.screen) + local w = self:get_wibox() + w:geometry({ width = n_w, height = n_h }) + + local align = self._private.align + + local real_placement = align_convert[align] + + a_placement[real_placement](w, { + parent = capi.mouse, + offset = offset[align] + }) + + a_placement.no_offscreen(w) end -- Show a tooltip. @@ -143,6 +180,42 @@ function tooltip:set_visible(value) 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 + --- Change displayed text. -- -- @property text @@ -233,6 +306,7 @@ function tooltip.new(args) rawset(self,"_private", {}) self._private.visible = false + self._private.align = beautiful.tooltip_align or "right" -- private data if args.delay_show then From 8468d0635047dd27f05475572b8ec7699cce08a7 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 May 2016 18:01:12 -0400 Subject: [PATCH 06/10] placement: Port next_to_mouse to the new API --- lib/awful/placement.lua | 63 ++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/lib/awful/placement.lua b/lib/awful/placement.lua index 621a31c71..e6fbedd76 100644 --- a/lib/awful/placement.lua +++ b/lib/awful/placement.lua @@ -95,7 +95,6 @@ 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) @@ -946,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. From 141b191921be604c3859bcd0c8a6addcf7c4b67a Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Mon, 30 May 2016 15:05:57 -0400 Subject: [PATCH 07/10] tooltip: Add shape support --- lib/awful/tooltip.lua | 141 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 12 deletions(-) diff --git a/lib/awful/tooltip.lua b/lib/awful/tooltip.lua index 7907185c3..9162efda7 100644 --- a/lib/awful/tooltip.lua +++ b/lib/awful/tooltip.lua @@ -44,16 +44,19 @@ 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} +local capi = {mouse=mouse, awesome=awesome} local tooltip = { mt = {} } @@ -83,7 +86,97 @@ local offset = { bottom = {x = 0, y = 1 }, } --- Place the tooltip under the mouse. +--- 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 + -- -- @tparam tooltip self A tooltip object. local function set_geometry(self) @@ -95,6 +188,10 @@ local function set_geometry(self) local w = self:get_wibox() w:geometry({ width = n_w, height = n_h }) + if self._private.shape then + apply_shape(self) + end + local align = self._private.align local real_placement = align_convert[align] @@ -216,6 +313,24 @@ function tooltip:set_align(value) 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 + --- Change displayed text. -- -- @property text @@ -292,6 +407,7 @@ 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 timeout @@ -306,7 +422,9 @@ function tooltip.new(args) rawset(self,"_private", {}) self._private.visible = false - self._private.align = beautiful.tooltip_align or "right" + 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 @@ -350,27 +468,26 @@ function tooltip.new(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 From bc3cbc44c974c503db9fc2f402934b1898257d61 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Wed, 20 Jul 2016 20:15:13 -0400 Subject: [PATCH 08/10] tooltip: Add position mode --- lib/awful/tooltip.lua | 120 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 9 deletions(-) diff --git a/lib/awful/tooltip.lua b/lib/awful/tooltip.lua index 9162efda7..5baf217cb 100644 --- a/lib/awful/tooltip.lua +++ b/lib/awful/tooltip.lua @@ -177,6 +177,35 @@ local function apply_shape(self) 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. -- -- @tparam tooltip self A tooltip object. local function set_geometry(self) @@ -192,14 +221,13 @@ local function set_geometry(self) apply_shape(self) end - local align = self._private.align + local mode = self.mode - local real_placement = align_convert[align] - - a_placement[real_placement](w, { - parent = capi.mouse, - offset = offset[align] - }) + 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 @@ -331,6 +359,53 @@ function tooltip:set_shape(s, ...) 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 @@ -378,6 +453,8 @@ end -- `mouse::leave` signals. -- @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 @@ -393,6 +470,15 @@ function tooltip:remove_from_object(obj) 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. @@ -436,7 +522,15 @@ function tooltip.new(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 @@ -448,7 +542,15 @@ function tooltip.new(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() From 2088ca26e81552e208616461e63a052723b9848e Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Wed, 1 Jun 2016 14:21:23 -0400 Subject: [PATCH 09/10] tooltip: Support generic properties in constructor --- lib/awful/tooltip.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/awful/tooltip.lua b/lib/awful/tooltip.lua index 5baf217cb..136bdd951 100644 --- a/lib/awful/tooltip.lua +++ b/lib/awful/tooltip.lua @@ -598,6 +598,13 @@ function tooltip.new(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 From 4aada37682081f1a2c44bef323cfe1adf3f4056e Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Wed, 1 Jun 2016 17:10:01 -0400 Subject: [PATCH 10/10] tests: Test tooltips --- tests/test-tooltip.lua | 124 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/test-tooltip.lua diff --git a/tests/test-tooltip.lua b/tests/test-tooltip.lua new file mode 100644 index 000000000..ab25a5652 --- /dev/null +++ b/tests/test-tooltip.lua @@ -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