1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
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
|