wibox.layout.align: make the middle widget really centered
[awesome.git] / lib / awful / menu.lua.in
blobba92da74ffc0f4463dacfcdf06a0ce8c535039a6
1 --------------------------------------------------------------------------------
2 -- @author Damien Leone <damien.leone@gmail.com>
3 -- @author Julien Danjou <julien@danjou.info>
4 -- @author dodo
5 -- @copyright 2008, 2011 Damien Leone, Julien Danjou, dodo
6 -- @release @AWESOME_VERSION@
7 --------------------------------------------------------------------------------
9 local wibox = require("wibox")
10 local button = require("awful.button")
11 local util = require("awful.util")
12 local tags = require("awful.tag")
13 local keygrabber = require("awful.keygrabber")
14 local beautiful = require("beautiful")
15 local object = require("gears.object")
16 local surface = require("gears.surface")
17 local cairo = require("lgi").cairo
18 local setmetatable = setmetatable
19 local tonumber = tonumber
20 local string = string
21 local ipairs = ipairs
22 local pairs = pairs
23 local pcall = pcall
24 local print = print
25 local table = table
26 local type = type
27 local math = math
28 local capi = {
29 timer = timer,
30 screen = screen,
31 mouse = mouse,
32 client = client }
35 -- awful.menu
36 local menu = { mt = {} }
39 local table_update = function (t, set)
40 for k, v in pairs(set) do
41 t[k] = v
42 end
43 return t
44 end
46 local table_merge = function (t, set)
47 for _, v in ipairs(set) do
48 table.insert(t, v)
49 end
50 end
53 --- Key bindings for menu navigation.
54 -- Keys are: up, down, exec, enter, back, close. Value are table with a list of valid
55 -- keys for the action, i.e. menu_keys.up = { "j", "k" } will bind 'j' and 'k'
56 -- key to up action. This is common to all created menu.
57 -- @class table
58 -- @name menu_keys
59 menu.menu_keys = { up = { "Up" },
60 down = { "Down" },
61 back = { "Left" },
62 exec = { "Return" },
63 enter = { "Right" },
64 close = { "Escape" } }
67 local function load_theme(a, b)
68 a = a or {}
69 b = b or {}
70 local ret = {}
71 local fallback = beautiful.get()
72 if a.reset then b = fallback end
73 if a == "reset" then a = fallback end
74 ret.border = a.border_color or b.menu_border_color or b.border_normal or
75 fallback.menu_border_color or fallback.border_normal
76 ret.border_width= a.border_width or b.menu_border_width or b.border_width or
77 fallback.menu_border_width or fallback.border_width or 0
78 ret.fg_focus = a.fg_focus or b.menu_fg_focus or b.fg_focus or
79 fallback.menu_fg_focus or fallback.fg_focus
80 ret.bg_focus = a.bg_focus or b.menu_bg_focus or b.bg_focus or
81 fallback.menu_bg_focus or fallback.bg_focus
82 ret.fg_normal = a.fg_normal or b.menu_fg_normal or b.fg_normal or
83 fallback.menu_fg_normal or fallback.fg_normal
84 ret.bg_normal = a.bg_normal or b.menu_bg_normal or b.bg_normal or
85 fallback.menu_bg_normal or fallback.bg_normal
86 ret.submenu_icon= a.submenu_icon or b.menu_submenu_icon or b.submenu_icon or
87 fallback.menu_submenu_icon or fallback.submenu_icon
88 ret.submenu = a.submenu or b.menu_submenu or b.submenu or
89 fallback.menu_submenu or fallback.submenu or "▶"
90 ret.height = a.height or b.menu_height or b.height or
91 fallback.menu_height or 16
92 ret.width = a.width or b.menu_width or b.width or
93 fallback.menu_width or 100
94 ret.font = a.font or b.font or fallback.font
95 for _, prop in ipairs({"width", "height", "menu_width"}) do
96 if type(ret[prop]) ~= "number" then ret[prop] = tonumber(ret[prop]) end
97 end
98 return ret
99 end
102 local function item_position(_menu, child)
103 local in_dir, other, a, b = 0, 0, "height", "width"
104 local dir = _menu.layout.get_dir and _menu.layout:get_dir() or "y"
105 if dir == "x" then a, b = b, a end
107 local in_dir, other = 0, _menu[b]
108 local num = util.table.hasitem(_menu.child, child)
109 if num then
110 for i = 0, num - 1 do
111 local item = _menu.items[i]
112 if item then
113 other = math.max(other, item[b])
114 in_dir = in_dir + item[a]
118 local w, h = other, in_dir
119 if dir == "x" then w, h = h, w end
120 return w, h
124 local function set_coords(_menu, screen_idx, m_coords)
125 local s_geometry = capi.screen[screen_idx].workarea
126 local screen_w = s_geometry.x + s_geometry.width
127 local screen_h = s_geometry.y + s_geometry.height
129 _menu.width = _menu.wibox.width
130 _menu.height = _menu.wibox.height
132 _menu.x = _menu.wibox.x
133 _menu.y = _menu.wibox.y
135 if _menu.parent then
136 local w, h = item_position(_menu.parent, _menu)
137 w = w + _menu.parent.theme.border_width
139 _menu.y = _menu.parent.y + h + _menu.height > screen_h and
140 screen_h - _menu.height or _menu.parent.y + h
141 _menu.x = _menu.parent.x + w + _menu.width > screen_w and
142 _menu.parent.x - _menu.width or _menu.parent.x + w
143 else
144 if m_coords == nil then
145 m_coords = capi.mouse.coords()
146 m_coords.x = m_coords.x + 1
147 m_coords.y = m_coords.y + 1
149 _menu.y = m_coords.y < s_geometry.y and s_geometry.y or m_coords.y
150 _menu.x = m_coords.x < s_geometry.x and s_geometry.x or m_coords.x
152 _menu.y = _menu.y + _menu.height > screen_h and
153 screen_h - _menu.height or _menu.y
154 _menu.x = _menu.x + _menu.width > screen_w and
155 screen_w - _menu.width or _menu.x
158 _menu.wibox.x = _menu.x
159 _menu.wibox.y = _menu.y
163 local function set_size(_menu)
164 local in_dir, other, a, b = 0, 0, "height", "width"
165 local dir = _menu.layout.get_dir and _menu.layout:get_dir() or "y"
166 if dir == "x" then a, b = b, a end
167 for _, item in ipairs(_menu.items) do
168 other = math.max(other, item[b])
169 in_dir = in_dir + item[a]
171 _menu[a], _menu[b] = in_dir, other
172 if in_dir > 0 and other > 0 then
173 _menu.wibox[a] = in_dir
174 _menu.wibox[b] = other
175 return true
177 return false
181 local function check_access_key(_menu, key)
182 for i, item in ipairs(_menu.items) do
183 if item.akey == key then
184 _menu:item_enter(i)
185 _menu:exec(i, { exec = true })
186 return
189 if _menu.parent then
190 check_access_key(_menu.parent, key)
195 local function grabber(_menu, mod, key, event)
196 if event ~= "press" then return end
198 local sel = _menu.sel or 0
199 if util.table.hasitem(menu.menu_keys.up, key) then
200 local sel_new = sel-1 < 1 and #_menu.items or sel-1
201 _menu:item_enter(sel_new)
202 elseif util.table.hasitem(menu.menu_keys.down, key) then
203 local sel_new = sel+1 > #_menu.items and 1 or sel+1
204 _menu:item_enter(sel_new)
205 elseif sel > 0 and util.table.hasitem(menu.menu_keys.enter, key) then
206 _menu:exec(sel)
207 elseif sel > 0 and util.table.hasitem(menu.menu_keys.exec, key) then
208 _menu:exec(sel, { exec = true })
209 elseif util.table.hasitem(menu.menu_keys.back, key) then
210 _menu:hide()
211 elseif util.table.hasitem(menu.menu_keys.close, key) then
212 menu.get_root(_menu):hide()
213 else
214 check_access_key(_menu, key)
219 function menu:exec(num, opts)
220 opts = opts or {}
221 local item = self.items[num]
222 if not item then return end
223 local cmd = item.cmd
224 if type(cmd) == "table" then
225 local action = cmd.cmd
226 if #cmd == 0 then
227 if opts.exec and action and type(action) == "function" then
228 action()
230 return
232 if not self.child[num] then
233 self.child[num] = menu.new(cmd, self)
235 local can_invoke_action = opts.exec and
236 action and type(action) == "function" and
237 (not opts.mouse or (opts.mouse and (self.auto_expand or
238 (self.active_child == self.child[num] and
239 self.active_child.wibox.visible))))
240 if can_invoke_action then
241 local visible = action(self.child[num], item)
242 if not visible then
243 menu.get_root(self):hide()
244 return
245 else
246 self.child[num]:update()
249 if self.active_child and self.active_child ~= self.child[num] then
250 self.active_child:hide()
252 self.active_child = self.child[num]
253 if not self.active_child.visible then
254 self.active_child:show()
256 elseif type(cmd) == "string" then
257 menu.get_root(self):hide()
258 util.spawn(cmd)
259 elseif type(cmd) == "function" then
260 local visible, action = cmd(item, self)
261 if not visible then
262 menu.get_root(self):hide()
263 else
264 self:update()
265 if self.items[num] then
266 self:item_enter(num, opts)
269 if action and type(action) == "function" then
270 action()
275 function menu:item_enter(num, opts)
276 opts = opts or {}
277 local item = self.items[num]
278 if num == nil or self.sel == num or not item then
279 return
280 elseif self.sel then
281 self:item_leave(self.sel)
283 --print("sel", num, menu.sel, item.theme.bg_focus)
284 item._background:set_fg(item.theme.fg_focus)
285 item._background:set_bg(item.theme.bg_focus)
286 self.sel = num
288 if self.auto_expand and opts.hover then
289 if self.active_child then
290 self.active_child:hide()
291 self.active_child = nil
294 if type(item.cmd) == "table" then
295 self:exec(num, opts)
301 function menu:item_leave(num)
302 --print("leave", num)
303 local item = self.items[num]
304 if item then
305 item._background:set_fg(item.theme.fg_normal)
306 item._background:set_bg(item.theme.bg_normal)
311 --- Show a menu.
312 -- @param args.coords Menu position defaulting to mouse.coords()
313 function menu:show(args)
314 args = args or {}
315 local coords = args.coords or nil
316 local screen_index = capi.mouse.screen
318 if not set_size(self) then return end
319 set_coords(self, screen_index, coords)
321 keygrabber.run(self._keygrabber)
322 self.wibox.visible = true
325 --- Hide a menu popup.
326 function menu:hide()
327 -- Remove items from screen
328 for i = 1, #self.items do
329 self:item_leave(i)
331 if self.active_child then
332 self.active_child:hide()
333 self.active_child = nil
335 self.sel = nil
337 keygrabber.stop(self._keygrabber)
338 self.wibox.visible = false
341 --- Toggle menu visibility.
342 -- @param _menu The menu to show if it's hidden, or to hide if it's shown.
343 -- @param args.coords Menu position {x,y}
344 function menu:toggle(args)
345 if self.wibox.visible then
346 self:hide()
347 else
348 self:show(args)
352 --- Update menu content
353 function menu:update()
354 if self.wibox.visible then
355 self:show({ coords = { x = self.x, y = self.y } })
360 --- Get the elder parent so for example when you kill
361 -- it, it will destroy the whole family.
362 function menu:get_root()
363 return self.parent and menu.get_root(self.parent) or self
366 --- Add a new menu entry.
367 -- args.new (Default: awful.menu.entry) The menu entry constructor.
368 -- args.theme (Optional) The menu entry theme.
369 -- args.* params needed for the menu entry constructor.
370 -- @param args The item params
371 -- @param index (Optional) the index where the new entry will inserted
372 function menu:add(args, index)
373 if not args then return end
374 local theme = load_theme(args.theme or {}, self.theme)
375 args.theme = theme
376 args.new = args.new or menu.entry
377 local success, item = pcall(args.new, self, args)
378 if not success then
379 print("Error while creating menu entry: " .. item)
380 return
382 if not item.widget then
383 print("Error while checking menu entry: no property widget found.")
384 return
386 item.parent = self
387 item.theme = item.theme or theme
388 item.width = item.width or theme.width
389 item.height = item.height or theme.height
390 wibox.widget.base.check_widget(item.widget)
391 item._background = wibox.widget.background()
392 item._background:set_widget(item.widget)
393 item._background:set_fg(item.theme.fg_normal)
394 item._background:set_bg(item.theme.bg_normal)
397 -- Create bindings
398 item._background:buttons(util.table.join(
399 button({}, 3, function () self:hide() end),
400 button({}, 1, function ()
401 local num = util.table.hasitem(self.items, item)
402 self:item_enter(num, { mouse = true })
403 self:exec(num, { exec = true, mouse = true })
404 end )))
407 item._mouse = function ()
408 local num = util.table.hasitem(self.items, item)
409 self:item_enter(num, { hover = true, moue = true })
411 item.widget:connect_signal("mouse::enter", item._mouse)
413 if index then
414 self.layout:reset()
415 table.insert(self.items, index, item)
416 for _, i in ipairs(self.items) do
417 self.layout:add(i._background)
419 else
420 table.insert(self.items, item)
421 self.layout:add(item._background)
423 return item
426 -- Delete menu entry at given position
427 -- @param _menu The menu
428 -- @param num The position in the table of the menu entry to be deleted; can be also the menu entry itself
429 function menu:delete(num)
430 if type(num) == "table" then
431 num = util.table.hasitem(self.items, num)
433 local item = self.items[num]
434 if not item then return end
435 item.widget:disconnect_signal("mouse::enter", item._mouse)
436 item.widget.visible = false
437 table.remove(self.items, num)
438 if self.sel == num then
439 self:item_leave(self.sel)
440 self.sel = nil
442 self.layout:reset()
443 for _, i in ipairs(self.items) do
444 self.layout:add(i._background)
446 if self.child[num] then
447 self.child[num]:hide()
448 if self.active_child == self.child[num] then
449 self.active_child = nil
451 table.remove(self.child, num)
455 --------------------------------------------------------------------------------
457 --- Build a popup menu with running clients and shows it.
458 -- @param _menu Menu table, see new() function for more informations
459 -- @return The menu.
460 function menu:clients(args) -- FIXME crude api
461 _menu = self or {}
462 local cls = capi.client.get()
463 local cls_t = {}
464 for k, c in pairs(cls) do
465 cls_t[#cls_t + 1] = {
466 util.escape(c.name) or "",
467 function ()
468 if not c:isvisible() then
469 tags.viewmore(c:tags(), c.screen)
471 capi.client.focus = c
472 c:raise()
473 end,
474 c.icon }
476 args = args or {}
477 args.items = args.items or {}
478 table_merge(args.items, cls_t)
480 local m = menu.new(args)
481 m:show(args)
482 return m
485 --------------------------------------------------------------------------------
487 --- Default awful.menu.entry constructor
488 -- @param parent The parent menu
489 -- @param args the item params
490 -- @return table with 'widget', 'cmd', 'akey' and all the properties the user wants to change
491 function menu.entry(parent, args)
492 args = args or {}
493 args.text = args[1] or args.text or ""
494 args.cmd = args[2] or args.cmd
495 args.icon = args[3] or args.icon
496 local ret = {}
497 -- Create the item label widget
498 local label = wibox.widget.textbox()
499 local key = ''
500 label:set_font(args.theme.font)
501 label:set_markup(string.gsub(
502 util.escape(args.text), "&amp;(%w)",
503 function (l)
504 key = string.lower(l)
505 return "<u>" .. l .. "</u>"
506 end, 1))
507 -- Set icon if needed
508 local icon, iconbox
509 local margin = wibox.layout.margin()
510 margin:set_widget(label)
511 if args.icon then
512 icon = surface.load(args.icon)
514 if icon then
515 local iw = icon:get_width()
516 local ih = icon:get_height()
517 if iw > args.theme.width or ih > args.theme.height then
518 local w, h
519 if ((args.theme.height / ih) * iw) > args.theme.width then
520 w, h = args.theme.height, (args.theme.height / iw) * ih
521 else
522 w, h = (args.theme.height / ih) * iw, args.theme.height
524 -- We need to scale the image to size w x h
525 local img = cairo.ImageSurface(cairo.Format.ARGB32, w, h)
526 local cr = cairo.Context(img)
527 cr:scale(w / iw, h / ih)
528 cr:set_source_surface(icon, 0, 0)
529 cr:paint()
530 icon = img
532 iconbox = wibox.widget.imagebox()
533 if iconbox:set_image(icon) then
534 margin:set_left(2)
535 else
536 iconbox = nil
539 if not iconbox then
540 margin:set_left(args.theme.height + 2)
542 -- Create the submenu icon widget
543 local submenu
544 if type(args.cmd) == "table" then
545 if args.theme.submenu_icon then
546 submenu = wibox.widget.imagebox()
547 submenu:set_image(surface.load(args.theme.submenu_icon))
548 else
549 submenu = wibox.widget.textbox()
550 submenu:set_font(args.theme.font)
551 submenu:set_text(args.theme.submenu)
554 -- Add widgets to the wibox
555 local left = wibox.layout.fixed.horizontal()
556 if iconbox then
557 left:add(iconbox)
559 -- This contains the label
560 left:add(margin)
562 local layout = wibox.layout.align.horizontal()
563 layout:set_left(left)
564 if submenu then
565 layout:set_right(submenu)
568 return table_update(ret, {
569 label = label,
570 sep = submenu,
571 icon = iconbox,
572 widget = layout,
573 cmd = args.cmd,
574 akey = key,
578 --------------------------------------------------------------------------------
580 --- Create a menu popup.
581 -- @param args Table containing the menu informations.<br/>
582 -- <ul>
583 -- <li> Key items: Table containing the displayed items. Each element is a table by default (when element 'new' is awful.menu.entry) containing: item name, triggered action, submenu table or function, item icon (optional). </li>
584 -- <li> Keys theme.[fg|bg]_[focus|normal], theme.border_color, theme.border_width, theme.submenu_icon, theme.height and theme.width override the default display for your menu and/or of your menu entry, each of them are optional. </li>
585 -- <li> Key auto_expand controls the submenu auto expand behaviour by setting it to true (default) or false. </li>
586 -- </ul>
587 -- @param parent Specify the parent menu if we want to open a submenu, this value should never be set by the user.
588 -- @usage The following function builds, and shows a menu of clients that match
589 -- a particular rule. Bound to a key, it can for example be used to select from
590 -- dozens of terminals open on several tags. With the use of
591 -- <code>match_any</code> instead of <code>match</code>, menu of clients with
592 -- different classes can also be build.
594 -- <p><code>
595 -- function terminal_menu () <br/>
596 -- &nbsp; terms = {} <br/>
597 -- &nbsp; for i, c in pairs(client.get()) do <br/>
598 -- &nbsp;&nbsp; if awful.rules.match(c, {class = "URxvt"}) then <br/>
599 -- &nbsp;&nbsp;&nbsp; terms[i] = <br/>
600 -- &nbsp;&nbsp;&nbsp; {c.name, <br/>
601 -- &nbsp;&nbsp;&nbsp; function() <br/>
602 -- &nbsp;&nbsp;&nbsp;&nbsp; awful.tag.viewonly(c:tags()[1]) <br/>
603 -- &nbsp;&nbsp;&nbsp;&nbsp; client.focus = c <br/>
604 -- &nbsp;&nbsp;&nbsp; end, <br/>
605 -- &nbsp;&nbsp;&nbsp; c.icon <br/>
606 -- &nbsp;&nbsp;&nbsp; } <br/>
607 -- &nbsp;&nbsp; end <br/>
608 -- &nbsp; end <br/>
609 -- &nbsp; awful.menu(terms):show() <br/>
610 -- end <br/>
611 --</code></p>
612 function menu.new(args, parent)
613 args = args or {}
614 args.layout = args.layout or wibox.layout.flex.vertical
615 local _menu = table_update(object(), {
616 item_enter = menu.item_enter,
617 item_leave = menu.item_leave,
618 get_root = menu.get_root,
619 delete = menu.delete,
620 update = menu.update,
621 toggle = menu.toggle,
622 hide = menu.hide,
623 show = menu.show,
624 exec = menu.exec,
625 add = menu.add,
626 child = {},
627 items = {},
628 parent = parent,
629 layout = args.layout(),
630 theme = load_theme(args.theme or {}, parent and parent.theme) })
632 if parent then
633 _menu.auto_expand = parent.auto_expand
634 elseif args.auto_expand ~= nil then
635 _menu.auto_expand = args.auto_expand
636 else
637 _menu.auto_expand = true
640 -- Create items
641 for i, v in ipairs(args) do _menu:add(v) end
642 if args.items then
643 for i, v in pairs(args.items) do _menu:add(v) end
646 _menu._keygrabber = function (...)
647 grabber(_menu, ...)
650 _menu.wibox = wibox({
651 ontop = true,
652 fg = _menu.theme.fg_normal,
653 bg = _menu.theme.bg_normal,
654 border_color = _menu.theme.border,
655 border_width = _menu.theme.border_width,
656 type = "popup_menu" })
657 _menu.wibox.visible = false
658 _menu.wibox:set_widget(_menu.layout)
659 set_size(_menu)
661 _menu.x = _menu.wibox.x
662 _menu.y = _menu.wibox.y
663 return _menu
666 function menu.mt:__call(...)
667 return menu.new(...)
670 return setmetatable(menu, menu.mt)
672 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80