summaryrefslogtreecommitdiff
path: root/lib/awful/tooltip.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lib/awful/tooltip.lua')
-rw-r--r--lib/awful/tooltip.lua616
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