awful.menu: Fix menu hide/show overloading
[awesome.git] / lib / awful / menu.lua.in
blob72d6a5667138c4e07883a10d070724d2eaef8509
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 wibox = wibox
15 local image = image
16 local widget = widget
17 local button = require("awful.button")
18 local capi =
20 screen = screen,
21 mouse = mouse,
22 client = client,
23 keygrabber = keygrabber
25 local util = require("awful.util")
26 local tags = require("awful.tag")
27 local layout = require("awful.widget.layout")
28 local awbeautiful = require("beautiful")
29 local tonumber = tonumber
31 --- Creation of menus.
32 module("awful.menu")
34 local cur_menu
36 --- Key bindings for menu navigation.
37 -- Keys are: up, down, exec, back, close. Value are table with a list of valid
38 -- keys for the action, i.e. menu_keys.up = { "j", "k" } will bind 'j' and 'k'
39 -- key to up action. This is common to all created menu.
40 -- @class table
41 -- @name menu_keys
42 menu_keys = { up = { "Up" },
43 down = { "Down" },
44 exec = { "Return", "Right" },
45 back = { "Left" },
46 close = { "Escape" } }
48 local function load_theme(custom)
49 local theme = {}
50 local beautiful
52 beautiful = awbeautiful.get()
54 theme.fg_focus = custom.fg_focus or beautiful.menu_fg_focus or beautiful.fg_focus
55 theme.bg_focus = custom.bg_focus or beautiful.menu_bg_focus or beautiful.bg_focus
56 theme.fg_normal = custom.fg_normal or beautiful.menu_fg_normal or beautiful.fg_normal
57 theme.bg_normal = custom.bg_normal or beautiful.menu_bg_normal or beautiful.bg_normal
59 theme.submenu_icon = custom.submenu_icon or beautiful.menu_submenu_icon
61 theme.menu_height = custom.height or beautiful.menu_height or 16
62 theme.menu_width = custom.width or beautiful.menu_width or 100
64 theme.border = custom.border_color or beautiful.menu_border_color or beautiful.border_normal
65 theme.border_width = custom.border_width or beautiful.menu_border_width or beautiful.border_width
67 return theme
68 end
70 local function item_leave(menu, num)
71 if num > 0 then
72 menu.items[num].wibox.fg = menu.theme.fg_normal
73 menu.items[num].wibox.bg = menu.theme.bg_normal
74 end
75 end
77 --- Hide a menu popup.
78 -- @param menu The menu to hide.
79 function hide(menu)
80 -- Remove items from screen
81 for i = 1, #menu.items do
82 item_leave(menu, i)
83 menu.items[i].wibox.screen = nil
84 end
85 if menu.active_child then
86 menu.active_child:hide()
87 menu.active_child = nil
88 end
89 menu.sel = nil
91 if cur_menu == menu then
92 cur_menu = cur_menu.parent
93 end
94 if not cur_menu and menu.keygrabber then
95 capi.keygrabber.stop()
96 end
97 end
99 -- Get the elder parent so for example when you kill
100 -- it, it will destroy the whole family.
101 local function get_parents(menu)
102 if menu.parent then
103 return get_parents(menu.parent)
105 return menu
108 local function exec(menu, num, mouse_event)
109 local cmd = menu.items[num].cmd
110 if type(cmd) == "table" then
111 if #cmd == 0 then
112 return
114 if not menu.child[num] then
115 menu.child[num] = new({ items = cmd }, menu, num)
118 if menu.active_child then
119 menu.active_child:hide()
120 menu.active_child = nil
122 menu.active_child = menu.child[num]
123 menu.active_child:show()
124 elseif type(cmd) == "string" then
125 get_parents(menu):hide()
126 util.spawn(cmd)
127 elseif type(cmd) == "function" then
128 get_parents(menu):hide()
129 cmd(menu.items[num].returned_value)
133 local function item_enter(menu, num, mouse_event)
134 if menu.sel == num then
135 return
136 elseif menu.sel then
137 item_leave(menu, menu.sel)
140 menu.items[num].wibox.fg = menu.theme.fg_focus
141 menu.items[num].wibox.bg = menu.theme.bg_focus
142 menu.sel = num
143 cur_menu = menu
145 if menu.auto_expand and mouse_event then
146 if menu.active_child then
147 menu.active_child:hide()
148 menu.active_child = nil
151 if type(menu.items[num].cmd) == "table" then
152 exec(menu, num)
157 local function check_access_key(menu, key)
158 for i, item in pairs(menu.items) do
159 if item.akey == key then
160 exec(menu, i)
161 return
164 if menu.parent then
165 check_access_key(menu.parent, key)
169 local function grabber(mod, key, event)
170 if event == "release" then
171 return true
174 local sel = cur_menu.sel or 0
175 if util.table.hasitem(menu_keys.up, key) then
176 local sel_new = sel-1 < 1 and #cur_menu.items or sel-1
177 item_enter(cur_menu, sel_new)
178 elseif util.table.hasitem(menu_keys.down, key) then
179 local sel_new = sel+1 > #cur_menu.items and 1 or sel+1
180 item_enter(cur_menu, sel_new)
181 elseif sel > 0 and util.table.hasitem(menu_keys.exec, key) then
182 exec(cur_menu, sel)
183 elseif util.table.hasitem(menu_keys.back, key) then
184 cur_menu:hide()
185 elseif util.table.hasitem(menu_keys.close, key) then
186 get_parents(cur_menu):hide()
187 else
188 check_access_key(cur_menu, key)
191 return true
194 local function add_item(data, num, item_info)
195 local item = wibox({
196 fg = data.theme.fg_normal,
197 bg = data.theme.bg_normal,
198 border_color = data.theme.border,
199 border_width = data.theme.border_width
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 = widget({ type = "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 if item_info[3] then
219 local icon = type(item_info[3]) == "string" and image(item_info[3]) or item_info[3]
220 if icon.width > data.h or icon.height > data.h then
221 local width, height
222 if ((data.h/icon.height) * icon.width) > data.h then
223 width, height = data.h, (data.h / icon.width) * icon.height
224 else
225 width, height = (data.h / icon.height) * icon.width, data.h
227 icon = icon:crop_and_scale(0, 0, icon.width, icon.height, width, height)
229 iconbox = widget { type = "imagebox" }
230 iconbox.image = icon
231 layout.margins[label] = { left = 2 }
232 else
233 layout.margins[label] = { left = data.h + 2 }
236 item:buttons(bindings)
238 local mouse_enter_func = function () item_enter(data, num, true) end
239 item:add_signal("mouse::enter", mouse_enter_func)
241 -- Create the submenu icon widget
242 local submenu
243 if type(item_info[2]) == "table" then
244 submenu = widget({ type = "imagebox" })
245 submenu.image = data.theme.submenu_icon and image(data.theme.submenu_icon)
246 submenu:buttons(bindings)
249 -- Add widgets to the wibox
250 if iconbox then
251 item.widgets = {
252 iconbox,
253 label,
254 { submenu, layout = layout.horizontal.rightleft },
255 layout = layout.horizontal.leftright
257 else
258 item.widgets = {
259 label,
260 { submenu, layout = layout.horizontal.rightleft },
261 layout = layout.horizontal.leftright
265 item.height = label:extents().height + 2
266 item.ontop = true
268 return { wibox = item, akey= key, cmd = item_info[2], returned_value=item_info[1] }
271 --- Build a popup menu with running clients and shows it.
272 -- @param menu Menu table, see new() function for more informations
273 -- @param keygrabber A boolean enabling or not the keyboard navigation.
274 -- @return The menu.
275 function clients(menu, keygrabber)
276 local cls = capi.client.get()
277 local cls_t = {}
278 for k, c in pairs(cls) do
279 cls_t[#cls_t + 1] = { util.escape(c.name) or "",
280 function ()
281 if not c:isvisible() then
282 tags.viewmore(c:tags(), c.screen)
284 capi.client.focus = c
285 end,
286 c.icon }
289 if not menu then
290 menu = {}
293 menu.items = cls_t
295 local m = new(menu)
296 m:show(keygrabber)
297 return m
300 local function set_coords(menu, screen_idx)
301 local s_geometry = capi.screen[screen_idx].workarea
302 local screen_w = s_geometry.x + s_geometry.width
303 local screen_h = s_geometry.y + s_geometry.height
305 local i_h = menu.h + menu.theme.border_width
306 local m_h = (i_h * #menu.items) + menu.theme.border_width
308 if menu.parent then
309 menu.w = menu.parent.w
310 menu.h = menu.parent.h
312 local p_w = i_h * (menu.num - 1)
313 local m_w = menu.w - menu.theme.border_width
315 menu.y = menu.parent.y + p_w + m_h > screen_h and screen_h - m_h or menu.parent.y + p_w
316 menu.x = menu.parent.x + m_w*2 > screen_w and menu.parent.x - m_w or menu.parent.x + m_w
317 else
318 local m_coords = capi.mouse.coords()
319 local m_w = menu.w
321 menu.y = m_coords.y+1 < s_geometry.y and s_geometry.y or m_coords.y+1
322 menu.x = m_coords.x+1 < s_geometry.x and s_geometry.x or m_coords.x+1
324 menu.y = menu.y + m_h > screen_h and screen_h - m_h or menu.y
325 menu.x = menu.x + m_w > screen_w and screen_w - m_w or menu.x
329 --- Show a menu.
330 -- @param menu The menu to show.
331 -- @param keygrabber A boolean enabling or not the keyboard navigation.
332 function show(menu, keygrabber)
333 local screen_index = capi.mouse.screen
334 set_coords(menu, screen_index)
335 for num, item in pairs(menu.items) do
336 local wibox = item.wibox
337 wibox.width = menu.w
338 wibox.height = menu.h
339 wibox.x = menu.x
340 wibox.y = menu.y + (num - 1) * (menu.h + wibox.border_width)
341 wibox.screen = screen_index
344 if menu.parent then
345 menu.keygrabber = menu.parent.keygrabber
346 elseif keygrabber ~= nil then
347 menu.keygrabber = keygrabber
348 else
349 menu.keygrabber = false
352 if not cur_menu and menu.keygrabber then
353 capi.keygrabber.run(grabber)
355 cur_menu = menu
358 --- Toggle menu visibility.
359 -- @param menu The menu to show if it's hidden, or to hide if it's shown.
360 -- @param keygrabber A boolean enabling or not the keyboard navigation.
361 function toggle(menu, keygrabber)
362 if menu.items[1] and menu.items[1].wibox.screen then
363 menu:hide()
364 else
365 menu:show(keygrabber)
369 --- Open a menu popup.
370 -- @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.
371 -- @param parent Specify the parent menu if we want to open a submenu, this value should never be set by the user.
372 -- @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.
373 function new(menu, parent, num)
374 -- Create a table to store our menu informations
375 local data = {}
377 data.items = {}
378 data.num = num or 1
379 data.theme = parent and parent.theme or load_theme(menu)
380 data.parent = parent
381 data.child = {}
382 if parent then
383 data.auto_expand = parent.auto_expand
384 elseif menu.auto_expand ~= nil then
385 data.auto_expand = menu.auto_expand
386 else
387 data.auto_expand = true
389 data.h = parent and parent.h or data.theme.menu_height
390 if type(data.h) ~= 'number' then data.h = tonumber(data.h) end
391 data.w = parent and parent.w or data.theme.menu_width
392 if type(data.w) ~= 'number' then data.w = tonumber(data.w) end
394 -- Create items
395 for k, v in pairs(menu.items) do
396 table.insert(data.items, add_item(data, k, v))
399 if #data.items > 0 and data.h < data.items[1].wibox.height then
400 data.h = data.items[1].wibox.height
403 -- Set methods
404 data.hide = hide
405 data.show = show
406 data.toggle = toggle
408 return data
411 setmetatable(_M, { __call = function(_, ...) return new(...) end })
413 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80