diff options
Diffstat (limited to 'awesome/lib/awful/menu.lua')
-rw-r--r-- | awesome/lib/awful/menu.lua | 723 |
1 files changed, 723 insertions, 0 deletions
diff --git a/awesome/lib/awful/menu.lua b/awesome/lib/awful/menu.lua new file mode 100644 index 0000000..dcbc5fa --- /dev/null +++ b/awesome/lib/awful/menu.lua @@ -0,0 +1,723 @@ +-------------------------------------------------------------------------------- +--- A menu for awful +-- +-- @author Damien Leone <damien.leone@gmail.com> +-- @author Julien Danjou <julien@danjou.info> +-- @author dodo +-- @copyright 2008, 2011 Damien Leone, Julien Danjou, dodo +-- @module awful.menu +-------------------------------------------------------------------------------- + +local wibox = require("wibox") +local button = require("awful.button") +local util = require("awful.util") +local spawn = require("awful.spawn") +local tags = require("awful.tag") +local keygrabber = require("awful.keygrabber") +local client_iterate = require("awful.client").iterate +local beautiful = require("beautiful") +local dpi = require("beautiful").xresources.apply_dpi +local object = require("gears.object") +local surface = require("gears.surface") +local protected_call = require("gears.protected_call") +local cairo = require("lgi").cairo +local setmetatable = setmetatable +local tonumber = tonumber +local string = string +local ipairs = ipairs +local pairs = pairs +local print = print +local table = table +local type = type +local math = math +local capi = { + screen = screen, + mouse = mouse, + client = client } +local screen = require("awful.screen") + + +local menu = { mt = {} } + + +local table_update = function (t, set) + for k, v in pairs(set) do + t[k] = v + end + return t +end + +--- The icon used for sub-menus. +-- @beautiful beautiful.menu_submenu_icon + +--- The item height. +-- @beautiful beautiful.menu_height +-- @tparam[opt=16] number menu_height + +--- The default menu width. +-- @beautiful beautiful.menu_width +-- @tparam[opt=100] number menu_width + +--- The menu item border color. +-- @beautiful beautiful.menu_border_color +-- @tparam[opt=0] number menu_border_color + +--- The menu item border width. +-- @beautiful beautiful.menu_border_width +-- @tparam[opt=0] number menu_border_width + +--- The default focused item foreground (text) color. +-- @beautiful beautiful.menu_fg_focus +-- @param color +-- @see gears.color + +--- The default focused item background color. +-- @beautiful beautiful.menu_bg_focus +-- @param color +-- @see gears.color + +--- The default foreground (text) color. +-- @beautiful beautiful.menu_fg_normal +-- @param color +-- @see gears.color + +--- The default background color. +-- @beautiful beautiful.menu_bg_normal +-- @param color +-- @see gears.color + +--- The default sub-menu indicator if no menu_submenu_icon is provided. +-- @beautiful beautiful.menu_submenu +-- @tparam[opt="▶"] string menu_submenu The sub-menu text. +-- @see beautiful.menu_submenu_icon + +--- Key bindings for menu navigation. +-- Keys are: up, down, exec, enter, back, close. Value are table with a list of valid +-- keys for the action, i.e. menu_keys.up = { "j", "k" } will bind 'j' and 'k' +-- key to up action. This is common to all created menu. +-- @class table +-- @name menu_keys +menu.menu_keys = { up = { "Up", "k" }, + down = { "Down", "j" }, + back = { "Left", "h" }, + exec = { "Return" }, + enter = { "Right", "l" }, + close = { "Escape" } } + + +local function load_theme(a, b) + a = a or {} + b = b or {} + local ret = {} + local fallback = beautiful.get() + if a.reset then b = fallback end + if a == "reset" then a = fallback end + ret.border = a.border_color or b.menu_border_color or b.border_normal or + fallback.menu_border_color or fallback.border_normal + ret.border_width= a.border_width or b.menu_border_width or b.border_width or + fallback.menu_border_width or fallback.border_width or 0 + ret.fg_focus = a.fg_focus or b.menu_fg_focus or b.fg_focus or + fallback.menu_fg_focus or fallback.fg_focus + ret.bg_focus = a.bg_focus or b.menu_bg_focus or b.bg_focus or + fallback.menu_bg_focus or fallback.bg_focus + ret.fg_normal = a.fg_normal or b.menu_fg_normal or b.fg_normal or + fallback.menu_fg_normal or fallback.fg_normal + ret.bg_normal = a.bg_normal or b.menu_bg_normal or b.bg_normal or + fallback.menu_bg_normal or fallback.bg_normal + ret.submenu_icon= a.submenu_icon or b.menu_submenu_icon or b.submenu_icon or + fallback.menu_submenu_icon or fallback.submenu_icon + ret.submenu = a.submenu or b.menu_submenu or b.submenu or + fallback.menu_submenu or fallback.submenu or "▶" + ret.height = a.height or b.menu_height or b.height or + fallback.menu_height or 16 + ret.width = a.width or b.menu_width or b.width or + fallback.menu_width or 100 + ret.font = a.font or b.font or fallback.font + for _, prop in ipairs({"width", "height", "menu_width"}) do + if type(ret[prop]) ~= "number" then ret[prop] = tonumber(ret[prop]) end + end + return ret +end + + +local function item_position(_menu, child) + local a, b = "height", "width" + local dir = _menu.layout.dir or "y" + if dir == "x" then a, b = b, a end + + local in_dir, other = 0, _menu[b] + local num = util.table.hasitem(_menu.child, child) + if num then + for i = 0, num - 1 do + local item = _menu.items[i] + if item then + other = math.max(other, item[b]) + in_dir = in_dir + item[a] + end + end + end + local w, h = other, in_dir + if dir == "x" then w, h = h, w end + return w, h +end + + +local function set_coords(_menu, s, m_coords) + local s_geometry = s.workarea + local screen_w = s_geometry.x + s_geometry.width + local screen_h = s_geometry.y + s_geometry.height + + _menu.width = _menu.wibox.width + _menu.height = _menu.wibox.height + + _menu.x = _menu.wibox.x + _menu.y = _menu.wibox.y + + if _menu.parent then + local w, h = item_position(_menu.parent, _menu) + w = w + _menu.parent.theme.border_width + + _menu.y = _menu.parent.y + h + _menu.height > screen_h and + screen_h - _menu.height or _menu.parent.y + h + _menu.x = _menu.parent.x + w + _menu.width > screen_w and + _menu.parent.x - _menu.width or _menu.parent.x + w + else + if m_coords == nil then + m_coords = capi.mouse.coords() + m_coords.x = m_coords.x + 1 + m_coords.y = m_coords.y + 1 + end + _menu.y = m_coords.y < s_geometry.y and s_geometry.y or m_coords.y + _menu.x = m_coords.x < s_geometry.x and s_geometry.x or m_coords.x + + _menu.y = _menu.y + _menu.height > screen_h and + screen_h - _menu.height or _menu.y + _menu.x = _menu.x + _menu.width > screen_w and + screen_w - _menu.width or _menu.x + end + + _menu.wibox.x = _menu.x + _menu.wibox.y = _menu.y +end + + +local function set_size(_menu) + local in_dir, other, a, b = 0, 0, "height", "width" + local dir = _menu.layout.dir or "y" + if dir == "x" then a, b = b, a end + for _, item in ipairs(_menu.items) do + other = math.max(other, item[b]) + in_dir = in_dir + item[a] + end + _menu[a], _menu[b] = in_dir, other + if in_dir > 0 and other > 0 then + _menu.wibox[a] = in_dir + _menu.wibox[b] = other + return true + end + return false +end + + +local function check_access_key(_menu, key) + for i, item in ipairs(_menu.items) do + if item.akey == key then + _menu:item_enter(i) + _menu:exec(i, { exec = true }) + return + end + end + if _menu.parent then + check_access_key(_menu.parent, key) + end +end + + +local function grabber(_menu, _, key, event) + if event ~= "press" then return end + + local sel = _menu.sel or 0 + if util.table.hasitem(menu.menu_keys.up, key) then + local sel_new = sel-1 < 1 and #_menu.items or sel-1 + _menu:item_enter(sel_new) + elseif util.table.hasitem(menu.menu_keys.down, key) then + local sel_new = sel+1 > #_menu.items and 1 or sel+1 + _menu:item_enter(sel_new) + elseif sel > 0 and util.table.hasitem(menu.menu_keys.enter, key) then + _menu:exec(sel) + elseif sel > 0 and util.table.hasitem(menu.menu_keys.exec, key) then + _menu:exec(sel, { exec = true }) + elseif util.table.hasitem(menu.menu_keys.back, key) then + _menu:hide() + elseif util.table.hasitem(menu.menu_keys.close, key) then + menu.get_root(_menu):hide() + else + check_access_key(_menu, key) + end +end + + +function menu:exec(num, opts) + opts = opts or {} + local item = self.items[num] + if not item then return end + local cmd = item.cmd + if type(cmd) == "table" then + local action = cmd.cmd + if #cmd == 0 then + if opts.exec and action and type(action) == "function" then + action() + end + return + end + if not self.child[num] then + self.child[num] = menu.new(cmd, self) + end + local can_invoke_action = opts.exec and + action and type(action) == "function" and + (not opts.mouse or (opts.mouse and (self.auto_expand or + (self.active_child == self.child[num] and + self.active_child.wibox.visible)))) + if can_invoke_action then + local visible = action(self.child[num], item) + if not visible then + menu.get_root(self):hide() + return + else + self.child[num]:update() + end + end + if self.active_child and self.active_child ~= self.child[num] then + self.active_child:hide() + end + self.active_child = self.child[num] + if not self.active_child.wibox.visible then + self.active_child:show() + end + elseif type(cmd) == "string" then + menu.get_root(self):hide() + spawn(cmd) + elseif type(cmd) == "function" then + local visible, action = cmd(item, self) + if not visible then + menu.get_root(self):hide() + else + self:update() + if self.items[num] then + self:item_enter(num, opts) + end + end + if action and type(action) == "function" then + action() + end + end +end + +function menu:item_enter(num, opts) + opts = opts or {} + local item = self.items[num] + if num == nil or self.sel == num or not item then + return + elseif self.sel then + self:item_leave(self.sel) + end + --print("sel", num, menu.sel, item.theme.bg_focus) + item._background:set_fg(item.theme.fg_focus) + item._background:set_bg(item.theme.bg_focus) + self.sel = num + + if self.auto_expand and opts.hover then + if self.active_child then + self.active_child:hide() + self.active_child = nil + end + + if type(item.cmd) == "table" then + self:exec(num, opts) + end + end +end + + +function menu:item_leave(num) + --print("leave", num) + local item = self.items[num] + if item then + item._background:set_fg(item.theme.fg_normal) + item._background:set_bg(item.theme.bg_normal) + end +end + + +--- Show a menu. +-- @param args The arguments +-- @param args.coords Menu position defaulting to mouse.coords() +function menu:show(args) + args = args or {} + local coords = args.coords or nil + local s = capi.screen[screen.focused()] + + if not set_size(self) then return end + set_coords(self, s, coords) + + keygrabber.run(self._keygrabber) + self.wibox.visible = true +end + +--- Hide a menu popup. +function menu:hide() + -- Remove items from screen + for i = 1, #self.items do + self:item_leave(i) + end + if self.active_child then + self.active_child:hide() + self.active_child = nil + end + self.sel = nil + + keygrabber.stop(self._keygrabber) + self.wibox.visible = false +end + +--- Toggle menu visibility. +-- @param args The arguments +-- @param args.coords Menu position {x,y} +function menu:toggle(args) + if self.wibox.visible then + self:hide() + else + self:show(args) + end +end + +--- Update menu content +function menu:update() + if self.wibox.visible then + self:show({ coords = { x = self.x, y = self.y } }) + end +end + + +--- Get the elder parent so for example when you kill +-- it, it will destroy the whole family. +function menu:get_root() + return self.parent and menu.get_root(self.parent) or self +end + +--- Add a new menu entry. +-- args.* params needed for the menu entry constructor. +-- @param args The item params +-- @param args.new (Default: awful.menu.entry) The menu entry constructor. +-- @param[opt] args.theme The menu entry theme. +-- @param[opt] index The index where the new entry will inserted. +function menu:add(args, index) + if not args then return end + local theme = load_theme(args.theme or {}, self.theme) + args.theme = theme + args.new = args.new or menu.entry + local item = protected_call(args.new, self, args) + if (not item) or (not item.widget) then + print("Error while checking menu entry: no property widget found.") + return + end + item.parent = self + item.theme = item.theme or theme + item.width = item.width or theme.width + item.height = item.height or theme.height + wibox.widget.base.check_widget(item.widget) + item._background = wibox.container.background() + item._background:set_widget(item.widget) + item._background:set_fg(item.theme.fg_normal) + item._background:set_bg(item.theme.bg_normal) + + + -- Create bindings + item._background:buttons(util.table.join( + button({}, 3, function () self:hide() end), + button({}, 1, function () + local num = util.table.hasitem(self.items, item) + self:item_enter(num, { mouse = true }) + self:exec(num, { exec = true, mouse = true }) + end ))) + + + item._mouse = function () + local num = util.table.hasitem(self.items, item) + self:item_enter(num, { hover = true, moue = true }) + end + item.widget:connect_signal("mouse::enter", item._mouse) + + if index then + self.layout:reset() + table.insert(self.items, index, item) + for _, i in ipairs(self.items) do + self.layout:add(i._background) + end + else + table.insert(self.items, item) + self.layout:add(item._background) + end + if self.wibox then + set_size(self) + end + return item +end + +--- Delete menu entry at given position +-- @param num The position in the table of the menu entry to be deleted; can be also the menu entry itself +function menu:delete(num) + if type(num) == "table" then + num = util.table.hasitem(self.items, num) + end + local item = self.items[num] + if not item then return end + item.widget:disconnect_signal("mouse::enter", item._mouse) + item.widget:set_visible(false) + table.remove(self.items, num) + if self.sel == num then + self:item_leave(self.sel) + self.sel = nil + end + self.layout:reset() + for _, i in ipairs(self.items) do + self.layout:add(i._background) + end + if self.child[num] then + self.child[num]:hide() + if self.active_child == self.child[num] then + self.active_child = nil + end + table.remove(self.child, num) + end + if self.wibox then + set_size(self) + end +end + +-------------------------------------------------------------------------------- + +--- Build a popup menu with running clients and show it. +-- @tparam[opt] table args Menu table, see `new()` for more information. +-- @tparam[opt] table item_args Table that will be merged into each item, see +-- `new()` for more information. +-- @tparam[opt] func filter A function taking a client as an argument and +-- returning `true` or `false` to indicate whether the client should be +-- included in the menu. +-- @return The menu. +function menu.clients(args, item_args, filter) + local cls_t = {} + for c in client_iterate(filter or function() return true end) do + cls_t[#cls_t + 1] = { + c.name or "", + function () + if not c:isvisible() then + tags.viewmore(c:tags(), c.screen) + end + c:emit_signal("request::activate", "menu.clients", {raise=true}) + end, + c.icon } + if item_args then + if type(item_args) == "function" then + util.table.merge(cls_t[#cls_t], item_args(c)) + else + util.table.merge(cls_t[#cls_t], item_args) + end + end + end + args = args or {} + args.items = args.items or {} + util.table.merge(args.items, cls_t) + + local m = menu.new(args) + m:show(args) + return m +end + +-------------------------------------------------------------------------------- + +--- Default awful.menu.entry constructor +-- @param parent The parent menu (TODO: This is apparently unused) +-- @param args the item params +-- @return table with 'widget', 'cmd', 'akey' and all the properties the user wants to change +function menu.entry(parent, args) -- luacheck: no unused args + args = args or {} + args.text = args[1] or args.text or "" + args.cmd = args[2] or args.cmd + args.icon = args[3] or args.icon + local ret = {} + -- Create the item label widget + local label = wibox.widget.textbox() + local key = '' + label:set_font(args.theme.font) + label:set_markup(string.gsub( + util.escape(args.text), "&(%w)", + function (l) + key = string.lower(l) + return "<u>" .. l .. "</u>" + end, 1)) + -- Set icon if needed + local icon, iconbox + local margin = wibox.container.margin() + margin:set_widget(label) + if args.icon then + icon = surface.load(args.icon) + end + if icon then + local iw = icon:get_width() + local ih = icon:get_height() + if iw > args.theme.width or ih > args.theme.height then + local w, h + if ((args.theme.height / ih) * iw) > args.theme.width then + w, h = args.theme.height, (args.theme.height / iw) * ih + else + w, h = (args.theme.height / ih) * iw, args.theme.height + end + -- We need to scale the image to size w x h + local img = cairo.ImageSurface(cairo.Format.ARGB32, w, h) + local cr = cairo.Context(img) + cr:scale(w / iw, h / ih) + cr:set_source_surface(icon, 0, 0) + cr:paint() + icon = img + end + iconbox = wibox.widget.imagebox() + if iconbox:set_image(icon) then + margin:set_left(dpi(2)) + else + iconbox = nil + end + end + if not iconbox then + margin:set_left(args.theme.height + dpi(2)) + end + -- Create the submenu icon widget + local submenu + if type(args.cmd) == "table" then + if args.theme.submenu_icon then + submenu = wibox.widget.imagebox() + submenu:set_image(args.theme.submenu_icon) + else + submenu = wibox.widget.textbox() + submenu:set_font(args.theme.font) + submenu:set_text(args.theme.submenu) + end + end + -- Add widgets to the wibox + local left = wibox.layout.fixed.horizontal() + if iconbox then + left:add(iconbox) + end + -- This contains the label + left:add(margin) + + local layout = wibox.layout.align.horizontal() + layout:set_left(left) + if submenu then + layout:set_right(submenu) + end + + return table_update(ret, { + label = label, + sep = submenu, + icon = iconbox, + widget = layout, + cmd = args.cmd, + akey = key, + }) +end + +-------------------------------------------------------------------------------- + +--- Create a menu popup. +-- @param args Table containing the menu informations. +-- +-- * 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). +-- * 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. +-- * Key auto_expand controls the submenu auto expand behaviour by setting it to true (default) or false. +-- +-- @param parent Specify the parent menu if we want to open a submenu, this value should never be set by the user. +-- @usage -- The following function builds and shows a menu of clients that match +-- -- a particular rule. +-- -- Bound to a key, it can be used to select from dozens of terminals open on +-- -- several tags. +-- -- When using @{rules.match_any} instead of @{rules.match}, +-- -- a menu of clients with different classes could be build. +-- +-- function terminal_menu () +-- terms = {} +-- for i, c in pairs(client.get()) do +-- if awful.rules.match(c, {class = "URxvt"}) then +-- terms[i] = +-- {c.name, +-- function() +-- c.first_tag:view_only() +-- client.focus = c +-- end, +-- c.icon +-- } +-- end +-- end +-- awful.menu(terms):show() +-- end +function menu.new(args, parent) + args = args or {} + args.layout = args.layout or wibox.layout.flex.vertical + local _menu = table_update(object(), { + item_enter = menu.item_enter, + item_leave = menu.item_leave, + get_root = menu.get_root, + delete = menu.delete, + update = menu.update, + toggle = menu.toggle, + hide = menu.hide, + show = menu.show, + exec = menu.exec, + add = menu.add, + child = {}, + items = {}, + parent = parent, + layout = args.layout(), + theme = load_theme(args.theme or {}, parent and parent.theme) }) + + if parent then + _menu.auto_expand = parent.auto_expand + elseif args.auto_expand ~= nil then + _menu.auto_expand = args.auto_expand + else + _menu.auto_expand = true + end + + -- Create items + for _, v in ipairs(args) do _menu:add(v) end + if args.items then + for _, v in pairs(args.items) do _menu:add(v) end + end + + _menu._keygrabber = function (...) + grabber(_menu, ...) + end + + _menu.wibox = wibox({ + ontop = true, + fg = _menu.theme.fg_normal, + bg = _menu.theme.bg_normal, + border_color = _menu.theme.border, + border_width = _menu.theme.border_width, + type = "popup_menu" }) + _menu.wibox.visible = false + _menu.wibox:set_widget(_menu.layout) + set_size(_menu) + + _menu.x = _menu.wibox.x + _menu.y = _menu.wibox.y + return _menu +end + +function menu.mt:__call(...) + return menu.new(...) +end + +return setmetatable(menu, menu.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |