awful.menu: Port to new widget system
[awesome.git] / lib / awful / menu.lua.in
blob9cd2352d353913341861d9606744f18a4a47c99e
1 ---------------------------------------------------------------------------
2 -- @author Damien Leone <damien.leone@gmail.com>
3 -- @author Julien Danjou <julien@danjou.info>
4 -- @copyright 2008 Damien Leone, Julien Danjou
5 -- @release @AWESOME_VERSION@
6 ---------------------------------------------------------------------------
8 -- Grab environment we need
9 local pairs = pairs
10 local table = table
11 local string = string
12 local type = type
13 local setmetatable = setmetatable
14 local tonumber = tonumber
15 local capi =
17 screen = screen,
18 mouse = mouse,
19 client = client,
20 keygrabber = keygrabber,
21 oocairo = oocairo
23 local wibox = require("wibox")
24 local button = require("awful.button")
25 local util = require("awful.util")
26 local tags = require("awful.tag")
27 local awbeautiful = require("beautiful")
29 --- Creation of menus.
30 module("awful.menu")
32 local cur_menu
34 --- Key bindings for menu navigation.
35 -- Keys are: up, down, exec, back, close. Value are table with a list of valid
36 -- keys for the action, i.e. menu_keys.up = { "j", "k" } will bind 'j' and 'k'
37 -- key to up action. This is common to all created menu.
38 -- @class table
39 -- @name menu_keys
40 menu_keys = { up = { "Up" },
41 down = { "Down" },
42 exec = { "Return", "Right" },
43 back = { "Left" },
44 close = { "Escape" } }
46 local function load_theme(custom)
47 local theme = {}
48 local beautiful
50 beautiful = awbeautiful.get()
52 theme.fg_focus = custom.fg_focus or beautiful.menu_fg_focus or beautiful.fg_focus
53 theme.bg_focus = custom.bg_focus or beautiful.menu_bg_focus or beautiful.bg_focus
54 theme.fg_normal = custom.fg_normal or beautiful.menu_fg_normal or beautiful.fg_normal
55 theme.bg_normal = custom.bg_normal or beautiful.menu_bg_normal or beautiful.bg_normal
57 theme.submenu_icon = custom.submenu_icon or beautiful.menu_submenu_icon
59 theme.menu_height = custom.height or beautiful.menu_height or 16
60 theme.menu_width = custom.width or beautiful.menu_width or 100
62 theme.border = custom.border_color or beautiful.menu_border_color or beautiful.border_normal
63 theme.border_width = custom.border_width or beautiful.menu_border_width or beautiful.border_width
65 return theme
66 end
68 local function item_leave(menu, num)
69 if num > 0 then
70 menu.items[num].wibox:set_fg(menu.theme.fg_normal)
71 menu.items[num].wibox:set_bg(menu.theme.bg_normal)
72 end
73 end
75 --- Hide a menu popup.
76 -- @param menu The menu to hide.
77 function hide(menu)
78 -- Remove items from screen
79 for i = 1, #menu.items do
80 item_leave(menu, i)
81 menu.items[i].wibox.screen = nil
82 end
83 if menu.active_child then
84 menu.active_child:hide()
85 menu.active_child = nil
86 end
87 menu.sel = nil
89 if cur_menu == menu then
90 cur_menu = cur_menu.parent
91 end
92 if not cur_menu and menu.keygrabber then
93 capi.keygrabber.stop()
94 end
95 end
97 -- Get the elder parent so for example when you kill
98 -- it, it will destroy the whole family.
99 local function get_parents(menu)
100 if menu.parent then
101 return get_parents(menu.parent)
103 return menu
106 local function exec(menu, num, mouse_event)
107 local cmd = menu.items[num].cmd
108 if type(cmd) == "table" then
109 if #cmd == 0 then
110 return
112 if not menu.child[num] then
113 menu.child[num] = new({ items = cmd }, menu, num)
116 if menu.active_child then
117 menu.active_child:hide()
118 menu.active_child = nil
120 menu.active_child = menu.child[num]
121 menu.active_child:show()
122 elseif type(cmd) == "string" then
123 get_parents(menu):hide()
124 util.spawn(cmd)
125 elseif type(cmd) == "function" then
126 get_parents(menu):hide()
127 cmd(menu.items[num].returned_value)
131 local function item_enter(menu, num, mouse_event)
132 if menu.sel == num then
133 return
134 elseif menu.sel then
135 item_leave(menu, menu.sel)
138 menu.items[num].wibox:set_fg(menu.theme.fg_focus)
139 menu.items[num].wibox:set_bg(menu.theme.bg_focus)
140 menu.sel = num
141 cur_menu = menu
143 if menu.auto_expand and mouse_event then
144 if menu.active_child then
145 menu.active_child:hide()
146 menu.active_child = nil
149 if type(menu.items[num].cmd) == "table" then
150 exec(menu, num)
155 local function check_access_key(menu, key)
156 for i, item in pairs(menu.items) do
157 if item.akey == key then
158 item_enter(menu, i)
159 exec(menu, i)
160 return
163 if menu.parent then
164 check_access_key(menu.parent, key)
168 local function grabber(mod, key, event)
169 if event == "release" then
170 return true
173 local sel = cur_menu.sel or 0
174 if util.table.hasitem(menu_keys.up, key) then
175 local sel_new = sel-1 < 1 and #cur_menu.items or sel-1
176 item_enter(cur_menu, sel_new)
177 elseif util.table.hasitem(menu_keys.down, key) then
178 local sel_new = sel+1 > #cur_menu.items and 1 or sel+1
179 item_enter(cur_menu, sel_new)
180 elseif sel > 0 and util.table.hasitem(menu_keys.exec, key) then
181 exec(cur_menu, sel)
182 elseif util.table.hasitem(menu_keys.back, key) then
183 cur_menu:hide()
184 elseif util.table.hasitem(menu_keys.close, key) then
185 get_parents(cur_menu):hide()
186 else
187 check_access_key(cur_menu, key)
190 return true
193 local function add_item(data, num, item_info)
194 local item = wibox({
195 fg = data.theme.fg_normal,
196 bg = data.theme.bg_normal,
197 border_color = data.theme.border,
198 border_width = data.theme.border_width,
199 type = "popup_menu"
202 -- Create bindings
203 local bindings = util.table.join(
204 button({}, 1, function () item_enter(data, num); exec(data, num) end),
205 button({}, 3, function () data:hide() end)
208 -- Create the item label widget
209 local label = wibox.widget.textbox()
210 local key = ''
211 label.text = string.gsub(util.escape(item_info[1]), "&amp;(%w)",
212 function (l)
213 key = string.lower(l)
214 return "<u>"..l.."</u>"
215 end, 1)
216 -- Set icon if needed
217 local iconbox
218 local margin = wibox.layout.margin()
219 margin:set_widget(label)
220 if item_info[3] then
221 local icon = type(item_info[3]) == "string" and capi.oocairo.image_surface_create_from_png(item_info[3]) or item_info[3]
222 local width = icon:get_width()
223 local height = icon:get_height()
224 if width > data.h or height > data.h then
225 local w, h
226 if ((data.h/height) * width) > data.h then
227 w, h = data.h, (data.h / width) * height
228 else
229 w, h = (data.h / height) * width, data.h
231 -- We need to scale the image to size w x h
232 local img = capi.oocairo.image_surface_create("argb32", w, h)
233 local cr = capi.oocairo.context_create(img)
234 cr:scale(w / width, h / height)
235 cr:set_source(icon, 0, 0)
236 cr:paint()
237 icon = img
239 iconbox = wibox.widget.imagebox()
240 iconbox:set_image(icon)
241 margin:set_left(2)
242 else
243 margin:set_left(data.h + 2)
246 item:buttons(bindings)
248 local mouse_enter_func = function () item_enter(data, num, true) end
249 item:connect_signal("mouse::enter", mouse_enter_func)
251 -- Create the submenu icon widget
252 local submenu
253 if type(item_info[2]) == "table" then
254 submenu = wibox.widget.imagebox()
255 if data.theme.submenu_icon then
256 submenu:set_image(capi.oocairo.image_surface_create_from_png(data.theme.submenu_icon))
258 submenu:buttons(bindings)
261 -- Add widgets to the wibox
262 local left = wibox.layout.fixed.horizontal()
263 if iconbox then
264 left:add(iconbox)
266 -- This contains the label
267 left:add(margin)
269 local layout = wibox.layout.align.horizontal()
270 layout:set_left(left)
271 if submenu then
272 layout:set_right(submenu)
275 item:set_widget(layout)
277 local w, h = label:fit(0, 0)
278 item.height = h + 2
279 item.ontop = true
281 return { wibox = item, akey= key, cmd = item_info[2], returned_value=item_info[1] }
284 --- Build a popup menu with running clients and shows it.
285 -- @param menu Menu table, see new() function for more informations
286 -- @param args.keygrabber A boolean enabling or not the keyboard navigation.
287 -- @return The menu.
288 function clients(menu, args)
289 local cls = capi.client.get()
290 local cls_t = {}
291 for k, c in pairs(cls) do
292 cls_t[#cls_t + 1] = { util.escape(c.name) or "",
293 function ()
294 if not c:isvisible() then
295 tags.viewmore(c:tags(), c.screen)
297 capi.client.focus = c
298 c:raise()
299 end,
300 c.icon }
303 if not menu then
304 menu = {}
307 menu.items = cls_t
309 local m = new(menu)
310 m:show(args)
311 return m
314 local function set_coords(menu, screen_idx, m_coords)
315 local s_geometry = capi.screen[screen_idx].workarea
316 local screen_w = s_geometry.x + s_geometry.width
317 local screen_h = s_geometry.y + s_geometry.height
319 local i_h = menu.h + menu.theme.border_width
320 local m_h = (i_h * #menu.items) + menu.theme.border_width
322 if menu.parent then
323 menu.w = menu.parent.w
324 menu.h = menu.parent.h
326 local p_w = i_h * (menu.num - 1)
327 local m_w = menu.w - menu.theme.border_width
329 menu.y = menu.parent.y + p_w + m_h > screen_h and screen_h - m_h or menu.parent.y + p_w
330 menu.x = menu.parent.x + m_w*2 > screen_w and menu.parent.x - m_w or menu.parent.x + m_w
331 else
332 local m_w = menu.w
333 if m_coords == nil then
334 m_coords = capi.mouse.coords()
335 m_coords.x = m_coords.x + 1
336 m_coords.y = m_coords.y + 1
339 menu.y = m_coords.y < s_geometry.y and s_geometry.y or m_coords.y
340 menu.x = m_coords.x < s_geometry.x and s_geometry.x or m_coords.x
342 menu.y = menu.y + m_h > screen_h and screen_h - m_h or menu.y
343 menu.x = menu.x + m_w > screen_w and screen_w - m_w or menu.x
347 --- Show a menu.
348 -- @param menu The menu to show.
349 -- @param args.keygrabber A boolean enabling or not the keyboard navigation.
350 -- @param args.coords Menu position defaulting to mouse.coords()
351 function show(menu, args)
352 args = args or {}
353 local screen_index = capi.mouse.screen
354 local keygrabber = args.keygrabber or false
355 local coords = args.coords or nil
356 set_coords(menu, screen_index, coords)
357 for num, item in pairs(menu.items) do
358 local wibox = item.wibox
359 wibox.width = menu.w
360 wibox.height = menu.h
361 wibox.x = menu.x
362 wibox.y = menu.y + (num - 1) * (menu.h + wibox.border_width)
363 wibox.screen = screen_index
366 if menu.parent then
367 menu.keygrabber = menu.parent.keygrabber
368 elseif keygrabber ~= nil then
369 menu.keygrabber = keygrabber
370 else
371 menu.keygrabber = false
374 if not cur_menu and menu.keygrabber then
375 capi.keygrabber.run(grabber)
377 cur_menu = menu
380 --- Toggle menu visibility.
381 -- @param menu The menu to show if it's hidden, or to hide if it's shown.
382 -- @param args.keygrabber A boolean enabling or not the keyboard navigation.
383 -- @param args.coords Menu position {x,y}
384 function toggle(menu, args)
385 if menu.items[1] and menu.items[1].wibox.screen then
386 menu:hide()
387 else
388 menu:show(args)
392 --- Open a menu popup.
393 -- @param menu Table containing the menu informations. Key items: Table containing the displayed items, each element is a tab containing: item name, triggered action, submenu table or function, item icon (optional). Keys [fg|bg]_[focus|normal], border, border_width, submenu_icon, height and width override the default display for your menu, each of them are optional. Key auto_expand controls the submenu auto expand behaviour by setting it to true (default) or false.
394 -- @param parent Specify the parent menu if we want to open a submenu, this value should never be set by the user.
395 -- @param num Specify the parent's clicked item number if we want to open a submenu, this value should never be set by the user.
396 function new(menu, parent, num)
397 -- Create a table to store our menu informations
398 local data = {}
400 data.items = {}
401 data.num = num or 1
402 data.theme = parent and parent.theme or load_theme(menu)
403 data.parent = parent
404 data.child = {}
405 if parent then
406 data.auto_expand = parent.auto_expand
407 elseif menu.auto_expand ~= nil then
408 data.auto_expand = menu.auto_expand
409 else
410 data.auto_expand = true
412 data.h = parent and parent.h or data.theme.menu_height
413 if type(data.h) ~= 'number' then data.h = tonumber(data.h) end
414 data.w = parent and parent.w or data.theme.menu_width
415 if type(data.w) ~= 'number' then data.w = tonumber(data.w) end
417 -- Create items
418 for k, v in pairs(menu.items) do
419 table.insert(data.items, add_item(data, k, v))
422 if #data.items > 0 and data.h < data.items[1].wibox.height then
423 data.h = data.items[1].wibox.height
426 -- Set methods
427 data.hide = hide
428 data.show = show
429 data.toggle = toggle
431 return data
434 setmetatable(_M, { __call = function(_, ...) return new(...) end })
436 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80