diff options
Diffstat (limited to 'lib/awful/tooltip.lua')
-rw-r--r-- | lib/awful/tooltip.lua | 616 |
1 files changed, 616 insertions, 0 deletions
diff --git a/lib/awful/tooltip.lua b/lib/awful/tooltip.lua new file mode 100644 index 0000000..cd151c4 --- /dev/null +++ b/lib/awful/tooltip.lua @@ -0,0 +1,616 @@ +------------------------------------------------------------------------- +--- Tooltip module for awesome objects. +-- +-- A tooltip is a small hint displayed when the mouse cursor +-- hovers a specific item. +-- In awesome, a tooltip can be linked with almost any +-- object having a `:connect_signal()` method and receiving +-- `mouse::enter` and `mouse::leave` signals. +-- +-- How to create a tooltip? +-- --- +-- +-- myclock = wibox.widget.textclock({}, "%T", 1) +-- myclock_t = awful.tooltip({ +-- objects = { myclock }, +-- timer_function = function() +-- return os.date("Today is %A %B %d %Y\nThe time is %T") +-- end, +-- }) +-- +-- How to add the same tooltip to multiple objects? +-- --- +-- +-- myclock_t:add_to_object(obj1) +-- myclock_t:add_to_object(obj2) +-- +-- Now the same tooltip is attached to `myclock`, `obj1`, `obj2`. +-- +-- How to remove a tooltip from several objects? +-- --- +-- +-- myclock_t:remove_from_object(obj1) +-- myclock_t:remove_from_object(obj2) +-- +-- Now the same tooltip is only attached to `myclock`. +-- +-- @author Sébastien Gross <seb•ɱɩɲʋʃ•awesome•ɑƬ•chezwam•ɖɵʈ•org> +-- @copyright 2009 Sébastien Gross +-- @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 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} + +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 }, +} + +--- 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. +-- +-- @tparam tooltip self A tooltip object. +local function set_geometry(self) + -- calculate width / height + 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 + + 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. +-- +-- @tparam tooltip self The tooltip to show. +local function show(self) + -- do nothing if the tooltip is already shown + if self._private.visible then return end + if self.timer then + if not self.timer.started then + self:timer_function() + self.timer:start() + end + end + set_geometry(self) + self.wibox.visible = true + self._private.visible = true + self:emit_signal("property::visible") +end + +-- Hide a tooltip. +-- +-- @tparam tooltip self The tooltip to hide. +local function hide(self) + -- do nothing if the tooltip is already hidden + 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._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`. + +function tooltip:set_text(text) + self.textbox:set_text(text) + 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`. + +function tooltip:set_markup(text) + self.textbox:set_markup(text) + 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. + +function tooltip:set_timeout(timeout) + if self.timer then + self.timer.timeout = timeout + end +end + +--- Add tooltip to an object. +-- +-- @tparam tooltip self The tooltip. +-- @tparam gears.object obj An object with `mouse::enter` and +-- `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 + +--- Remove tooltip from an object. +-- +-- @tparam tooltip self The tooltip. +-- @tparam gears.object obj An object with `mouse::enter` and +-- `mouse::leave` signals. +-- @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. +-- @tparam table args Arguments for tooltip creation. +-- @tparam function args.timer_function A function to dynamically set the +-- tooltip text. Its return value will be passed to +-- `wibox.widget.textbox.set_markup`. +-- @tparam[opt=1] number args.timeout The timeout value for +-- `timer_function`. +-- @tparam[opt] table args.objects A list of objects linked to the tooltip. +-- @tparam[opt] number args.delay_show Delay showing the tooltip by this many +-- 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 +-- @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 + local delay_timeout + + delay_timeout = timer { timeout = args.delay_show } + delay_timeout:connect_signal("timeout", function () + show(self) + delay_timeout:stop() + end) + + 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 + end + function self.hide() + if delay_timeout.started then + delay_timeout:stop() + end + hide(self) + end + else + 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() + hide(self) + end + end + + -- export functions + util.table.crush(self, tooltip, true) + + -- setup the timer action only if needed + if args.timer_function then + self.timer = timer { timeout = args.timeout and args.timeout or 1 } + self.timer_function = function() + self:set_markup(args.timer_function()) + end + 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 + + -- Set default properties + self.wibox_properties = { + visible = false, + ontop = true, + border_width = 0, + fg = fg, + bg = color.transparent, + opacity = beautiful.tooltip_opacity or 1, + } + + self.textbox = textbox() + self.textbox:set_font(font) + + -- 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.textbox, m_lr, m_lr, m_tb, m_tb) + + -- Add tooltip to objects + if args.objects then + for _, obj in ipairs(args.objects) do + self:add_to_object(obj) + 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 + +function tooltip.mt:__call(...) + return tooltip.new(...) +end + +return setmetatable(tooltip, tooltip.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |