1 --------------------------------------------------------------------------------
2 -- @author Damien Leone <damien.leone@gmail.com>
3 -- @author Julien Danjou <julien@danjou.info>
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
36 local menu
= { mt
= {} }
39 local table_update
= function (t
, set
)
40 for k
, v
in pairs(set
) do
46 local table_merge
= function (t
, set
)
47 for _
, v
in ipairs(set
) do
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.
59 menu
.menu_keys
= { up
= { "Up" },
64 close
= { "Escape" } }
67 local function load_theme(a
, b
)
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
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
)
110 for i
= 0, num
- 1 do
111 local item
= _menu
.items
[i
]
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
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
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
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
181 local function check_access_key(_menu
, key
)
182 for i
, item
in ipairs(_menu
.items
) do
183 if item
.akey
== key
then
185 _menu
:exec(i
, { exec
= true })
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
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
211 elseif util
.table.hasitem(menu
.menu_keys
.close
, key
) then
212 menu
.get_root(_menu
):hide()
214 check_access_key(_menu
, key
)
219 function menu
:exec(num
, opts
)
221 local item
= self
.items
[num
]
222 if not item
then return end
224 if type(cmd
) == "table" then
225 local action
= cmd
.cmd
227 if opts
.exec
and action
and type(action
) == "function" then
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
)
243 menu
.get_root(self
):hide()
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()
259 elseif type(cmd
) == "function" then
260 local visible
, action
= cmd(item
, self
)
262 menu
.get_root(self
):hide()
265 if self
.items
[num
] then
266 self
:item_enter(num
, opts
)
269 if action
and type(action
) == "function" then
275 function menu
:item_enter(num
, opts
)
277 local item
= self
.items
[num
]
278 if num
== nil or self
.sel
== num
or not item
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
)
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
301 function menu
:item_leave(num
)
302 --print("leave", num)
303 local item
= self
.items
[num
]
305 item
._background
:set_fg(item
.theme
.fg_normal
)
306 item
._background
:set_bg(item
.theme
.bg_normal
)
312 -- @param args.coords Menu position defaulting to mouse.coords()
313 function menu
:show(args
)
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.
327 -- Remove items from screen
328 for i
= 1, #self
.items
do
331 if self
.active_child
then
332 self
.active_child
:hide()
333 self
.active_child
= 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
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
)
376 args
.new
= args
.new
or menu
.entry
377 local success
, item
= pcall(args
.new
, self
, args
)
379 print("Error while creating menu entry: " .. item
)
382 if not item
.widget
then
383 print("Error while checking menu entry: no property widget found.")
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
)
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 })
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
)
415 table.insert(self
.items
, index
, item
)
416 for _
, i
in ipairs(self
.items
) do
417 self
.layout
:add(i
._background
)
420 table.insert(self
.items
, item
)
421 self
.layout
:add(item
._background
)
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
)
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
460 function menu
:clients(args
) -- FIXME crude api
462 local cls
= capi
.client
.get()
464 for k
, c
in pairs(cls
) do
465 cls_t
[#cls_t
+ 1] = {
466 util
.escape(c
.name
) or "",
468 if not c
:isvisible() then
469 tags
.viewmore(c
:tags(), c
.screen
)
471 capi
.client
.focus
= c
477 args
.items
= args
.items
or {}
478 table_merge(args
.items
, cls_t
)
480 local m
= menu
.new(args
)
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
)
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
497 -- Create the item label widget
498 local label
= wibox
.widget
.textbox()
500 label
:set_font(args
.theme
.font
)
501 label
:set_markup(string.gsub(
502 util
.escape(args
.text
), "&(%w)",
504 key
= string.lower(l
)
505 return "<u>" .. l
.. "</u>"
507 -- Set icon if needed
509 local margin
= wibox
.layout
.margin()
510 margin
:set_widget(label
)
512 icon
= surface
.load(args
.icon
)
515 local iw
= icon
:get_width()
516 local ih
= icon
:get_height()
517 if iw
> args
.theme
.width
or ih
> args
.theme
.height
then
519 if ((args
.theme
.height
/ ih
) * iw
) > args
.theme
.width
then
520 w
, h
= args
.theme
.height
, (args
.theme
.height
/ iw
) * ih
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)
532 iconbox
= wibox
.widget
.imagebox()
533 if iconbox
:set_image(icon
) then
540 margin
:set_left(args
.theme
.height
+ 2)
542 -- Create the submenu icon widget
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
))
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()
559 -- This contains the label
562 local layout
= wibox
.layout
.align
.horizontal()
563 layout
:set_left(left
)
565 layout
:set_right(submenu
)
568 return table_update(ret
, {
578 --------------------------------------------------------------------------------
580 --- Create a menu popup.
581 -- @param args Table containing the menu informations.<br/>
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>
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.
595 -- function terminal_menu () <br/>
596 -- terms = {} <br/>
597 -- for i, c in pairs(client.get()) do <br/>
598 -- if awful.rules.match(c, {class = "URxvt"}) then <br/>
599 -- terms[i] = <br/>
600 -- {c.name, <br/>
601 -- function() <br/>
602 -- awful.tag.viewonly(c:tags()[1]) <br/>
603 -- client.focus = c <br/>
604 -- end, <br/>
605 -- c.icon <br/>
606 -- } <br/>
607 -- end <br/>
609 -- awful.menu(terms):show() <br/>
612 function menu
.new(args
, parent
)
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
,
629 layout
= args
.layout(),
630 theme
= load_theme(args
.theme
or {}, parent
and parent
.theme
) })
633 _menu
.auto_expand
= parent
.auto_expand
634 elseif args
.auto_expand
~= nil then
635 _menu
.auto_expand
= args
.auto_expand
637 _menu
.auto_expand
= true
641 for i
, v
in ipairs(args
) do _menu
:add(v
) end
643 for i
, v
in pairs(args
.items
) do _menu
:add(v
) end
646 _menu
._keygrabber
= function (...)
650 _menu
.wibox
= wibox({
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
)
661 _menu
.x
= _menu
.wibox
.x
662 _menu
.y
= _menu
.wibox
.y
666 function menu
.mt
:__call(...)
670 return setmetatable(menu
, menu
.mt
)
672 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80