From 22d656903563f75678f3634964731ccf93355dfd Mon Sep 17 00:00:00 2001 From: ache Date: Mon, 13 Mar 2017 23:17:19 +0100 Subject: Init commit --- lib/awful/autofocus.lua | 64 ++ lib/awful/button.lua | 61 ++ lib/awful/client.lua | 1238 +++++++++++++++++++++ lib/awful/client/focus.lua | 215 ++++ lib/awful/client/shape.lua | 93 ++ lib/awful/client/urgent.lua | 88 ++ lib/awful/completion.lua | 201 ++++ lib/awful/dbus.lua | 19 + lib/awful/ewmh.lua | 295 +++++ lib/awful/hotkeys_popup/init.lua | 17 + lib/awful/hotkeys_popup/keys/init.lua | 15 + lib/awful/hotkeys_popup/keys/vim.lua | 173 +++ lib/awful/hotkeys_popup/widget.lua | 482 ++++++++ lib/awful/init.lua | 64 ++ lib/awful/key.lua | 136 +++ lib/awful/keygrabber.lua | 96 ++ lib/awful/layout/init.lua | 323 ++++++ lib/awful/layout/suit/corner.lua | 204 ++++ lib/awful/layout/suit/fair.lua | 108 ++ lib/awful/layout/suit/floating.lua | 112 ++ lib/awful/layout/suit/init.lua | 19 + lib/awful/layout/suit/magnifier.lua | 147 +++ lib/awful/layout/suit/max.lua | 61 ++ lib/awful/layout/suit/spiral.lua | 89 ++ lib/awful/layout/suit/tile.lua | 348 ++++++ lib/awful/menu.lua | 723 ++++++++++++ lib/awful/mouse/drag_to_tag.lua | 58 + lib/awful/mouse/init.lua | 437 ++++++++ lib/awful/mouse/resize.lua | 229 ++++ lib/awful/mouse/snap.lua | 266 +++++ lib/awful/placement.lua | 1694 +++++++++++++++++++++++++++++ lib/awful/prompt.lua | 777 +++++++++++++ lib/awful/remote.lua | 47 + lib/awful/rules.lua | 545 ++++++++++ lib/awful/screen.lua | 477 ++++++++ lib/awful/spawn.lua | 421 +++++++ lib/awful/startup_notification.lua | 53 + lib/awful/tag.lua | 1505 +++++++++++++++++++++++++ lib/awful/titlebar.lua | 509 +++++++++ lib/awful/tooltip.lua | 616 +++++++++++ lib/awful/util.lua | 588 ++++++++++ lib/awful/wibar.lua | 603 ++++++++++ lib/awful/wibox.lua | 12 + lib/awful/widget/button.lua | 263 +++++ lib/awful/widget/common.lua | 119 ++ lib/awful/widget/graph.lua | 16 + lib/awful/widget/init.lua | 24 + lib/awful/widget/keyboardlayout.lua | 308 ++++++ lib/awful/widget/launcher.lua | 41 + lib/awful/widget/layoutbox.lua | 73 ++ lib/awful/widget/progressbar.lua | 16 + lib/awful/widget/prompt.lua | 64 ++ lib/awful/widget/taglist.lua | 452 ++++++++ lib/awful/widget/tasklist.lua | 573 ++++++++++ lib/awful/widget/textclock.lua | 16 + lib/awful/widget/watch.lua | 91 ++ lib/beautiful.lua | 7 + lib/beautiful/init.lua | 217 ++++ lib/beautiful/xresources.lua | 117 ++ lib/gears/cache.lua | 51 + lib/gears/color.lua | 346 ++++++ lib/gears/debug.lua | 78 ++ lib/gears/geometry.lua | 240 ++++ lib/gears/init.lua | 23 + lib/gears/matrix.lua | 219 ++++ lib/gears/object.lua | 285 +++++ lib/gears/object/properties.lua | 88 ++ lib/gears/protected_call.lua | 57 + lib/gears/shape.lua | 785 +++++++++++++ lib/gears/surface.lua | 252 +++++ lib/gears/timer.lua | 187 ++++ lib/gears/wallpaper.lua | 221 ++++ lib/menubar/icon_theme.lua | 251 +++++ lib/menubar/index_theme.lua | 164 +++ lib/menubar/init.lua | 480 ++++++++ lib/menubar/menu_gen.lua | 141 +++ lib/menubar/utils.lua | 316 ++++++ lib/naughty.lua | 7 + lib/naughty/core.lua | 688 ++++++++++++ lib/naughty/dbus.lua | 261 +++++ lib/naughty/init.lua | 14 + lib/shifty | 1 + lib/shifty.lua | 1138 +++++++++++++++++++ lib/wibox/container/arcchart.lua | 566 ++++++++++ lib/wibox/container/background.lua | 626 +++++++++++ lib/wibox/container/constraint.lua | 371 +++++++ lib/wibox/container/init.lua | 21 + lib/wibox/container/margin.lua | 419 +++++++ lib/wibox/container/mirror.lua | 340 ++++++ lib/wibox/container/radialprogressbar.lua | 481 ++++++++ lib/wibox/container/rotate.lua | 384 +++++++ lib/wibox/container/scroll.lua | 716 ++++++++++++ lib/wibox/drawable.lua | 489 +++++++++ lib/wibox/hierarchy.lua | 333 ++++++ lib/wibox/init.lua | 479 ++++++++ lib/wibox/layout/align.lua | 526 +++++++++ lib/wibox/layout/constraint.lua | 17 + lib/wibox/layout/fixed.lua | 585 ++++++++++ lib/wibox/layout/flex.lua | 429 ++++++++ lib/wibox/layout/init.lua | 23 + lib/wibox/layout/margin.lua | 17 + lib/wibox/layout/mirror.lua | 17 + lib/wibox/layout/ratio.lua | 583 ++++++++++ lib/wibox/layout/rotate.lua | 17 + lib/wibox/layout/scroll.lua | 16 + lib/wibox/layout/stack.lua | 402 +++++++ lib/wibox/widget/background.lua | 16 + lib/wibox/widget/base.lua | 694 ++++++++++++ lib/wibox/widget/checkbox.lua | 530 +++++++++ lib/wibox/widget/graph.lua | 575 ++++++++++ lib/wibox/widget/imagebox.lua | 395 +++++++ lib/wibox/widget/init.lua | 22 + lib/wibox/widget/piechart.lua | 467 ++++++++ lib/wibox/widget/progressbar.lua | 754 +++++++++++++ lib/wibox/widget/slider.lua | 709 ++++++++++++ lib/wibox/widget/systray.lua | 186 ++++ lib/wibox/widget/textbox.lua | 507 +++++++++ lib/wibox/widget/textclock.lua | 254 +++++ 118 files changed, 35884 insertions(+) create mode 100644 lib/awful/autofocus.lua create mode 100644 lib/awful/button.lua create mode 100644 lib/awful/client.lua create mode 100644 lib/awful/client/focus.lua create mode 100644 lib/awful/client/shape.lua create mode 100644 lib/awful/client/urgent.lua create mode 100644 lib/awful/completion.lua create mode 100644 lib/awful/dbus.lua create mode 100644 lib/awful/ewmh.lua create mode 100644 lib/awful/hotkeys_popup/init.lua create mode 100644 lib/awful/hotkeys_popup/keys/init.lua create mode 100644 lib/awful/hotkeys_popup/keys/vim.lua create mode 100644 lib/awful/hotkeys_popup/widget.lua create mode 100644 lib/awful/init.lua create mode 100644 lib/awful/key.lua create mode 100644 lib/awful/keygrabber.lua create mode 100644 lib/awful/layout/init.lua create mode 100644 lib/awful/layout/suit/corner.lua create mode 100644 lib/awful/layout/suit/fair.lua create mode 100644 lib/awful/layout/suit/floating.lua create mode 100644 lib/awful/layout/suit/init.lua create mode 100644 lib/awful/layout/suit/magnifier.lua create mode 100644 lib/awful/layout/suit/max.lua create mode 100644 lib/awful/layout/suit/spiral.lua create mode 100644 lib/awful/layout/suit/tile.lua create mode 100644 lib/awful/menu.lua create mode 100644 lib/awful/mouse/drag_to_tag.lua create mode 100644 lib/awful/mouse/init.lua create mode 100644 lib/awful/mouse/resize.lua create mode 100644 lib/awful/mouse/snap.lua create mode 100644 lib/awful/placement.lua create mode 100644 lib/awful/prompt.lua create mode 100644 lib/awful/remote.lua create mode 100644 lib/awful/rules.lua create mode 100644 lib/awful/screen.lua create mode 100644 lib/awful/spawn.lua create mode 100644 lib/awful/startup_notification.lua create mode 100644 lib/awful/tag.lua create mode 100644 lib/awful/titlebar.lua create mode 100644 lib/awful/tooltip.lua create mode 100644 lib/awful/util.lua create mode 100644 lib/awful/wibar.lua create mode 100644 lib/awful/wibox.lua create mode 100644 lib/awful/widget/button.lua create mode 100644 lib/awful/widget/common.lua create mode 100644 lib/awful/widget/graph.lua create mode 100644 lib/awful/widget/init.lua create mode 100644 lib/awful/widget/keyboardlayout.lua create mode 100644 lib/awful/widget/launcher.lua create mode 100644 lib/awful/widget/layoutbox.lua create mode 100644 lib/awful/widget/progressbar.lua create mode 100644 lib/awful/widget/prompt.lua create mode 100644 lib/awful/widget/taglist.lua create mode 100644 lib/awful/widget/tasklist.lua create mode 100644 lib/awful/widget/textclock.lua create mode 100644 lib/awful/widget/watch.lua create mode 100644 lib/beautiful.lua create mode 100644 lib/beautiful/init.lua create mode 100644 lib/beautiful/xresources.lua create mode 100644 lib/gears/cache.lua create mode 100644 lib/gears/color.lua create mode 100644 lib/gears/debug.lua create mode 100644 lib/gears/geometry.lua create mode 100644 lib/gears/init.lua create mode 100644 lib/gears/matrix.lua create mode 100644 lib/gears/object.lua create mode 100644 lib/gears/object/properties.lua create mode 100644 lib/gears/protected_call.lua create mode 100644 lib/gears/shape.lua create mode 100644 lib/gears/surface.lua create mode 100644 lib/gears/timer.lua create mode 100644 lib/gears/wallpaper.lua create mode 100644 lib/menubar/icon_theme.lua create mode 100644 lib/menubar/index_theme.lua create mode 100644 lib/menubar/init.lua create mode 100644 lib/menubar/menu_gen.lua create mode 100644 lib/menubar/utils.lua create mode 100644 lib/naughty.lua create mode 100644 lib/naughty/core.lua create mode 100644 lib/naughty/dbus.lua create mode 100644 lib/naughty/init.lua create mode 160000 lib/shifty create mode 100644 lib/shifty.lua create mode 100644 lib/wibox/container/arcchart.lua create mode 100644 lib/wibox/container/background.lua create mode 100644 lib/wibox/container/constraint.lua create mode 100644 lib/wibox/container/init.lua create mode 100644 lib/wibox/container/margin.lua create mode 100644 lib/wibox/container/mirror.lua create mode 100644 lib/wibox/container/radialprogressbar.lua create mode 100644 lib/wibox/container/rotate.lua create mode 100644 lib/wibox/container/scroll.lua create mode 100644 lib/wibox/drawable.lua create mode 100644 lib/wibox/hierarchy.lua create mode 100644 lib/wibox/init.lua create mode 100644 lib/wibox/layout/align.lua create mode 100644 lib/wibox/layout/constraint.lua create mode 100644 lib/wibox/layout/fixed.lua create mode 100644 lib/wibox/layout/flex.lua create mode 100644 lib/wibox/layout/init.lua create mode 100644 lib/wibox/layout/margin.lua create mode 100644 lib/wibox/layout/mirror.lua create mode 100644 lib/wibox/layout/ratio.lua create mode 100644 lib/wibox/layout/rotate.lua create mode 100644 lib/wibox/layout/scroll.lua create mode 100644 lib/wibox/layout/stack.lua create mode 100644 lib/wibox/widget/background.lua create mode 100644 lib/wibox/widget/base.lua create mode 100644 lib/wibox/widget/checkbox.lua create mode 100644 lib/wibox/widget/graph.lua create mode 100644 lib/wibox/widget/imagebox.lua create mode 100644 lib/wibox/widget/init.lua create mode 100644 lib/wibox/widget/piechart.lua create mode 100644 lib/wibox/widget/progressbar.lua create mode 100644 lib/wibox/widget/slider.lua create mode 100644 lib/wibox/widget/systray.lua create mode 100644 lib/wibox/widget/textbox.lua create mode 100644 lib/wibox/widget/textclock.lua (limited to 'lib') diff --git a/lib/awful/autofocus.lua b/lib/awful/autofocus.lua new file mode 100644 index 0000000..5fcd220 --- /dev/null +++ b/lib/awful/autofocus.lua @@ -0,0 +1,64 @@ +--------------------------------------------------------------------------- +--- Autofocus functions. +-- +-- When loaded, this module makes sure that there's always a client that will +-- have focus on events such as tag switching, client unmanaging, etc. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @module awful.autofocus +--------------------------------------------------------------------------- + +local client = client +local aclient = require("awful.client") +local timer = require("gears.timer") + +--- Give focus when clients appear/disappear. +-- +-- @param obj An object that should have a .screen property. +local function check_focus(obj) + if not obj.screen.valid then return end + -- When no visible client has the focus... + if not client.focus or not client.focus:isvisible() then + local c = aclient.focus.history.get(screen[obj.screen], 0, aclient.focus.filter) + if c then + c:emit_signal("request::activate", "autofocus.check_focus", + {raise=false}) + end + end +end + +--- Check client focus (delayed). +-- @param obj An object that should have a .screen property. +local function check_focus_delayed(obj) + timer.delayed_call(check_focus, {screen = obj.screen}) +end + +--- Give focus on tag selection change. +-- +-- @param tag A tag object +local function check_focus_tag(t) + local s = t.screen + if (not s) or (not s.valid) then return end + s = screen[s] + check_focus({ screen = s }) + if client.focus and screen[client.focus.screen] ~= s then + local c = aclient.focus.history.get(s, 0, aclient.focus.filter) + if c then + c:emit_signal("request::activate", "autofocus.check_focus_tag", + {raise=false}) + end + end +end + +tag.connect_signal("property::selected", function (t) + timer.delayed_call(check_focus_tag, t) +end) +client.connect_signal("unmanage", check_focus_delayed) +client.connect_signal("tagged", check_focus_delayed) +client.connect_signal("untagged", check_focus_delayed) +client.connect_signal("property::hidden", check_focus_delayed) +client.connect_signal("property::minimized", check_focus_delayed) +client.connect_signal("property::sticky", check_focus_delayed) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/button.lua b/lib/awful/button.lua new file mode 100644 index 0000000..b50664d --- /dev/null +++ b/lib/awful/button.lua @@ -0,0 +1,61 @@ +--------------------------------------------------------------------------- +--- Create easily new buttons objects ignoring certain modifiers. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @classmod awful.button +--------------------------------------------------------------------------- + +-- Grab environment we need +local setmetatable = setmetatable +local ipairs = ipairs +local capi = { button = button } +local util = require("awful.util") + +local button = { mt = {} } + +--- Modifiers to ignore. +-- +-- By default this is initialized as `{ "Lock", "Mod2" }` +-- so the `Caps Lock` or `Num Lock` modifier are not taking into account by awesome +-- when pressing keys. +-- +-- @table ignore_modifiers +local ignore_modifiers = { "Lock", "Mod2" } + +--- Create a new button to use as binding. +-- +-- This function is useful to create several buttons from one, because it will use +-- the ignore_modifier variable to create more button with or without the ignored +-- modifiers activated. +-- +-- For example if you want to ignore CapsLock in your buttonbinding (which is +-- ignored by default by this function), creating button binding with this function +-- will return 2 button objects: one with CapsLock on, and the other one with +-- CapsLock off. +-- +-- @see button +-- @treturn table A table with one or several button objects. +function button.new(mod, _button, press, release) + local ret = {} + local subsets = util.subsets(ignore_modifiers) + for _, set in ipairs(subsets) do + ret[#ret + 1] = capi.button({ modifiers = util.table.join(mod, set), + button = _button }) + if press then + ret[#ret]:connect_signal("press", function(_, ...) press(...) end) + end + if release then + ret[#ret]:connect_signal("release", function (_, ...) release(...) end) + end + end + return ret +end + +function button.mt:__call(...) + return button.new(...) +end + +return setmetatable(button, button.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/client.lua b/lib/awful/client.lua new file mode 100644 index 0000000..809c055 --- /dev/null +++ b/lib/awful/client.lua @@ -0,0 +1,1238 @@ +--------------------------------------------------------------------------- +--- Useful client manipulation functions. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module client +--------------------------------------------------------------------------- + +-- Grab environment we need +local util = require("awful.util") +local spawn = require("awful.spawn") +local object = require("gears.object") +local grect = require("gears.geometry").rectangle +local pairs = pairs +local type = type +local ipairs = ipairs +local table = table +local math = math +local setmetatable = setmetatable +local capi = +{ + client = client, + mouse = mouse, + screen = screen, + awesome = awesome, +} + +local function get_screen(s) + return s and capi.screen[s] +end + +-- We use a metatable to prevent circular dependency loops. +local screen +do + screen = setmetatable({}, { + __index = function(_, k) + screen = require("awful.screen") + return screen[k] + end, + __newindex = error -- Just to be sure in case anything ever does this + }) +end +local client = {object={}} + +-- Private data +client.data = {} +client.data.marked = {} +client.data.persistent_properties_registered = {} -- keys are names of persistent properties, value always true + +-- Functions +client.urgent = require("awful.client.urgent") +client.swap = {} +client.floating = {} +client.dockable = {} +client.property = {} +client.shape = require("awful.client.shape") +client.focus = require("awful.client.focus") + +--- Jump to the given client. +-- Takes care of focussing the screen, the right tag, etc. +-- +-- @deprecated awful.client.jumpto +-- @see client.jump_to +-- @client c the client to jump to +-- @tparam bool|function merge If true then merge tags (select the client's +-- first tag additionally) when the client is not visible. +-- If it is a function, it will be called with the client and its first +-- tag as arguments. +function client.jumpto(c, merge) + util.deprecate("Use c:jump_to(merge) instead of awful.client.jumpto") + client.object.jump_to(c, merge) +end + +--- Jump to the given client. +-- Takes care of focussing the screen, the right tag, etc. +-- +-- @function client.jump_to +-- @tparam bool|function merge If true then merge tags (select the client's +-- first tag additionally) when the client is not visible. +-- If it is a function, it will be called with the client and its first +-- tag as arguments. +function client.object.jump_to(self, merge) + local s = get_screen(screen.focused()) + -- focus the screen + if s ~= get_screen(self.screen) then + screen.focus(self.screen) + end + + self.minimized = false + + -- Try to make client visible, this also covers e.g. sticky. + if not self:isvisible() then + local t = self.first_tag + if merge then + if type(merge) == "function" then + merge(self, t) + elseif t then + t.selected = true + end + elseif t then + t:view_only() + end + end + + self:emit_signal("request::activate", "client.jumpto", {raise=true}) +end + +--- Get visible clients from a screen. +-- +-- @deprecated awful.client.visible +-- @see screen.clients +-- @tparam[opt] integer|screen s The screen, or nil for all screens. +-- @tparam[opt=false] boolean stacked Use stacking order? (top to bottom) +-- @treturn table A table with all visible clients. +function client.visible(s, stacked) + local cls = capi.client.get(s, stacked) + local vcls = {} + for _, c in pairs(cls) do + if c:isvisible() then + table.insert(vcls, c) + end + end + return vcls +end + +--- Get visible and tiled clients +-- +-- @deprecated awful.client.tiled +-- @see screen.tiled_clients +-- @tparam integer|screen s The screen, or nil for all screens. +-- @tparam[opt=false] boolean stacked Use stacking order? (top to bottom) +-- @treturn table A table with all visible and tiled clients. +function client.tiled(s, stacked) + local clients = client.visible(s, stacked) + local tclients = {} + -- Remove floating clients + for _, c in pairs(clients) do + if not client.object.get_floating(c) + and not c.fullscreen + and not c.maximized_vertical + and not c.maximized_horizontal then + table.insert(tclients, c) + end + end + return tclients +end + +--- Get a client by its relative index to another client. +-- If no client is passed, the focused client will be used. +-- +-- @function awful.client.next +-- @tparam int i The index. Use 1 to get the next, -1 to get the previous. +-- @client[opt] sel The client. +-- @tparam[opt=false] boolean stacked Use stacking order? (top to bottom) +-- @return A client, or nil if no client is available. +-- +-- @usage -- focus the next window in the index +-- awful.client.next(1) +-- -- focus the previous +-- awful.client.next(-1) +function client.next(i, sel, stacked) + -- Get currently focused client + sel = sel or capi.client.focus + if sel then + -- Get all visible clients + local cls = client.visible(sel.screen, stacked) + local fcls = {} + -- Remove all non-normal clients + for _, c in ipairs(cls) do + if client.focus.filter(c) or c == sel then + table.insert(fcls, c) + end + end + cls = fcls + -- Loop upon each client + for idx, c in ipairs(cls) do + if c == sel then + -- Cycle + return cls[util.cycle(#cls, idx + i)] + end + end + end +end + +--- Swap a client with another client in the given direction. +-- @function awful.client.swap.bydirection +-- @tparam string dir The direction, can be either "up", "down", "left" or "right". +-- @client[opt=focused] c The client. +-- @tparam[opt=false] boolean stacked Use stacking order? (top to bottom) +function client.swap.bydirection(dir, c, stacked) + local sel = c or capi.client.focus + if sel then + local cltbl = client.visible(sel.screen, stacked) + local geomtbl = {} + for i,cl in ipairs(cltbl) do + geomtbl[i] = cl:geometry() + end + local target = grect.get_in_direction(dir, geomtbl, sel:geometry()) + + -- If we found a client to swap with, then go for it + if target then + cltbl[target]:swap(sel) + end + end +end + +--- Swap a client with another client in the given direction. Swaps across screens. +-- @function awful.client.swap.global_bydirection +-- @param dir The direction, can be either "up", "down", "left" or "right". +-- @client[opt] sel The client. +function client.swap.global_bydirection(dir, sel) + sel = sel or capi.client.focus + local scr = get_screen(sel and sel.screen or screen.focused()) + + if sel then + -- move focus + client.focus.global_bydirection(dir, sel) + local c = capi.client.focus + + -- swapping inside a screen + if get_screen(sel.screen) == get_screen(c.screen) and sel ~= c then + c:swap(sel) + + -- swapping to an empty screen + elseif get_screen(sel.screen) ~= get_screen(c.screen) and sel == c then + sel:move_to_screen(screen.focused()) + + -- swapping to a nonempty screen + elseif get_screen(sel.screen) ~= get_screen(c.screen) and sel ~= c then + sel:move_to_screen(c.screen) + c:move_to_screen(scr) + end + + screen.focus(sel.screen) + sel:emit_signal("request::activate", "client.swap.global_bydirection", + {raise=false}) + end +end + +--- Swap a client by its relative index. +-- @function awful.client.swap.byidx +-- @param i The index. +-- @client[opt] c The client, otherwise focused one is used. +function client.swap.byidx(i, c) + local sel = c or capi.client.focus + local target = client.next(i, sel) + if target then + target:swap(sel) + end +end + +--- Cycle clients. +-- +-- @function awful.client.cycle +-- @param clockwise True to cycle clients clockwise. +-- @param[opt] s The screen where to cycle clients. +-- @tparam[opt=false] boolean stacked Use stacking order? (top to bottom) +function client.cycle(clockwise, s, stacked) + s = s or screen.focused() + local cls = client.visible(s, stacked) + -- We can't rotate without at least 2 clients, buddy. + if #cls >= 2 then + local c = table.remove(cls, 1) + if clockwise then + for i = #cls, 1, -1 do + c:swap(cls[i]) + end + else + for _, rc in pairs(cls) do + c:swap(rc) + end + end + end +end + +--- Get the master window. +-- +-- @legacylayout awful.client.getmaster +-- @screen_or_idx[opt=awful.screen.focused()] s The screen. +-- @return The master window. +function client.getmaster(s) + s = s or screen.focused() + return client.visible(s)[1] +end + +--- Set the client as master: put it at the beginning of other windows. +-- +-- @legacylayout awful.client.setmaster +-- @client c The window to set as master. +function client.setmaster(c) + local cls = util.table.reverse(capi.client.get(c.screen)) + for _, v in pairs(cls) do + c:swap(v) + end +end + +--- Set the client as slave: put it at the end of other windows. +-- @legacylayout awful.client.setslave +-- @client c The window to set as slave. +function client.setslave(c) + local cls = capi.client.get(c.screen) + for _, v in pairs(cls) do + c:swap(v) + end +end + +--- Move/resize a client relative to current coordinates. +-- @deprecated awful.client.moveresize +-- @param x The relative x coordinate. +-- @param y The relative y coordinate. +-- @param w The relative width. +-- @param h The relative height. +-- @client[opt] c The client, otherwise focused one is used. +-- @see client.relative_move +function client.moveresize(x, y, w, h, c) + util.deprecate("Use c:relative_move(x, y, w, h) instead of awful.client.moveresize") + client.object.relative_move(c or capi.client.focus, x, y, w, h) +end + +--- Move/resize a client relative to current coordinates. +-- @function client.relative_move +-- @see geometry +-- @tparam[opt=c.x] number x The relative x coordinate. +-- @tparam[opt=c.y] number y The relative y coordinate. +-- @tparam[opt=c.width] number w The relative width. +-- @tparam[opt=c.height] number h The relative height. +function client.object.relative_move(self, x, y, w, h) + local geometry = self:geometry() + geometry['x'] = geometry['x'] + x + geometry['y'] = geometry['y'] + y + geometry['width'] = geometry['width'] + w + geometry['height'] = geometry['height'] + h + self:geometry(geometry) +end + +--- Move a client to a tag. +-- @deprecated awful.client.movetotag +-- @param target The tag to move the client to. +-- @client[opt] c The client to move, otherwise the focused one is used. +-- @see client.move_to_tag +function client.movetotag(target, c) + util.deprecate("Use c:move_to_tag(target) instead of awful.client.movetotag") + client.object.move_to_tag(c or capi.client.focus, target) +end + +--- Move a client to a tag. +-- @function client.move_to_tag +-- @tparam tag target The tag to move the client to. +function client.object.move_to_tag(self, target) + local s = target.screen + if self and s then + if self == capi.client.focus then + self:emit_signal("request::activate", "client.movetotag", {raise=true}) + end + -- Set client on the same screen as the tag. + self.screen = s + self:tags({ target }) + end +end + +--- Toggle a tag on a client. +-- @deprecated awful.client.toggletag +-- @param target The tag to toggle. +-- @client[opt] c The client to toggle, otherwise the focused one is used. +-- @see client.toggle_tag +function client.toggletag(target, c) + util.deprecate("Use c:toggle_tag(target) instead of awful.client.toggletag") + client.object.toggle_tag(c or capi.client.focus, target) +end + +--- Toggle a tag on a client. +-- @function client.toggle_tag +-- @tparam tag target The tag to move the client to. +function client.object.toggle_tag(self, target) + -- Check that tag and client screen are identical + if self and get_screen(self.screen) == get_screen(target.screen) then + local tags = self:tags() + local index = nil; + for i, v in ipairs(tags) do + if v == target then + index = i + break + end + end + if index then + -- If it's the only tag for the window, stop. + if #tags == 1 then return end + tags[index] = nil + else + tags[#tags + 1] = target + end + self:tags(tags) + end +end + +--- Move a client to a screen. Default is next screen, cycling. +-- @deprecated awful.client.movetoscreen +-- @client c The client to move. +-- @param s The screen, default to current + 1. +-- @see screen +-- @see client.move_to_screen +function client.movetoscreen(c, s) + util.deprecate("Use c:move_to_screen(s) instead of awful.client.movetoscreen") + client.object.move_to_screen(c or capi.client.focus, s) +end + +--- Move a client to a screen. Default is next screen, cycling. +-- @function client.move_to_screen +-- @tparam[opt=c.screen.index+1] screen s The screen, default to current + 1. +-- @see screen +-- @see request::activate +function client.object.move_to_screen(self, s) + if self then + local sc = capi.screen.count() + if not s then + s = self.screen.index + 1 + end + if type(s) == "number" then + if s > sc then s = 1 elseif s < 1 then s = sc end + end + s = get_screen(s) + if get_screen(self.screen) ~= s then + local sel_is_focused = self == capi.client.focus + self.screen = s + screen.focus(s) + + if sel_is_focused then + self:emit_signal("request::activate", "client.movetoscreen", + {raise=true}) + end + end + end +end + +--- Tag a client with the set of current tags. +-- @function client.to_selected_tags +-- @see screen.selected_tags +function client.object.to_selected_tags(self) + local tags = {} + + for _, t in ipairs(self:tags()) do + if get_screen(t.screen) == get_screen(self.screen) then + table.insert(tags, t) + end + end + + if self.screen then + if #tags == 0 then + tags = self.screen.selected_tags + end + + if #tags == 0 then + tags = self.screen.tags + end + end + + if #tags ~= 0 then + self:tags(tags) + end +end + +--- If a client is marked or not. +-- +-- **Signal:** +-- +-- * *marked* (for legacy reasons, use `property::marked`) +-- * *unmarked* (for legacy reasons, use `property::marked`) +-- * *property::marked* +-- +-- @property marked +-- @param boolean + +--- The border color when the client is focused. +-- +-- @beautiful beautiful.border_marked +-- @param string +-- + +function client.object.set_marked(self, value) + local is_marked = self.marked + + if value == false and is_marked then + for k, v in pairs(client.data.marked) do + if self == v then + table.remove(client.data.marked, k) + end + end + self:emit_signal("unmarked") + elseif not is_marked and value then + self:emit_signal("marked") + table.insert(client.data.marked, self) + end + + client.property.set(self, "marked", value) +end + +function client.object.get_marked(self) + return client.property.get(self, "marked") +end + +--- Mark a client, and then call 'marked' hook. +-- @deprecated awful.client.mark +-- @client c The client to mark, the focused one if not specified. +function client.mark(c) + util.deprecate("Use c.marked = true instead of awful.client.mark") + client.object.set_marked(c or capi.client.focus, true) +end + +--- Unmark a client and then call 'unmarked' hook. +-- @deprecated awful.client.unmark +-- @client c The client to unmark, or the focused one if not specified. +function client.unmark(c) + util.deprecate("Use c.marked = false instead of awful.client.unmark") + client.object.set_marked(c or capi.client.focus, false) +end + +--- Check if a client is marked. +-- @deprecated awful.client.ismarked +-- @client c The client to check, or the focused one otherwise. +function client.ismarked(c) + util.deprecate("Use c.marked instead of awful.client.ismarked") + return client.object.get_marked(c or capi.client.focus) +end + +--- Toggle a client as marked. +-- @deprecated awful.client.togglemarked +-- @client c The client to toggle mark. +function client.togglemarked(c) + util.deprecate("Use c.marked = not c.marked instead of awful.client.togglemarked") + c = c or capi.client.focus + if c then + c.marked = not c.marked + end +end + +--- Return the marked clients and empty the marked table. +-- @function awful.client.getmarked +-- @return A table with all marked clients. +function client.getmarked() + local copy = util.table.clone(client.data.marked, false) + + for _, v in pairs(copy) do + client.property.set(v, "marked", false) + v:emit_signal("unmarked") + end + + client.data.marked = {} + + return copy +end + +--- Set a client floating state, overriding auto-detection. +-- Floating client are not handled by tiling layouts. +-- @deprecated awful.client.floating.set +-- @client c A client. +-- @param s True or false. +function client.floating.set(c, s) + util.deprecate("Use c.floating = true instead of awful.client.floating.set") + client.object.set_floating(c, s) +end + +-- Set a client floating state, overriding auto-detection. +-- Floating client are not handled by tiling layouts. +-- @client c A client. +-- @param s True or false. +function client.object.set_floating(c, s) + c = c or capi.client.focus + if c and client.property.get(c, "floating") ~= s then + client.property.set(c, "floating", s) + local scr = c.screen + if s == true then + c:geometry(client.property.get(c, "floating_geometry")) + end + c.screen = scr + end +end + +local function store_floating_geometry(c) + if client.object.get_floating(c) then + client.property.set(c, "floating_geometry", c:geometry()) + end +end + +-- Store the initial client geometry. +capi.client.connect_signal("new", function(cl) + local function store_init_geometry(c) + client.property.set(c, "floating_geometry", c:geometry()) + c:disconnect_signal("property::border_width", store_init_geometry) + end + cl:connect_signal("property::border_width", store_init_geometry) +end) + +capi.client.connect_signal("property::geometry", store_floating_geometry) + +--- Return if a client has a fixed size or not. +-- This function is deprecated, use `c.is_fixed` +-- @client c The client. +-- @deprecated awful.client.isfixed +-- @see is_fixed +-- @see size_hints_honor +function client.isfixed(c) + util.deprecate("Use c.is_fixed instead of awful.client.isfixed") + c = c or capi.client.focus + return client.object.is_fixed(c) +end + +--- Return if a client has a fixed size or not. +-- +-- **Signal:** +-- +-- * *property::is_fixed* +-- +-- This property is read only. +-- @property is_fixed +-- @param boolean The floating state +-- @see size_hints +-- @see size_hints_honor + +function client.object.is_fixed(c) + if not c then return end + local h = c.size_hints + if h.min_width and h.max_width + and h.max_height and h.min_height + and h.min_width > 0 and h.max_width > 0 + and h.max_height > 0 and h.min_height > 0 + and h.min_width == h.max_width + and h.min_height == h.max_height then + return true + end + return false +end + +--- Get a client floating state. +-- @client c A client. +-- @see floating +-- @deprecated awful.client.floating.get +-- @return True or false. Note that some windows might be floating even if you +-- did not set them manually. For example, windows with a type different than +-- normal. +function client.floating.get(c) + util.deprecate("Use c.floating instead of awful.client.floating.get") + return client.object.get_floating(c) +end + +--- The client floating state. +-- If the client is part of the tiled layout or free floating. +-- +-- Note that some windows might be floating even if you +-- did not set them manually. For example, windows with a type different than +-- normal. +-- +-- **Signal:** +-- +-- * *property::floating* +-- +-- @property floating +-- @param boolean The floating state + +function client.object.get_floating(c) + c = c or capi.client.focus + if c then + local value = client.property.get(c, "floating") + if value ~= nil then + return value + end + if c.type ~= "normal" + or c.fullscreen + or c.maximized_vertical + or c.maximized_horizontal + or client.object.is_fixed(c) then + return true + end + return false + end +end + +--- Toggle the floating state of a client between 'auto' and 'true'. +-- Use `c.floating = not c.floating` +-- @deprecated awful.client.floating.toggle +-- @client c A client. +-- @see floating +function client.floating.toggle(c) + c = c or capi.client.focus + -- If it has been set to floating + client.object.set_floating(c, not client.object.get_floating(c)) +end + +-- Remove the floating information on a client. +-- @client c The client. +function client.floating.delete(c) + client.object.set_floating(c, nil) +end + +--- The x coordinates. +-- +-- **Signal:** +-- +-- * *property::x* +-- +-- @property x +-- @param integer + +--- The y coordinates. +-- +-- **Signal:** +-- +-- * *property::y* +-- +-- @property y +-- @param integer + +--- The width of the wibox. +-- +-- **Signal:** +-- +-- * *property::width* +-- +-- @property width +-- @param width + +--- The height of the wibox. +-- +-- **Signal:** +-- +-- * *property::height* +-- +-- @property height +-- @param height + +-- Add the geometry helpers to match the wibox API +for _, v in ipairs {"x", "y", "width", "height"} do + client.object["get_"..v] = function(c) + return c:geometry()[v] + end + + client.object["set_"..v] = function(c, value) + return c:geometry({[v] = value}) + end +end + + +--- Restore (=unminimize) a random client. +-- @function awful.client.restore +-- @param s The screen to use. +-- @return The restored client if some client was restored, otherwise nil. +function client.restore(s) + s = s or screen.focused() + local cls = capi.client.get(s) + local tags = s.selected_tags + for _, c in pairs(cls) do + local ctags = c:tags() + if c.minimized then + for _, t in ipairs(tags) do + if util.table.hasitem(ctags, t) then + c.minimized = false + return c + end + end + end + end + return nil +end + +--- Normalize a set of numbers to 1 +-- @param set the set of numbers to normalize +-- @param num the number of numbers to normalize +local function normalize(set, num) + num = num or #set + local total = 0 + if num then + for i = 1,num do + total = total + set[i] + end + for i = 1,num do + set[i] = set[i] / total + end + else + for _,v in ipairs(set) do + total = total + v + end + + for i,v in ipairs(set) do + set[i] = v / total + end + end +end + +--- Calculate a client's column number, index in that column, and +-- number of visible clients in this column. +-- +-- @legacylayout awful.client.idx +-- @client c the client +-- @return col the column number +-- @return idx index of the client in the column +-- @return num the number of visible clients in the column +function client.idx(c) + c = c or capi.client.focus + if not c then return end + + -- Only check the tiled clients, the others un irrelevant + local clients = client.tiled(c.screen) + local idx = nil + for k, cl in ipairs(clients) do + if cl == c then + idx = k + break + end + end + + local t = c.screen.selected_tag + local nmaster = t.master_count + + -- This will happen for floating or maximized clients + if not idx then return nil end + + if idx <= nmaster then + return {idx = idx, col=0, num=nmaster} + end + local nother = #clients - nmaster + idx = idx - nmaster + + -- rather than regenerate the column number we can calculate it + -- based on the how the tiling algorithm places clients we calculate + -- the column, we could easily use the for loop in the program but we can + -- calculate it. + local ncol = t.column_count + -- minimum number of clients per column + local percol = math.floor(nother / ncol) + -- number of columns with an extra client + local overcol = math.fmod(nother, ncol) + -- number of columns filled with [percol] clients + local regcol = ncol - overcol + + local col = math.floor( (idx - 1) / percol) + 1 + if col > regcol then + -- col = math.floor( (idx - (percol*regcol) - 1) / (percol + 1) ) + regcol + 1 + -- simplified + col = math.floor( (idx + regcol + percol) / (percol+1) ) + -- calculate the index in the column + idx = idx - percol*regcol - (col - regcol - 1) * (percol+1) + percol = percol+1 + else + idx = idx - percol*(col-1) + end + + return {idx = idx, col=col, num=percol} +end + + +--- Set the window factor of a client +-- +-- @legacylayout awful.client.setwfact +-- @param wfact the window factor value +-- @client c the client +function client.setwfact(wfact, c) + -- get the currently selected window + c = c or capi.client.focus + if not c or not c:isvisible() then return end + + local w = client.idx(c) + + if not w then return end + + local t = c.screen.selected_tag + + -- n is the number of windows currently visible for which we have to be concerned with the properties + local data = t.windowfact or {} + local colfact = data[w.col] + + local need_normalize = colfact ~= nil + + if not need_normalize then + colfact = {} + end + + colfact[w.idx] = wfact + + if not need_normalize then + t:emit_signal("property::windowfact") + return + end + + local rest = 1-wfact + + -- calculate the current denominator + local total = 0 + for i = 1,w.num do + if i ~= w.idx then + total = total + colfact[i] + end + end + + -- normalize the windows + for i = 1,w.num do + if i ~= w.idx then + colfact[i] = (colfact[i] * rest) / total + end + end + + t:emit_signal("property::windowfact") +end + +--- Change window factor of a client. +-- +-- @legacylayout awful.client.incwfact +-- @tparam number add Amount to increase/decrease the client's window factor. +-- Should be between `-current_window_factor` and something close to +-- infinite. The normalisation then ensures that the sum of all factors is 1. +-- @client c the client +function client.incwfact(add, c) + c = c or capi.client.focus + if not c then return end + + local t = c.screen.selected_tag + local w = client.idx(c) + local data = t.windowfact or {} + local colfact = data[w.col] or {} + local curr = colfact[w.idx] or 1 + colfact[w.idx] = curr + add + + -- keep our ratios normalized + normalize(colfact, w.num) + + t:emit_signal("property::windowfact") +end + +--- Get a client dockable state. +-- +-- @client c A client. +-- @return True or false. Note that some windows might be dockable even if you +-- did not set them manually. For example, windows with a type "utility", +-- "toolbar" or "dock" +-- @deprecated awful.client.dockable.get +function client.dockable.get(c) + util.deprecate("Use c.dockable instead of awful.client.dockable.get") + return client.object.get_dockable(c) +end + +--- If the client is dockable. +-- A dockable client is an application confined to the edge of the screen. The +-- space it occupy is substracted from the `screen.workarea`. +-- +-- **Signal:** +-- +-- * *property::dockable* +-- +-- @property dockable +-- @param boolean The dockable state + +function client.object.get_dockable(c) + local value = client.property.get(c, "dockable") + + -- Some sane defaults + if value == nil then + if (c.type == "utility" or c.type == "toolbar" or c.type == "dock") then + value = true + else + value = false + end + end + + return value +end + +--- Set a client dockable state, overriding auto-detection. +-- With this enabled you can dock windows by moving them from the center +-- to the edge of the workarea. +-- +-- @client c A client. +-- @param value True or false. +-- @deprecated awful.client.dockable.set +function client.dockable.set(c, value) + util.deprecate("Use c.dockable = value instead of awful.client.dockable.set") + client.property.set(c, "dockable", value) +end + +--- Get a client property. +-- +-- This method is deprecated. It is now possible to use `c.value` directly. +-- +-- @client c The client. +-- @param prop The property name. +-- @return The property. +-- @deprecated awful.client.property.get +function client.property.get(c, prop) + if not c.data._persistent_properties_loaded then + c.data._persistent_properties_loaded = true + for p in pairs(client.data.persistent_properties_registered) do + local value = c:get_xproperty("awful.client.property." .. p) + if value ~= nil then + client.property.set(c, p, value) + end + end + end + if c.data.awful_client_properties then + return c.data.awful_client_properties[prop] + end +end + +--- Set a client property. +-- +-- This method is deprecated. It is now possible to use `c.value = value` +-- directly. +-- +-- @client c The client. +-- @param prop The property name. +-- @param value The value. +-- @deprecated awful.client.property.set +function client.property.set(c, prop, value) + if not c.data.awful_client_properties then + c.data.awful_client_properties = {} + end + if c.data.awful_client_properties[prop] ~= value then + if client.data.persistent_properties_registered[prop] then + c:set_xproperty("awful.client.property." .. prop, value) + end + c.data.awful_client_properties[prop] = value + c:emit_signal("property::" .. prop) + end +end + +--- Set a client property to be persistent across restarts (via X properties). +-- +-- @function awful.client.property.persist +-- @param prop The property name. +-- @param kind The type (used for register_xproperty). +-- One of "string", "number" or "boolean". +function client.property.persist(prop, kind) + local xprop = "awful.client.property." .. prop + capi.awesome.register_xproperty(xprop, kind) + client.data.persistent_properties_registered[prop] = true + + -- Make already-set properties persistent + for c in pairs(capi.client.get()) do + if c.data.awful_client_properties and c.data.awful_client_properties[prop] ~= nil then + c:set_xproperty(xprop, c.data.awful_client_properties[prop]) + end + end +end + +--- +-- Returns an iterator to cycle through, starting from the client in focus or +-- the given index, all clients that match a given criteria. +-- +-- @param filter a function that returns true to indicate a positive match +-- @param start what index to start iterating from. Defaults to using the +-- index of the currently focused client. +-- @param s which screen to use. nil means all screens. +-- +-- @function awful.client.iterate +-- @usage -- un-minimize all urxvt instances +-- local urxvt = function (c) +-- return awful.rules.match(c, {class = "URxvt"}) +-- end +-- +-- for c in awful.client.iterate(urxvt) do +-- c.minimized = false +-- end +function client.iterate(filter, start, s) + local clients = capi.client.get(s) + local focused = capi.client.focus + start = start or util.table.hasitem(clients, focused) + return util.table.iterate(clients, filter, start) +end + +--- Switch to a client matching the given condition if running, else spawn it. +-- If multiple clients match the given condition then the next one is +-- focussed. +-- +-- @param cmd the command to execute +-- @param matcher a function that returns true to indicate a matching client +-- @tparam bool|function merge If true then merge tags (select the client's +-- first tag additionally) when the client is not visible. +-- If it is a function, it will be called with the client as argument. +-- +-- @function awful.client.run_or_raise +-- @usage -- run or raise urxvt (perhaps, with tabs) on modkey + semicolon +-- awful.key({ modkey, }, 'semicolon', function () +-- local matcher = function (c) +-- return awful.rules.match(c, {class = 'URxvt'}) +-- end +-- awful.client.run_or_raise('urxvt', matcher) +-- end); +function client.run_or_raise(cmd, matcher, merge) + local clients = capi.client.get() + local findex = util.table.hasitem(clients, capi.client.focus) or 1 + local start = util.cycle(#clients, findex + 1) + + local c = client.iterate(matcher, start)() + if c then + c:jump_to(merge) + else + -- client not found, spawn it + spawn(cmd) + end +end + +--- Get a matching transient_for client (if any). +-- @deprecated awful.client.get_transient_for_matching +-- @see client.get_transient_for_matching +-- @client c The client. +-- @tparam function matcher A function that should return true, if +-- a matching parent client is found. +-- @treturn client.client|nil The matching parent client or nil. +function client.get_transient_for_matching(c, matcher) + util.deprecate("Use c:get_transient_for_matching(matcher) instead of".. + "awful.client.get_transient_for_matching") + + return client.object.get_transient_for_matching(c, matcher) +end + +--- Get a matching transient_for client (if any). +-- @function client.get_transient_for_matching +-- @tparam function matcher A function that should return true, if +-- a matching parent client is found. +-- @treturn client.client|nil The matching parent client or nil. +function client.object.get_transient_for_matching(self, matcher) + local tc = self.transient_for + while tc do + if matcher(tc) then + return tc + end + tc = tc.transient_for + end + return nil +end + +--- Is a client transient for another one? +-- @deprecated awful.client.is_transient_for +-- @see client.is_transient_for +-- @client c The child client (having transient_for). +-- @client c2 The parent client to check. +-- @treturn client.client|nil The parent client or nil. +function client.is_transient_for(c, c2) + util.deprecate("Use c:is_transient_for(c2) instead of".. + "awful.client.is_transient_for") + return client.object.is_transient_for(c, c2) +end + +--- Is a client transient for another one? +-- @function client.is_transient_for +-- @client c2 The parent client to check. +-- @treturn client.client|nil The parent client or nil. +function client.object.is_transient_for(self, c2) + local tc = self + while tc.transient_for do + if tc.transient_for == c2 then + return tc + end + tc = tc.transient_for + end + return nil +end + +-- Register standards signals + +--- The last geometry when client was floating. +-- @signal property::floating_geometry + +--- Emited when a client need to get a titlebar. +-- @signal request::titlebars +-- @tparam[opt=nil] string content The context (like "rules") +-- @tparam[opt=nil] table hints Some hints. + +--- The client marked signal (deprecated). +-- @signal .marked + +--- The client unmarked signal (deprecated). +-- @signal unmarked + +-- Add clients during startup to focus history. +-- This used to happen through ewmh.activate, but that only handles visible +-- clients now. +capi.client.connect_signal("manage", function (c) + if awesome.startup then + client.focus.history.add(c) + end +end) +capi.client.connect_signal("unmanage", client.focus.history.delete) + +capi.client.connect_signal("unmanage", client.floating.delete) + +-- Connect to "focus" signal, and allow to disable tracking. +do + local disabled_count = 1 + --- Disable history tracking. + -- + -- See `awful.client.focus.history.enable_tracking` to enable it again. + -- @treturn int The internal value of `disabled_count` (calls to this + -- function without calling `awful.client.focus.history.enable_tracking`). + -- @function awful.client.focus.history.disable_tracking + function client.focus.history.disable_tracking() + disabled_count = disabled_count + 1 + if disabled_count == 1 then + capi.client.disconnect_signal("focus", client.focus.history.add) + end + return disabled_count + end + + --- Enable history tracking. + -- + -- This is the default, but can be disabled + -- through `awful.client.focus.history.disable_tracking`. + -- @treturn boolean True if history tracking has been enabled. + -- @function awful.client.focus.history.enable_tracking + function client.focus.history.enable_tracking() + assert(disabled_count > 0) + disabled_count = disabled_count - 1 + if disabled_count == 0 then + capi.client.connect_signal("focus", client.focus.history.add) + end + return disabled_count == 0 + end + + --- Is history tracking enabled? + -- @treturn bool True if history tracking is enabled. + -- @treturn int The number of times that tracking has been disabled. + -- @function awful.client.focus.history.is_enabled + function client.focus.history.is_enabled() + return disabled_count == 0, disabled_count + end +end +client.focus.history.enable_tracking() + +-- Register persistent properties +client.property.persist("floating", "boolean") + +-- Extend the luaobject +object.properties(capi.client, { + getter_class = client.object, + setter_class = client.object, + getter_fallback = client.property.get, + setter_fallback = client.property.set, +}) + +return client + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/client/focus.lua b/lib/awful/client/focus.lua new file mode 100644 index 0000000..a8b04f2 --- /dev/null +++ b/lib/awful/client/focus.lua @@ -0,0 +1,215 @@ +--------------------------------------------------------------------------- +--- Keep track of the focused clients. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @submodule client +--------------------------------------------------------------------------- +local grect = require("gears.geometry").rectangle + +local capi = +{ + screen = screen, + client = client, +} + +-- We use a metatable to prevent circular dependency loops. +local screen +do + screen = setmetatable({}, { + __index = function(_, k) + screen = require("awful.screen") + return screen[k] + end, + __newindex = error -- Just to be sure in case anything ever does this + }) +end + +local client +do + client = setmetatable({}, { + __index = function(_, k) + client = require("awful.client") + return client[k] + end, + __newindex = error -- Just to be sure in case anything ever does this + }) +end + +local focus = {history = {list = {}}} + +local function get_screen(s) + return s and capi.screen[s] +end + +--- Remove a client from the focus history +-- +-- @client c The client that must be removed. +-- @function awful.client.focus.history.delete +function focus.history.delete(c) + for k, v in ipairs(focus.history.list) do + if v == c then + table.remove(focus.history.list, k) + break + end + end +end + +--- Focus a client by its relative index. +-- +-- @function awful.client.focus.byidx +-- @param i The index. +-- @client[opt] c The client. +function focus.byidx(i, c) + local target = client.next(i, c) + if target then + target:emit_signal("request::activate", "client.focus.byidx", + {raise=true}) + end +end + +--- Filter out window that we do not want handled by focus. +-- This usually means that desktop, dock and splash windows are +-- not registered and cannot get focus. +-- +-- @client c A client. +-- @return The same client if it's ok, nil otherwise. +-- @function awful.client.focus.filter +function focus.filter(c) + if c.type == "desktop" + or c.type == "dock" + or c.type == "splash" + or not c.focusable then + return nil + end + return c +end + +--- Update client focus history. +-- +-- @client c The client that has been focused. +-- @function awful.client.focus.history.add +function focus.history.add(c) + -- Remove the client if its in stack + focus.history.delete(c) + -- Record the client has latest focused + table.insert(focus.history.list, 1, c) +end + +--- Get the latest focused client for a screen in history. +-- +-- @tparam int|screen s The screen to look for. +-- @tparam int idx The index: 0 will return first candidate, +-- 1 will return second, etc. +-- @tparam function filter An optional filter. If no client is found in the +-- first iteration, `awful.client.focus.filter` is used by default to get any +-- client. +-- @treturn client.object A client. +-- @function awful.client.focus.history.get +function focus.history.get(s, idx, filter) + s = get_screen(s) + -- When this counter is equal to idx, we return the client + local counter = 0 + local vc = client.visible(s, true) + for _, c in ipairs(focus.history.list) do + if get_screen(c.screen) == s then + if not filter or filter(c) then + for _, vcc in ipairs(vc) do + if vcc == c then + if counter == idx then + return c + end + -- We found one, increment the counter only. + counter = counter + 1 + break + end + end + end + end + end + -- Argh nobody found in history, give the first one visible if there is one + -- that passes the filter. + filter = filter or focus.filter + if counter == 0 then + for _, v in ipairs(vc) do + if filter(v) then + return v + end + end + end +end + +--- Focus the previous client in history. +-- @function awful.client.focus.history.previous +function focus.history.previous() + local sel = capi.client.focus + local s = sel and sel.screen or screen.focused() + local c = focus.history.get(s, 1) + if c then + c:emit_signal("request::activate", "client.focus.history.previous", + {raise=false}) + end +end + +--- Focus a client by the given direction. +-- +-- @tparam string dir The direction, can be either +-- `"up"`, `"down"`, `"left"` or `"right"`. +-- @client[opt] c The client. +-- @tparam[opt=false] boolean stacked Use stacking order? (top to bottom) +-- @function awful.client.focus.bydirection +function focus.bydirection(dir, c, stacked) + local sel = c or capi.client.focus + if sel then + local cltbl = client.visible(sel.screen, stacked) + local geomtbl = {} + for i,cl in ipairs(cltbl) do + geomtbl[i] = cl:geometry() + end + + local target = grect.get_in_direction(dir, geomtbl, sel:geometry()) + + -- If we found a client to focus, then do it. + if target then + cltbl[target]:emit_signal("request::activate", + "client.focus.bydirection", {raise=false}) + end + end +end + +--- Focus a client by the given direction. Moves across screens. +-- +-- @param dir The direction, can be either "up", "down", "left" or "right". +-- @client[opt] c The client. +-- @tparam[opt=false] boolean stacked Use stacking order? (top to bottom) +-- @function awful.client.focus.global_bydirection +function focus.global_bydirection(dir, c, stacked) + local sel = c or capi.client.focus + local scr = get_screen(sel and sel.screen or screen.focused()) + + -- change focus inside the screen + focus.bydirection(dir, sel) + + -- if focus not changed, we must change screen + if sel == capi.client.focus then + screen.focus_bydirection(dir, scr) + if scr ~= get_screen(screen.focused()) then + local cltbl = client.visible(screen.focused(), stacked) + local geomtbl = {} + for i,cl in ipairs(cltbl) do + geomtbl[i] = cl:geometry() + end + local target = grect.get_in_direction(dir, geomtbl, scr.geometry) + + if target then + cltbl[target]:emit_signal("request::activate", + "client.focus.global_bydirection", + {raise=false}) + end + end + end +end + +return focus + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/client/shape.lua b/lib/awful/client/shape.lua new file mode 100644 index 0000000..e51d873 --- /dev/null +++ b/lib/awful/client/shape.lua @@ -0,0 +1,93 @@ +--------------------------------------------------------------------------- +--- Handle client shapes. +-- +-- @author Uli Schlachter <psychon@znc.in> +-- @copyright 2014 Uli Schlachter +-- @submodule client +--------------------------------------------------------------------------- + +-- Grab environment we need +local surface = require("gears.surface") +local cairo = require("lgi").cairo +local capi = +{ + client = client, +} + +local shape = {} +shape.update = {} + +--- Get one of a client's shapes and transform it to include window decorations. +-- @function awful.shape.get_transformed +-- @client c The client whose shape should be retrieved +-- @tparam string shape_name Either "bounding" or "clip" +function shape.get_transformed(c, shape_name) + local border = shape_name == "bounding" and c.border_width or 0 + local shape_img = surface.load_silently(c["client_shape_" .. shape_name], false) + if not shape_img then return end + + -- Get information about various sizes on the client + local geom = c:geometry() + local _, t = c:titlebar_top() + local _, b = c:titlebar_bottom() + local _, l = c:titlebar_left() + local _, r = c:titlebar_right() + + -- Figure out the size of the shape that we need + local img_width = geom.width + 2*border + local img_height = geom.height + 2*border + local result = cairo.ImageSurface(cairo.Format.A1, img_width, img_height) + local cr = cairo.Context(result) + + -- Fill everything (this paints the titlebars and border) + cr:paint() + + -- Draw the client's shape in the middle + cr:set_operator(cairo.Operator.SOURCE) + cr:set_source_surface(shape_img, border + l, border + t) + cr:rectangle(border + l, border + t, geom.width - l - r, geom.height - t - b) + cr:fill() + + return result +end + +--- Update a client's bounding shape from the shape the client set itself. +-- @function awful.shape.update.bounding +-- @client c The client to act on +function shape.update.bounding(c) + local res = shape.get_transformed(c, "bounding") + c.shape_bounding = res and res._native + -- Free memory + if res then + res:finish() + end +end + +--- Update a client's clip shape from the shape the client set itself. +-- @function awful.shape.update.clip +-- @client c The client to act on +function shape.update.clip(c) + local res = shape.get_transformed(c, "clip") + c.shape_clip = res and res._native + -- Free memory + if res then + res:finish() + end +end + +--- Update all of a client's shapes from the shapes the client set itself. +-- @function awful.shape.update.all +-- @client c The client to act on +function shape.update.all(c) + shape.update.bounding(c) + shape.update.clip(c) +end + +capi.client.connect_signal("property::shape_client_bounding", shape.update.bounding) +capi.client.connect_signal("property::shape_client_clip", shape.update.clip) +capi.client.connect_signal("property::width", shape.update.all) +capi.client.connect_signal("property::height", shape.update.all) + +return shape + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/client/urgent.lua b/lib/awful/client/urgent.lua new file mode 100644 index 0000000..22f6a6d --- /dev/null +++ b/lib/awful/client/urgent.lua @@ -0,0 +1,88 @@ +--------------------------------------------------------------------------- +--- Keep track of the urgent clients. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @submodule client +--------------------------------------------------------------------------- + +local urgent = {} + +local capi = +{ + client = client, +} + +local client +do + client = setmetatable({}, { + __index = function(_, k) + client = require("awful.client") + return client[k] + end, + __newindex = error -- Just to be sure in case anything ever does this + }) +end + +local data = setmetatable({}, { __mode = 'k' }) + +--- Get the first client that got the urgent hint. +-- +-- @function awful.urgent.get +-- @treturn client.object The first urgent client. +function urgent.get() + if #data > 0 then + return data[1] + else + -- fallback behaviour: iterate through clients and get the first urgent + local clients = capi.client.get() + for _, cl in pairs(clients) do + if cl.urgent then + return cl + end + end + end +end + +--- Jump to the client that received the urgent hint first. +-- +-- @function awful.urgent.jumpto +-- @tparam bool|function merge If true then merge tags (select the client's +-- first tag additionally) when the client is not visible. +-- If it is a function, it will be called with the client as argument. +function urgent.jumpto(merge) + local c = client.urgent.get() + if c then + c:jump_to(merge) + end +end + +--- Adds client to urgent stack. +-- +-- @function awful.urgent.add +-- @client c The client object. +-- @param prop The property which is updated. +function urgent.add(c, prop) + if type(c) == "client" and prop == "urgent" and c.urgent then + table.insert(data, c) + end +end + +--- Remove client from urgent stack. +-- +-- @function awful.urgent.delete +-- @client c The client object. +function urgent.delete(c) + for k, cl in ipairs(data) do + if c == cl then + table.remove(data, k) + break + end + end +end + +capi.client.connect_signal("property::urgent", urgent.add) +capi.client.connect_signal("focus", urgent.delete) +capi.client.connect_signal("unmanage", urgent.delete) + +return urgent diff --git a/lib/awful/completion.lua b/lib/awful/completion.lua new file mode 100644 index 0000000..3462ed1 --- /dev/null +++ b/lib/awful/completion.lua @@ -0,0 +1,201 @@ +--------------------------------------------------------------------------- +--- Completion module. +-- +-- This module store a set of function using shell to complete commands name. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @author Sébastien Gross <seb-awesome@chezwam.org> +-- @copyright 2008 Julien Danjou, Sébastien Gross +-- @module awful.completion +--------------------------------------------------------------------------- + +-- Grab environment we need +local io = io +local os = os +local table = table +local math = math +local print = print +local pairs = pairs +local string = string + +local completion = {} + +-- mapping of command/completion function +local bashcomp_funcs = {} +local bashcomp_src = "/etc/bash_completion" + +--- Enable programmable bash completion in awful.completion.bash at the price of +-- a slight overhead. +-- @param src The bash completion source file, /etc/bash_completion by default. +function completion.bashcomp_load(src) + if src then bashcomp_src = src end + local c, err = io.popen("/usr/bin/env bash -c 'source " .. bashcomp_src .. "; complete -p'") + if c then + while true do + local line = c:read("*line") + if not line then break end + -- if a bash function is used for completion, register it + if line:match(".* -F .*") then + bashcomp_funcs[line:gsub(".* (%S+)$","%1")] = line:gsub(".*-F +(%S+) .*$", "%1") + end + end + c:close() + else + print(err) + end +end + +local function bash_escape(str) + str = str:gsub(" ", "\\ ") + str = str:gsub("%[", "\\[") + str = str:gsub("%]", "\\]") + str = str:gsub("%(", "\\(") + str = str:gsub("%)", "\\)") + return str +end + +--- Use shell completion system to complete command and filename. +-- @param command The command line. +-- @param cur_pos The cursor position. +-- @param ncomp The element number to complete. +-- @param shell The shell to use for completion (bash (default) or zsh). +-- @return The new command, the new cursor position, the table of all matches. +function completion.shell(command, cur_pos, ncomp, shell) + local wstart = 1 + local wend = 1 + local words = {} + local cword_index = 0 + local cword_start = 0 + local cword_end = 0 + local i = 1 + local comptype = "file" + + -- do nothing if we are on a letter, i.e. not at len + 1 or on a space + if cur_pos ~= #command + 1 and command:sub(cur_pos, cur_pos) ~= " " then + return command, cur_pos + elseif #command == 0 then + return command, cur_pos + end + + while wend <= #command do + wend = command:find(" ", wstart) + if not wend then wend = #command + 1 end + table.insert(words, command:sub(wstart, wend - 1)) + if cur_pos >= wstart and cur_pos <= wend + 1 then + cword_start = wstart + cword_end = wend + cword_index = i + end + wstart = wend + 1 + i = i + 1 + end + + if cword_index == 1 and not string.find(words[cword_index], "/") then + comptype = "command" + end + + local shell_cmd + if shell == "zsh" or (not shell and os.getenv("SHELL"):match("zsh$")) then + if comptype == "file" then + -- NOTE: ${~:-"..."} turns on GLOB_SUBST, useful for expansion of + -- "~/" ($HOME). ${:-"foo"} is the string "foo" as var. + shell_cmd = "/usr/bin/env zsh -c 'local -a res; res=( ${~:-" + .. string.format('%q', words[cword_index]) .. "}* ); " + .. "print -ln -- ${res[@]}'" + else + -- check commands, aliases, builtins, functions and reswords + shell_cmd = "/usr/bin/env zsh -c 'local -a res; ".. + "res=( ".. + "\"${(k)commands[@]}\" \"${(k)aliases[@]}\" \"${(k)builtins[@]}\" \"${(k)functions[@]}\" \"${(k)reswords[@]}\" ".. + "${PWD}/*(:t)".. + "); ".. + "print -ln -- ${(M)res[@]:#" .. string.format('%q', words[cword_index]) .. "*}'" + end + else + if bashcomp_funcs[words[1]] then + -- fairly complex command with inline bash script to get the possible completions + shell_cmd = "/usr/bin/env bash -c 'source " .. bashcomp_src .. "; " .. + "__print_completions() { for ((i=0;i<${#COMPREPLY[*]};i++)); do echo ${COMPREPLY[i]}; done }; " .. + "COMP_WORDS=(" .. command .."); COMP_LINE=\"" .. command .. "\"; " .. + "COMP_COUNT=" .. cur_pos .. "; COMP_CWORD=" .. cword_index-1 .. "; " .. + bashcomp_funcs[words[1]] .. "; __print_completions'" + else + shell_cmd = "/usr/bin/env bash -c 'compgen -A " .. comptype .. " " + .. string.format('%q', words[cword_index]) .. "'" + end + end + local c, err = io.popen(shell_cmd .. " | sort -u") + local output = {} + if c then + while true do + local line = c:read("*line") + if not line then break end + if os.execute("test -d " .. string.format('%q', line)) == 0 then + line = line .. "/" + end + table.insert(output, bash_escape(line)) + end + + c:close() + else + print(err) + end + + -- no completion, return + if #output == 0 then + return command, cur_pos + end + + -- cycle + while ncomp > #output do + ncomp = ncomp - #output + end + + local str = command:sub(1, cword_start - 1) .. output[ncomp] .. command:sub(cword_end) + cur_pos = cword_end + #output[ncomp] + 1 + + return str, cur_pos, output +end + +--- Run a generic completion. +-- For this function to run properly the awful.completion.keyword table should +-- be fed up with all keywords. The completion is run against these keywords. +-- @param text The current text the user had typed yet. +-- @param cur_pos The current cursor position. +-- @param ncomp The number of yet requested completion using current text. +-- @param keywords The keywords table uised for completion. +-- @return The new match, the new cursor position, the table of all matches. +function completion.generic(text, cur_pos, ncomp, keywords) -- luacheck: no unused args + -- The keywords table may be empty + if #keywords == 0 then + return text, #text + 1 + end + + -- if no text had been typed yet, then we could start cycling around all + -- keywords with out filtering and move the cursor at the end of keyword + if text == nil or #text == 0 then + ncomp = math.fmod(ncomp - 1, #keywords) + 1 + return keywords[ncomp], #keywords[ncomp] + 2 + end + + -- Filter out only keywords starting with text + local matches = {} + for _, x in pairs(keywords) do + if x:sub(1, #text) == text then + table.insert(matches, x) + end + end + + -- if there are no matches just leave out with the current text and position + if #matches == 0 then + return text, #text + 1, matches + end + + -- cycle around all matches + ncomp = math.fmod(ncomp - 1, #matches) + 1 + return matches[ncomp], #matches[ncomp] + 1, matches +end + +return completion + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/dbus.lua b/lib/awful/dbus.lua new file mode 100644 index 0000000..8660dce --- /dev/null +++ b/lib/awful/dbus.lua @@ -0,0 +1,19 @@ +--------------------------------------------------------------------------- +--- D-Bus module for awful. +-- +-- This module simply request the org.awesomewm.awful name on the D-Bus +-- for futur usage by other awful modules. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @module awful.dbus +--------------------------------------------------------------------------- + +-- Grab environment we need +local dbus = dbus + +if dbus then + dbus.request_name("session", "org.awesomewm.awful") +end + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/ewmh.lua b/lib/awful/ewmh.lua new file mode 100644 index 0000000..eb72a16 --- /dev/null +++ b/lib/awful/ewmh.lua @@ -0,0 +1,295 @@ +--------------------------------------------------------------------------- +--- Implements EWMH requests handling. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @module awful.ewmh +--------------------------------------------------------------------------- + +local client = client +local screen = screen +local ipairs = ipairs +local util = require("awful.util") +local aclient = require("awful.client") +local aplace = require("awful.placement") +local asuit = require("awful.layout.suit") + +local ewmh = { + generic_activate_filters = {}, + contextual_activate_filters = {}, +} + +--- The list of all registered generic request::activate (focus stealing) +-- filters. If a filter is added to only one context, it will be in +-- `ewmh.contextual_activate_filters`["context_name"]. +-- @table[opt={}] generic_activate_filters +-- @see ewmh.activate +-- @see ewmh.add_activate_filter +-- @see ewmh.remove_activate_filter + +--- The list of all registered contextual request::activate (focus stealing) +-- filters. If a filter is added to only one context, it will be in +-- `ewmh.generic_activate_filters`. +-- @table[opt={}] contextual_activate_filters +-- @see ewmh.activate +-- @see ewmh.add_activate_filter +-- @see ewmh.remove_activate_filter + +--- Update a client's settings when its geometry changes, skipping signals +-- resulting from calls within. +local geometry_change_lock = false +local function geometry_change(window) + if geometry_change_lock then return end + geometry_change_lock = true + + -- Fix up the geometry in case this window needs to cover the whole screen. + local bw = window.border_width or 0 + local g = window.screen.workarea + if window.maximized_vertical then + window:geometry { height = g.height - 2*bw, y = g.y } + end + if window.maximized_horizontal then + window:geometry { width = g.width - 2*bw, x = g.x } + end + if window.fullscreen then + window.border_width = 0 + window:geometry(window.screen.geometry) + end + + geometry_change_lock = false +end + +--- Activate a window. +-- +-- This sets the focus only if the client is visible. +-- +-- It is the default signal handler for `request::activate` on a `client`. +-- +-- @signalhandler awful.ewmh.activate +-- @client c A client to use +-- @tparam string context The context where this signal was used. +-- @tparam[opt] table hints A table with additional hints: +-- @tparam[opt=false] boolean hints.raise should the client be raised? +function ewmh.activate(c, context, hints) -- luacheck: no unused args + hints = hints or {} + + if c.focusable == false and not hints.force then return end + + local found, ret = false + + -- Execute the filters until something handle the request + for _, tab in ipairs { + ewmh.contextual_activate_filters[context] or {}, + ewmh.generic_activate_filters + } do + for i=#tab, 1, -1 do + ret = tab[i](c, context, hints) + if ret ~= nil then found=true; break end + end + + if found then break end + end + + if ret ~= false and c:isvisible() then + client.focus = c + elseif ret == false and not hints.force then + return + end + + if hints and hints.raise then + c:raise() + if not awesome.startup and not c:isvisible() then + c.urgent = true + end + end +end + +--- Add an activate (focus stealing) filter function. +-- +-- The callback takes the following parameters: +-- +-- * **c** (*client*) The client requesting the activation +-- * **context** (*string*) The activation context. +-- * **hints** (*table*) Some additional hints (depending on the context) +-- +-- If the callback returns `true`, the client will be activated unless the `force` +-- hint is set. If the callback returns `false`, the activation request is +-- cancelled. If the callback returns `nil`, the previous callback will be +-- executed. This will continue until either a callback handles the request or +-- when it runs out of callbacks. In that case, the request will be granted if +-- the client is visible. +-- +-- For example, to block Firefox from stealing the focus, use: +-- +-- awful.ewmh.add_activate_filter(function(c, "ewmh") +-- if c.class == "Firefox" then return false end +-- end) +-- +-- @tparam function f The callback +-- @tparam[opt] string context The `request::activate` context +-- @see generic_activate_filters +-- @see contextual_activate_filters +-- @see remove_activate_filter +function ewmh.add_activate_filter(f, context) + if not context then + table.insert(ewmh.generic_activate_filters, f) + else + ewmh.contextual_activate_filters[context] = + ewmh.contextual_activate_filters[context] or {} + + table.insert(ewmh.contextual_activate_filters[context], f) + end +end + +--- Remove an activate (focus stealing) filter function. +-- This is an helper to avoid dealing with `ewmh.add_activate_filter` directly. +-- @tparam function f The callback +-- @tparam[opt] string context The `request::activate` context +-- @treturn boolean If the callback existed +-- @see generic_activate_filters +-- @see contextual_activate_filters +-- @see add_activate_filter +function ewmh.remove_activate_filter(f, context) + local tab = context and (ewmh.contextual_activate_filters[context] or {}) + or ewmh.generic_activate_filters + + for k, v in ipairs(tab) do + if v == f then + table.remove(tab, k) + + -- In case the callback is there multiple time. + ewmh.remove_activate_filter(f, context) + + return true + end + end + + return false +end + +-- Get tags that are on the same screen as the client. This should _almost_ +-- always return the same content as c:tags(). +local function get_valid_tags(c, s) + local tags, new_tags = c:tags(), {} + + for _, t in ipairs(tags) do + if s == t.screen then + table.insert(new_tags, t) + end + end + + return new_tags +end + +--- Tag a window with its requested tag. +-- +-- It is the default signal handler for `request::tag` on a `client`. +-- +-- @signalhandler awful.ewmh.tag +-- @client c A client to tag +-- @tparam[opt] tag|boolean t A tag to use. If true, then the client is made sticky. +-- @tparam[opt={}] table hints Extra information +function ewmh.tag(c, t, hints) --luacheck: no unused + -- There is nothing to do + if not t and #get_valid_tags(c, c.screen) > 0 then return end + + if not t then + if c.transient_for then + c.screen = c.transient_for.screen + if not c.sticky then + c:tags(c.transient_for:tags()) + end + else + c:to_selected_tags() + end + elseif type(t) == "boolean" and t then + c.sticky = true + else + c.screen = t.screen + c:tags({ t }) + end +end + +--- Handle client urgent request +-- @signalhandler awful.ewmh.urgent +-- @client c A client +-- @tparam boolean urgent If the client should be urgent +function ewmh.urgent(c, urgent) + if c ~= client.focus and not aclient.property.get(c,"ignore_urgent") then + c.urgent = urgent + end +end + +-- Map the state to the action name +local context_mapper = { + maximized_vertical = "maximize_vertically", + maximized_horizontal = "maximize_horizontally", + fullscreen = "maximize" +} + +--- Move and resize the client. +-- +-- This is the default geometry request handler. +-- +-- @signalhandler awful.ewmh.geometry +-- @tparam client c The client +-- @tparam string context The context +-- @tparam[opt={}] table hints The hints to pass to the handler +function ewmh.geometry(c, context, hints) + local layout = c.screen.selected_tag and c.screen.selected_tag.layout or nil + + -- Setting the geometry wont work unless the client is floating. + if (not c.floating) and (not layout == asuit.floating) then + return + end + + context = context or "" + + local original_context = context + + -- Now, map it to something useful + context = context_mapper[context] or context + + local props = util.table.clone(hints or {}, false) + props.store_geometry = props.store_geometry==nil and true or props.store_geometry + + -- If it is a known placement function, then apply it, otherwise let + -- other potential handler resize the client (like in-layout resize or + -- floating client resize) + if aplace[context] then + + -- Check if it correspond to a boolean property + local state = c[original_context] + + -- If the property is boolean and it correspond to the undo operation, + -- restore the stored geometry. + if state == false then + aplace.restore(c,{context=context}) + return + end + + local honor_default = original_context ~= "fullscreen" + + if props.honor_workarea == nil then + props.honor_workarea = honor_default + end + + aplace[context](c, props) + end +end + +client.connect_signal("request::activate", ewmh.activate) +client.connect_signal("request::tag", ewmh.tag) +client.connect_signal("request::urgent", ewmh.urgent) +client.connect_signal("request::geometry", ewmh.geometry) +client.connect_signal("property::border_width", geometry_change) +client.connect_signal("property::geometry", geometry_change) +screen.connect_signal("property::workarea", function(s) + for _, c in pairs(client.get(s)) do + geometry_change(c) + end +end) + +return ewmh + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/hotkeys_popup/init.lua b/lib/awful/hotkeys_popup/init.lua new file mode 100644 index 0000000..cb8c8be --- /dev/null +++ b/lib/awful/hotkeys_popup/init.lua @@ -0,0 +1,17 @@ +--------------------------------------------------------------------------- +--- Popup widget which shows current hotkeys and their descriptions. +-- +-- @author Yauheni Kirylau <yawghen@gmail.com> +-- @copyright 2014-2015 Yauheni Kirylau +-- @module awful.hotkeys_popup +--------------------------------------------------------------------------- + + +local hotkeys_popup = { + widget = require("awful.hotkeys_popup.widget"), + keys = require("awful.hotkeys_popup.keys") +} +hotkeys_popup.show_help = hotkeys_popup.widget.show_help +return hotkeys_popup + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/hotkeys_popup/keys/init.lua b/lib/awful/hotkeys_popup/keys/init.lua new file mode 100644 index 0000000..cbcbb62 --- /dev/null +++ b/lib/awful/hotkeys_popup/keys/init.lua @@ -0,0 +1,15 @@ +--------------------------------------------------------------------------- +--- Additional hotkeys for awful.hotkeys_widget +-- +-- @author Yauheni Kirylau <yawghen@gmail.com> +-- @copyright 2014-2015 Yauheni Kirylau +-- @module awful.hotkeys_popup.keys +--------------------------------------------------------------------------- + + +local keys = { + vim = require("awful.hotkeys_popup.keys.vim") +} +return keys + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/hotkeys_popup/keys/vim.lua b/lib/awful/hotkeys_popup/keys/vim.lua new file mode 100644 index 0000000..69d21e9 --- /dev/null +++ b/lib/awful/hotkeys_popup/keys/vim.lua @@ -0,0 +1,173 @@ +--------------------------------------------------------------------------- +--- VIM hotkeys for awful.hotkeys_widget +-- +-- @author Yauheni Kirylau <yawghen@gmail.com> +-- @copyright 2014-2015 Yauheni Kirylau +-- @module awful.hotkeys_popup.keys.vim +--------------------------------------------------------------------------- + +local hotkeys_popup = require("awful.hotkeys_popup.widget") + +local vim_rule_any = {name={"vim", "VIM"}} +for group_name, group_data in pairs({ + ["VIM: motion"] = { color="#009F00", rule_any=vim_rule_any }, + ["VIM: command"] = { color="#aFaF00", rule_any=vim_rule_any }, + ["VIM: command (insert)"] = { color="#cF4F40", rule_any=vim_rule_any }, + ["VIM: operator"] = { color="#aF6F00", rule_any=vim_rule_any }, + ["VIM: find"] = { color="#65cF9F", rule_any=vim_rule_any }, + ["VIM: scroll"] = { color="#659FdF", rule_any=vim_rule_any }, +}) do + hotkeys_popup.group_rules[group_name] = group_data +end + + +local vim_keys = { + + ["VIM: motion"] = {{ + modifiers = {}, + keys = { + ['`']="goto mark", + ['0']='"hard" BOL', + ['-']="prev line", + w="next word", + e="end word", + ['[']=". misc", + [']']=". misc", + ["'"]=". goto mk. BOL", + b="prev word", + ["|"]='BOL/goto col', + ["$"]='EOL', + ["%"]='goto matching bracket', + ["^"]='"soft" BOL', + ["("]='sentence begin', + [")"]='sentence end', + ["_"]='"soft" BOL down', + ["+"]='next line', + W='next WORD', + E='end WORD', + ['{']="paragraph begin", + ['}']="paragraph end", + G='EOF/goto line', + H='move cursor to screen top', + M='move cursor to screen middle', + L='move cursor to screen bottom', + B='prev WORD', + } + }, { + modifiers = {"Ctrl"}, + keys = { + u="half page up", + d="half page down", + b="page up", + f="page down", + o="prev mark", + } + }}, + + ["VIM: operator"] = {{ + modifiers = {}, + keys = { + ['=']="auto format", + y="yank", + d="delete", + c="change", + ["!"]='external filter', + ['<']='unindent', + ['>']='indent', + } + }}, + + ["VIM: command"] = {{ + modifiers = {}, + keys = { + ['~']="toggle case", + q=". record macro", + r=". replace char", + u="undo", + p="paste after", + gg="go to the top of file", + gf="open file under cursor", + x="delete char", + v="visual mode", + m=". set mark", + ['.']="repeat command", + ["@"]='. play macro', + ["&"]='repeat :s', + Q='ex mode', + Y='yank line', + U='undo line', + P='paste before cursor', + D='delete to EOL', + J='join lines', + K='help', + [':']='ex cmd line', + ['"']='. register spec', + ZZ='quit and save', + ZQ='quit discarding changes', + X='back-delete', + V='visual lines selection', + } + }, { + modifiers = {"Ctrl"}, + keys = { + w=". window operations", + r="redo", + ["["]="normal mode", + a="increase number", + x="decrease number", + g="file/cursor info", + z="suspend", + c="cancel/normal mode", + v="visual block selection", + } + }}, + + ["VIM: command (insert)"] = {{ + modifiers = {}, + keys = { + i="insert mode", + o="open below", + a="append", + s="subst char", + R='replace mode', + I='insert at BOL', + O='open above', + A='append at EOL', + S='subst line', + C='change to EOL', + } + }}, + + ["VIM: find"] = {{ + modifiers = {}, + keys = { + [';']="repeat t/T/f/F", + [',']="reverse t/T/f/F", + ['/']=". find", + ['?']='. reverse find', + n="next search match", + N='prev search match', + f=". find char", + F='. reverse find char', + t=". 'till char", + T=". reverse 'till char", + ["*"]='find word under cursor', + ["#"]='reverse find under cursor', + } + }}, + + ["VIM: scroll"] = {{ + modifiers = {}, + keys = { + e="scroll line up", + y="scroll line down", + zt="scroll cursor to the top", + zz="scroll cursor to the center", + zb="scroll cursor to the bottom", + } + }}, +} + +hotkeys_popup.add_hotkeys(vim_keys) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/hotkeys_popup/widget.lua b/lib/awful/hotkeys_popup/widget.lua new file mode 100644 index 0000000..62a2231 --- /dev/null +++ b/lib/awful/hotkeys_popup/widget.lua @@ -0,0 +1,482 @@ +--------------------------------------------------------------------------- +--- Popup widget which shows current hotkeys and their descriptions. +-- +-- @author Yauheni Kirylau <yawghen@gmail.com> +-- @copyright 2014-2015 Yauheni Kirylau +-- @module awful.hotkeys_popup.widget +--------------------------------------------------------------------------- + +local capi = { + screen = screen, + client = client, + keygrabber = keygrabber, +} +local awful = require("awful") +local wibox = require("wibox") +local beautiful = require("beautiful") +local dpi = beautiful.xresources.apply_dpi +local compute_textbox_width = require("menubar").utils.compute_textbox_width + + +-- Stripped copy of this module https://github.com/copycat-killer/lain/blob/master/util/markup.lua: +local markup = {} +-- Set the font. +function markup.font(font, text) + return '' .. tostring(text) ..'' +end +-- Set the foreground. +function markup.fg(color, text) + return '' .. tostring(text) .. '' +end +-- Set the background. +function markup.bg(color, text) + return '' .. tostring(text) .. '' +end + +local widget_module = { + group_rules = {}, +} + +--- Don't show hotkeys without descriptions. +widget_module.hide_without_description = true + +--- Merge hotkey records into one if they have the same modifiers and +-- description. +widget_module.merge_duplicates = true + + +function widget_module.new() +local widget = { + hide_without_description = widget_module.hide_without_description, + merge_duplicates = widget_module.merge_duplicates, + group_rules = awful.util.table.clone(widget_module.group_rules), + title_font = "Monospace Bold 9", + description_font = "Monospace 8", + width = dpi(1200), + height = dpi(800), + border_width = beautiful.border_width or dpi(2), + modifiers_color = beautiful.bg_minimize or "#555555", + group_margin = dpi(6), + additional_hotkeys = {}, + labels = { + Mod4="Super", + Mod1="Alt", + Escape="Esc", + Insert="Ins", + Delete="Del", + Backspace="BackSpc", + Return="Enter", + Next="PgDn", + Prior="PgUp", + ['#108']="Alt Gr", + Left='←', + Up='↑', + Right='→', + Down='↓', + ['#67']="F1", + ['#68']="F2", + ['#69']="F3", + ['#70']="F4", + ['#71']="F5", + ['#72']="F6", + ['#73']="F7", + ['#74']="F8", + ['#75']="F9", + ['#76']="F10", + ['#95']="F11", + ['#96']="F12", + ['#10']="1", + ['#11']="2", + ['#12']="3", + ['#13']="4", + ['#14']="5", + ['#15']="6", + ['#16']="7", + ['#17']="8", + ['#18']="9", + ['#19']="0", + ['#20']="-", + ['#21']="=", + Control="Ctrl" + }, +} + +local cached_wiboxes = {} +local cached_awful_keys = nil +local colors_counter = {} +local colors = beautiful.xresources.get_current_theme() +local group_list = {} + + +local function get_next_color(id) + id = id or "default" + if colors_counter[id] then + colors_counter[id] = math.fmod(colors_counter[id] + 1, 15) + 1 + else + colors_counter[id] = 1 + end + return colors["color"..tostring(colors_counter[id], 15)] +end + + +local function join_plus_sort(modifiers) + if #modifiers<1 then return "none" end + table.sort(modifiers) + return table.concat(modifiers, '+') +end + + +local function add_hotkey(key, data, target) + if widget.hide_without_description and not data.description then return end + + local readable_mods = {} + for _, mod in ipairs(data.mod) do + table.insert(readable_mods, widget.labels[mod] or mod) + end + local joined_mods = join_plus_sort(readable_mods) + + local group = data.group or "none" + group_list[group] = true + if not target[group] then target[group] = {} end + local new_key = { + key = (widget.labels[key] or key), + mod = joined_mods, + description = data.description + } + local index = data.description or "none" -- or use its hash? + if not target[group][index] then + target[group][index] = new_key + else + if widget.merge_duplicates and joined_mods == target[group][index].mod then + target[group][index].key = target[group][index].key .. "/" .. new_key.key + else + while target[group][index] do + index = index .. " " + end + target[group][index] = new_key + end + end +end + + +local function sort_hotkeys(target) + -- @TODO: add sort by 12345qwertyasdf etc + for group, _ in pairs(group_list) do + if target[group] then + local sorted_table = {} + for _, key in pairs(target[group]) do + table.insert(sorted_table, key) + end + table.sort( + sorted_table, + function(a,b) return (a.mod or '')..a.key<(b.mod or '')..b.key end + ) + target[group] = sorted_table + end + end +end + + +local function import_awful_keys() + if cached_awful_keys then + return + end + cached_awful_keys = {} + for _, data in pairs(awful.key.hotkeys) do + add_hotkey(data.key, data, cached_awful_keys) + end + sort_hotkeys(cached_awful_keys) +end + + +local function group_label(group, color) + local textbox = wibox.widget.textbox( + markup.font(widget.title_font, + markup.bg( + color or (widget.group_rules[group] and + widget.group_rules[group].color or get_next_color("group_title") + ), + markup.fg(beautiful.bg_normal or "#000000", " "..group.." ") + ) + ) + ) + local margin = wibox.container.margin() + margin:set_widget(textbox) + margin:set_top(widget.group_margin) + return margin +end + +local function get_screen(s) + return s and capi.screen[s] +end + +local function create_wibox(s, available_groups) + s = get_screen(s) + + local wa = s.workarea + local height = (widget.height < wa.height) and widget.height or + (wa.height - widget.border_width * 2) + local width = (widget.width < wa.width) and widget.width or + (wa.width - widget.border_width * 2) + + -- arrange hotkey groups into columns + local line_height = beautiful.get_font_height(widget.title_font) + local group_label_height = line_height + widget.group_margin + -- -1 for possible pagination: + local max_height_px = height - group_label_height + local column_layouts = {} + for _, group in ipairs(available_groups) do + local keys = cached_awful_keys[group] or widget.additional_hotkeys[group] + local joined_descriptions = "" + for i, key in ipairs(keys) do + joined_descriptions = joined_descriptions .. key.description .. (i~=#keys and "\n" or "") + end + -- +1 for group label: + local items_height = awful.util.linecount(joined_descriptions) * line_height + group_label_height + local current_column + local available_height_px = max_height_px + local add_new_column = true + for i, column in ipairs(column_layouts) do + if ((column.height_px + items_height) < max_height_px) or + (i == #column_layouts and column.height_px < max_height_px / 2) + then + current_column = column + add_new_column = false + available_height_px = max_height_px - current_column.height_px + break + end + end + local overlap_leftovers + if items_height > available_height_px then + local new_keys = {} + overlap_leftovers = {} + -- +1 for group title and +1 for possible hyphen (v): + local available_height_items = (available_height_px - group_label_height*2) / line_height + for i=1,#keys do + table.insert(((i max_label_width then + max_label_width = length + max_label_content = rendered_hotkey + end + joined_labels = joined_labels .. rendered_hotkey .. (i~=#_keys and "\n" or "") + end + current_column.layout:add(wibox.widget.textbox(joined_labels)) + local max_width = compute_textbox_width(wibox.widget.textbox(max_label_content), s) + + widget.group_margin + if not current_column.max_width or max_width > current_column.max_width then + current_column.max_width = max_width + end + -- +1 for group label: + current_column.height_px = (current_column.height_px or 0) + + awful.util.linecount(joined_labels)*line_height + group_label_height + if _add_new_column then + table.insert(column_layouts, current_column) + end + end + + insert_keys(keys, add_new_column) + if overlap_leftovers then + current_column = {layout=wibox.layout.fixed.vertical()} + insert_keys(overlap_leftovers, true) + end + end + + -- arrange columns into pages + local available_width_px = width + local pages = {} + local columns = wibox.layout.fixed.horizontal() + for _, item in ipairs(column_layouts) do + if item.max_width > available_width_px then + columns.widgets[#columns.widgets]['widget']:add( + group_label("PgDn - Next Page", beautiful.fg_normal) + ) + table.insert(pages, columns) + columns = wibox.layout.fixed.horizontal() + available_width_px = width - item.max_width + local old_widgets = item.layout.widgets + item.layout.widgets = {group_label("PgUp - Prev Page", beautiful.fg_normal)} + awful.util.table.merge(item.layout.widgets, old_widgets) + else + available_width_px = available_width_px - item.max_width + end + local column_margin = wibox.container.margin() + column_margin:set_widget(item.layout) + column_margin:set_left(widget.group_margin) + columns:add(column_margin) + end + table.insert(pages, columns) + + local mywibox = wibox({ + ontop = true, + opacity = beautiful.notification_opacity or 1, + border_width = widget.border_width, + border_color = beautiful.fg_normal, + }) + mywibox:geometry({ + x = wa.x + math.floor((wa.width - width - widget.border_width*2) / 2), + y = wa.y + math.floor((wa.height - height - widget.border_width*2) / 2), + width = width, + height = height, + }) + mywibox:set_widget(pages[1]) + mywibox:buttons(awful.util.table.join( + awful.button({ }, 1, function () mywibox.visible=false end), + awful.button({ }, 3, function () mywibox.visible=false end) + )) + + local widget_obj = {} + widget_obj.current_page = 1 + widget_obj.wibox = mywibox + function widget_obj:page_next() + if self.current_page == #pages then return end + self.current_page = self.current_page + 1 + self.wibox:set_widget(pages[self.current_page]) + end + function widget_obj:page_prev() + if self.current_page == 1 then return end + self.current_page = self.current_page - 1 + self.wibox:set_widget(pages[self.current_page]) + end + function widget_obj:show() + self.wibox.visible = true + end + function widget_obj:hide() + self.wibox.visible = false + end + + return widget_obj +end + + +--- Show popup with hotkeys help. +-- @tparam[opt] client c Client. +-- @tparam[opt] screen s Screen. +function widget.show_help(c, s) + import_awful_keys() + c = c or capi.client.focus + s = s or (c and c.screen or awful.screen.focused()) + + local available_groups = {} + for group, _ in pairs(group_list) do + local need_match + for group_name, data in pairs(widget.group_rules) do + if group_name==group and ( + data.rule or data.rule_any or data.except or data.except_any + ) then + if not c or not awful.rules.matches(c, { + rule=data.rule, + rule_any=data.rule_any, + except=data.except, + except_any=data.except_any + }) then + need_match = true + break + end + end + end + if not need_match then table.insert(available_groups, group) end + end + + local joined_groups = join_plus_sort(available_groups) + if not cached_wiboxes[s] then + cached_wiboxes[s] = {} + end + if not cached_wiboxes[s][joined_groups] then + cached_wiboxes[s][joined_groups] = create_wibox(s, available_groups) + end + local help_wibox = cached_wiboxes[s][joined_groups] + help_wibox:show() + + return capi.keygrabber.run(function(_, key, event) + if event == "release" then return end + if key then + if key == "Next" then + help_wibox:page_next() + elseif key == "Prior" then + help_wibox:page_prev() + else + capi.keygrabber.stop() + help_wibox:hide() + end + end + end) +end + + +--- Add hotkey descriptions for third-party applications. +-- @tparam table hotkeys Table with bindings, +-- see `awful.hotkeys_popup.key.vim` as an example. +function widget.add_hotkeys(hotkeys) + for group, bindings in pairs(hotkeys) do + for _, binding in ipairs(bindings) do + local modifiers = binding.modifiers + local keys = binding.keys + for key, description in pairs(keys) do + add_hotkey(key, { + mod=modifiers, + description=description, + group=group}, + widget.additional_hotkeys + ) + end + end + end + sort_hotkeys(widget.additional_hotkeys) +end + + +return widget +end + +local function get_default_widget() + if not widget_module.default_widget then + widget_module.default_widget = widget_module.new() + end + return widget_module.default_widget +end + +--- Show popup with hotkeys help (default widget instance will be used). +-- @tparam[opt] client c Client. +-- @tparam[opt] screen s Screen. +function widget_module.show_help(...) + return get_default_widget().show_help(...) +end + +--- Add hotkey descriptions for third-party applications +-- (default widget instance will be used). +-- @tparam table hotkeys Table with bindings, +-- see `awful.hotkeys_popup.key.vim` as an example. +function widget_module.add_hotkeys(...) + return get_default_widget().add_hotkeys(...) +end + +return widget_module + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/init.lua b/lib/awful/init.lua new file mode 100644 index 0000000..840897e --- /dev/null +++ b/lib/awful/init.lua @@ -0,0 +1,64 @@ +--------------------------------------------------------------------------- +--- AWesome Functions very UsefuL +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module awful +--------------------------------------------------------------------------- + +-- TODO: This is a hack for backwards-compatibility with 3.5, remove! +local util = require("awful.util") +local gtimer = require("gears.timer") +function timer(...) -- luacheck: ignore + util.deprecate("gears.timer") + return gtimer(...) +end + +--TODO: This is a hack for backwards-compatibility with 3.5, remove! +-- Set awful.util.spawn* and awful.util.pread. +local spawn = require("awful.spawn") + +util.spawn = function(...) + util.deprecate("awful.spawn") + return spawn.spawn(...) +end + +util.spawn_with_shell = function(...) + util.deprecate("awful.spawn.with_shell") + return spawn.with_shell(...) +end + +util.pread = function() + util.deprecate("Use io.popen() directly or look at awful.spawn.easy_async() " + .. "for an asynchronous alternative") + return "" +end + +return +{ + client = require("awful.client"); + completion = require("awful.completion"); + layout = require("awful.layout"); + placement = require("awful.placement"); + prompt = require("awful.prompt"); + screen = require("awful.screen"); + tag = require("awful.tag"); + util = require("awful.util"); + widget = require("awful.widget"); + keygrabber = require("awful.keygrabber"); + menu = require("awful.menu"); + mouse = require("awful.mouse"); + remote = require("awful.remote"); + key = require("awful.key"); + button = require("awful.button"); + wibar = require("awful.wibar"); + wibox = require("awful.wibox"); + startup_notification = require("awful.startup_notification"); + tooltip = require("awful.tooltip"); + ewmh = require("awful.ewmh"); + titlebar = require("awful.titlebar"); + rules = require("awful.rules"); + spawn = spawn; +} + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/key.lua b/lib/awful/key.lua new file mode 100644 index 0000000..c3ce359 --- /dev/null +++ b/lib/awful/key.lua @@ -0,0 +1,136 @@ +--------------------------------------------------------------------------- +--- Create easily new key objects ignoring certain modifiers. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @module awful.key +--------------------------------------------------------------------------- + +-- Grab environment we need +local setmetatable = setmetatable +local ipairs = ipairs +local capi = { key = key, root = root } +local util = require("awful.util") + + + +local key = { mt = {}, hotkeys = {} } + + +--- Modifiers to ignore. +-- By default this is initialized as { "Lock", "Mod2" } +-- so the Caps Lock or Num Lock modifier are not taking into account by awesome +-- when pressing keys. +-- @name awful.key.ignore_modifiers +-- @class table +key.ignore_modifiers = { "Lock", "Mod2" } + +--- Convert the modifiers into pc105 key names +local conversion = { + mod4 = "Super_L", + control = "Control_L", + shift = "Shift_L", + mod1 = "Alt_L", +} + +--- Execute a key combination. +-- If an awesome keybinding is assigned to the combination, it should be +-- executed. +-- @see root.fake_input +-- @tparam table mod A modified table. Valid modifiers are: Any, Mod1, +-- Mod2, Mod3, Mod4, Mod5, Shift, Lock and Control. +-- @tparam string k The key +function key.execute(mod, k) + for _, v in ipairs(mod) do + local m = conversion[v:lower()] + if m then + root.fake_input("key_press", m) + end + end + + root.fake_input("key_press" , k) + root.fake_input("key_release", k) + + for _, v in ipairs(mod) do + local m = conversion[v:lower()] + if m then + root.fake_input("key_release", m) + end + end +end + +--- Create a new key to use as binding. +-- This function is useful to create several keys from one, because it will use +-- the ignore_modifier variable to create several keys with and without the +-- ignored modifiers activated. +-- For example if you want to ignore CapsLock in your keybinding (which is +-- ignored by default by this function), creating a key binding with this +-- function will return 2 key objects: one with CapsLock on, and another one +-- with CapsLock off. +-- @see key.key +-- @tparam table mod A list of modifier keys. Valid modifiers are: Any, Mod1, +-- Mod2, Mod3, Mod4, Mod5, Shift, Lock and Control. +-- @tparam string _key The key to trigger an event. +-- @tparam function press Callback for when the key is pressed. +-- @tparam[opt] function release Callback for when the key is released. +-- @tparam table data User data for key, +-- for example {description="select next tag", group="tag"}. +-- @treturn table A table with one or several key objects. +function key.new(mod, _key, press, release, data) + if type(release)=='table' then + data=release + release=nil + end + local ret = {} + local subsets = util.subsets(key.ignore_modifiers) + for _, set in ipairs(subsets) do + ret[#ret + 1] = capi.key({ modifiers = util.table.join(mod, set), + key = _key }) + if press then + ret[#ret]:connect_signal("press", function(_, ...) press(...) end) + end + if release then + ret[#ret]:connect_signal("release", function(_, ...) release(...) end) + end + end + + -- append custom userdata (like description) to a hotkey + data = data or {} + data.mod = mod + data.key = _key + table.insert(key.hotkeys, data) + data.execute = function(_) key.execute(mod, _key) end + + return ret +end + +--- Compare a key object with modifiers and key. +-- @param _key The key object. +-- @param pressed_mod The modifiers to compare with. +-- @param pressed_key The key to compare with. +function key.match(_key, pressed_mod, pressed_key) + -- First, compare key. + if pressed_key ~= _key.key then return false end + -- Then, compare mod + local mod = _key.modifiers + -- For each modifier of the key object, check that the modifier has been + -- pressed. + for _, m in ipairs(mod) do + -- Has it been pressed? + if not util.table.hasitem(pressed_mod, m) then + -- No, so this is failure! + return false + end + end + -- If the number of pressed modifier is ~=, it is probably >, so this is not + -- the same, return false. + return #pressed_mod == #mod +end + +function key.mt:__call(...) + return key.new(...) +end + +return setmetatable(key, key.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/keygrabber.lua b/lib/awful/keygrabber.lua new file mode 100644 index 0000000..4351f5b --- /dev/null +++ b/lib/awful/keygrabber.lua @@ -0,0 +1,96 @@ +--------------------------------------------------------------------------- +--- Keygrabber Stack +-- +-- @author dodo +-- @copyright 2012 dodo +-- @module awful.keygrabber +--------------------------------------------------------------------------- + +local ipairs = ipairs +local table = table +local capi = { + keygrabber = keygrabber } + +local keygrabber = {} + +-- Private data +local grabbers = {} +local keygrabbing = false + + +local function grabber(mod, key, event) + for _, keygrabber_function in ipairs(grabbers) do + -- continue if the grabber explicitly returns false + if keygrabber_function(mod, key, event) ~= false then + break + end + end +end + +--- Stop grabbing the keyboard for the provided callback. +-- When no callback is given, the last grabber gets removed (last one added to +-- the stack). +-- @param g The key grabber that must be removed. +function keygrabber.stop(g) + for i, v in ipairs(grabbers) do + if v == g then + table.remove(grabbers, i) + break + end + end + -- Stop the global key grabber if the last grabber disappears from stack. + if #grabbers == 0 then + keygrabbing = false + capi.keygrabber.stop() + end +end + +--- +-- Grab keyboard input and read pressed keys, calling the least callback +-- function from the stack at each keypress, until the stack is empty. +-- +-- Calling run with the same callback again will bring the callback +-- to the top of the stack. +-- +-- The callback function receives three arguments: +-- +-- * a table containing modifiers keys +-- * a string with the pressed key +-- * a string with either "press" or "release" to indicate the event type +-- +-- A callback can return `false` to pass the events to the next +-- keygrabber in the stack. +-- @param g The key grabber callback that will get the key events until it will be deleted or a new grabber is added. +-- @return the given callback `g`. +-- @usage +-- -- The following function can be bound to a key, and be used to resize a +-- -- client using the keyboard. +-- +-- function resize(c) +-- local grabber = awful.keygrabber.run(function(mod, key, event) +-- if event == "release" then return end +-- +-- if key == 'Up' then awful.client.moveresize(0, 0, 0, 5, c) +-- elseif key == 'Down' then awful.client.moveresize(0, 0, 0, -5, c) +-- elseif key == 'Right' then awful.client.moveresize(0, 0, 5, 0, c) +-- elseif key == 'Left' then awful.client.moveresize(0, 0, -5, 0, c) +-- else awful.keygrabber.stop(grabber) +-- end +-- end) +-- end +function keygrabber.run(g) + -- Remove the grabber if it is in the stack. + keygrabber.stop(g) + -- Record the grabber that has been added most recently. + table.insert(grabbers, 1, g) + -- Start the keygrabber if it is not running already. + if not keygrabbing then + keygrabbing = true + capi.keygrabber.run(grabber) + end + return g +end + +return keygrabber + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/init.lua b/lib/awful/layout/init.lua new file mode 100644 index 0000000..dfce562 --- /dev/null +++ b/lib/awful/layout/init.lua @@ -0,0 +1,323 @@ +--------------------------------------------------------------------------- +--- Layout module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module awful.layout +--------------------------------------------------------------------------- + +-- Grab environment we need +local ipairs = ipairs +local type = type +local util = require("awful.util") +local capi = { + screen = screen, + mouse = mouse, + awesome = awesome, + client = client, + tag = tag +} +local tag = require("awful.tag") +local client = require("awful.client") +local ascreen = require("awful.screen") +local timer = require("gears.timer") + +local function get_screen(s) + return s and capi.screen[s] +end + +local layout = {} + +layout.suit = require("awful.layout.suit") + +layout.layouts = { + layout.suit.floating, + layout.suit.tile, + layout.suit.tile.left, + layout.suit.tile.bottom, + layout.suit.tile.top, + layout.suit.fair, + layout.suit.fair.horizontal, + layout.suit.spiral, + layout.suit.spiral.dwindle, + layout.suit.max, + layout.suit.max.fullscreen, + layout.suit.magnifier, + layout.suit.corner.nw, + layout.suit.corner.ne, + layout.suit.corner.sw, + layout.suit.corner.se, +} + +--- The default list of layouts. +-- +-- The default value is: +-- +-- awful.layout.suit.floating, +-- awful.layout.suit.tile, +-- awful.layout.suit.tile.left, +-- awful.layout.suit.tile.bottom, +-- awful.layout.suit.tile.top, +-- awful.layout.suit.fair, +-- awful.layout.suit.fair.horizontal, +-- awful.layout.suit.spiral, +-- awful.layout.suit.spiral.dwindle, +-- awful.layout.suit.max, +-- awful.layout.suit.max.fullscreen, +-- awful.layout.suit.magnifier, +-- awful.layout.suit.corner.nw, +-- awful.layout.suit.corner.ne, +-- awful.layout.suit.corner.sw, +-- awful.layout.suit.corner.se, +-- +-- @field layout.layouts + + +-- This is a special lock used by the arrange function. +-- This avoids recurring call by emitted signals. +local arrange_lock = false +-- Delay one arrange call per screen. +local delayed_arrange = {} + +--- Get the current layout. +-- @param screen The screen. +-- @return The layout function. +function layout.get(screen) + screen = screen or capi.mouse.screen + local t = get_screen(screen).selected_tag + return tag.getproperty(t, "layout") or layout.suit.floating +end + +--- Change the layout of the current tag. +-- @param i Relative index. +-- @param s The screen. +-- @param[opt] layouts A table of layouts. +function layout.inc(i, s, layouts) + if type(i) == "table" then + -- Older versions of this function had arguments (layouts, i, s), but + -- this was changed so that 'layouts' can be an optional parameter + layouts, i, s = i, s, layouts + end + s = get_screen(s or ascreen.focused()) + local t = s.selected_tag + layouts = layouts or layout.layouts + if t then + local curlayout = layout.get(s) + local curindex + for k, v in ipairs(layouts) do + if v == curlayout or curlayout._type == v then + curindex = k + break + end + end + if not curindex then + -- Safety net: handle cases where another reference of the layout + -- might be given (e.g. when (accidentally) cloning it). + for k, v in ipairs(layouts) do + if v.name == curlayout.name then + curindex = k + break + end + end + end + if curindex then + local newindex = util.cycle(#layouts, curindex + i) + layout.set(layouts[newindex], t) + end + end +end + +--- Set the layout function of the current tag. +-- @param _layout Layout name. +-- @tparam[opt=mouse.screen.selected_tag] tag t The tag to modify. +function layout.set(_layout, t) + t = t or capi.mouse.screen.selected_tag + t.layout = _layout +end + +--- Get the layout parameters used for the screen +-- +-- This should give the same result as "arrange", but without the "geometries" +-- parameter, as this is computed during arranging. +-- +-- If `t` is given, `screen` is ignored, if none are given, the mouse screen is +-- used. +-- +-- @tparam[opt] tag t The tag to query +-- @param[opt] screen The screen +-- @treturn table A table with the workarea (x, y, width, height), the screen +-- geometry (x, y, width, height), the clients, the screen and sometime, a +-- "geometries" table with client as keys and geometry as value +function layout.parameters(t, screen) + screen = get_screen(screen) + t = t or screen.selected_tag + + screen = get_screen(t and t.screen or 1) + + local p = {} + + local clients = client.tiled(screen) + local gap_single_client = true + + if(t and t.gap_single_client ~= nil) then + gap_single_client = t.gap_single_client + end + + local min_clients = gap_single_client and 1 or 2 + local useless_gap = t and (#clients >= min_clients and t.gap or 0) or 0 + + p.workarea = screen:get_bounding_geometry { + honor_padding = true, + honor_workarea = true, + margins = useless_gap, + } + + p.geometry = screen.geometry + p.clients = clients + p.screen = screen.index + p.padding = screen.padding + p.useless_gap = useless_gap + + return p +end + +--- Arrange a screen using its current layout. +-- @param screen The screen to arrange. +function layout.arrange(screen) + screen = get_screen(screen) + if not screen or delayed_arrange[screen] then return end + delayed_arrange[screen] = true + + timer.delayed_call(function() + if not screen.valid then + -- Screen was removed + delayed_arrange[screen] = nil + return + end + if arrange_lock then return end + arrange_lock = true + + local p = layout.parameters(nil, screen) + + local useless_gap = p.useless_gap + + p.geometries = setmetatable({}, {__mode = "k"}) + layout.get(screen).arrange(p) + for c, g in pairs(p.geometries) do + g.width = math.max(1, g.width - c.border_width * 2 - useless_gap * 2) + g.height = math.max(1, g.height - c.border_width * 2 - useless_gap * 2) + g.x = g.x + useless_gap + g.y = g.y + useless_gap + c:geometry(g) + end + arrange_lock = false + delayed_arrange[screen] = nil + + screen:emit_signal("arrange") + end) +end + +--- Get the current layout name. +-- @param _layout The layout. +-- @return The layout name. +function layout.getname(_layout) + _layout = _layout or layout.get() + return _layout.name +end + +local function arrange_prop_nf(obj) + if not client.object.get_floating(obj) then + layout.arrange(obj.screen) + end +end + +local function arrange_prop(obj) layout.arrange(obj.screen) end + +capi.client.connect_signal("property::size_hints_honor", arrange_prop_nf) +capi.client.connect_signal("property::struts", arrange_prop) +capi.client.connect_signal("property::minimized", arrange_prop_nf) +capi.client.connect_signal("property::sticky", arrange_prop_nf) +capi.client.connect_signal("property::fullscreen", arrange_prop_nf) +capi.client.connect_signal("property::maximized_horizontal", arrange_prop_nf) +capi.client.connect_signal("property::maximized_vertical", arrange_prop_nf) +capi.client.connect_signal("property::border_width", arrange_prop_nf) +capi.client.connect_signal("property::hidden", arrange_prop_nf) +capi.client.connect_signal("property::floating", arrange_prop) +capi.client.connect_signal("property::geometry", arrange_prop_nf) +capi.client.connect_signal("property::screen", function(c, old_screen) + if old_screen then + layout.arrange(old_screen) + end + layout.arrange(c.screen) +end) + +local function arrange_tag(t) + layout.arrange(t.screen) +end + +capi.tag.connect_signal("property::master_width_factor", arrange_tag) +capi.tag.connect_signal("property::master_count", arrange_tag) +capi.tag.connect_signal("property::column_count", arrange_tag) +capi.tag.connect_signal("property::layout", arrange_tag) +capi.tag.connect_signal("property::windowfact", arrange_tag) +capi.tag.connect_signal("property::selected", arrange_tag) +capi.tag.connect_signal("property::activated", arrange_tag) +capi.tag.connect_signal("property::useless_gap", arrange_tag) +capi.tag.connect_signal("property::master_fill_policy", arrange_tag) +capi.tag.connect_signal("tagged", arrange_tag) + +capi.screen.connect_signal("property::workarea", layout.arrange) +capi.screen.connect_signal("padding", layout.arrange) + +capi.client.connect_signal("raised", function(c) layout.arrange(c.screen) end) +capi.client.connect_signal("lowered", function(c) layout.arrange(c.screen) end) +capi.client.connect_signal("list", function() + for screen in capi.screen do + layout.arrange(screen) + end + end) + +--- Default handler for `request::geometry` signals for tiled clients with +-- the "mouse.move" context. +-- @tparam client c The client +-- @tparam string context The context +-- @tparam table hints Additional hints +function layout.move_handler(c, context, hints) --luacheck: no unused args + -- Quit if it isn't a mouse.move on a tiled layout, that's handled elsewhere + if c.floating then return end + if context ~= "mouse.move" then return end + + if capi.mouse.screen ~= c.screen then + c.screen = capi.mouse.screen + end + + local l = c.screen.selected_tag and c.screen.selected_tag.layout or nil + if l == layout.suit.floating then return end + + local c_u_m = capi.mouse.current_client + if c_u_m and not c_u_m.floating then + if c_u_m ~= c then + c:swap(c_u_m) + end + end +end + +capi.client.connect_signal("request::geometry", layout.move_handler) + +-- When a screen is moved, make (floating) clients follow it +capi.screen.connect_signal("property::geometry", function(s, old_geom) + local geom = s.geometry + local xshift = geom.x - old_geom.x + local yshift = geom.y - old_geom.y + for _, c in ipairs(capi.client.get(s)) do + local cgeom = c:geometry() + c:geometry({ + x = cgeom.x + xshift, + y = cgeom.y + yshift + }) + end +end) + +return layout + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/suit/corner.lua b/lib/awful/layout/suit/corner.lua new file mode 100644 index 0000000..4b746c9 --- /dev/null +++ b/lib/awful/layout/suit/corner.lua @@ -0,0 +1,204 @@ +--------------------------------------------------------------------------- +-- Corner layout. +-- Display master client in a corner of the screen, and slaves in one +-- column and one row around the master. +-- See Pull Request for example : https://github.com/awesomeWM/awesome/pull/251 +-- @module awful.layout +-- @author Alexis Brenon <brenon.alexis+awesomewm@gmail.com> +-- @copyright 2015 Alexis Brenon + +-- Grab environment we need +local ipairs = ipairs +local math = math +local capi = {screen = screen} + +--- The cornernw layout layoutbox icon. +-- @beautiful beautiful.layout_cornernw +-- @param surface +-- @see gears.surface + +--- The cornerne layout layoutbox icon. +-- @beautiful beautiful.layout_cornerne +-- @param surface +-- @see gears.surface + +--- The cornersw layout layoutbox icon. +-- @beautiful beautiful.layout_cornersw +-- @param surface +-- @see gears.surface + +--- The cornerse layout layoutbox icon. +-- @beautiful beautiful.layout_cornerse +-- @param surface +-- @see gears.surface + +-- Actually arrange clients of p.clients for corner layout +-- @param p Mandatory table containing required informations for layouts +-- (clients to arrange, workarea geometry, etc.) +-- @param orientation String indicating in which corner is the master window. +-- Available values are : NE, NW, SW, SE +local function do_corner(p, orientation) + local t = p.tag or capi.screen[p.screen].selected_tag + local wa = p.workarea + local cls = p.clients + + if #cls == 0 then return end + + local master = {} + local column = {} + local row = {} + -- Use the nmaster field of the tag in a cheaty way + local row_privileged = ((cls[1].screen.selected_tag.master_count % 2) == 0) + + local master_factor = cls[1].screen.selected_tag.master_width_factor + master.width = master_factor * wa.width + master.height = master_factor * wa.height + + local number_privileged_win = math.ceil((#cls - 1)/2) + local number_unprivileged_win = (#cls - 1) - number_privileged_win + + -- Define some obvious parameters + column.width = wa.width - master.width + column.x_increment = 0 + row.height = wa.height - master.height + row.y_increment = 0 + + -- Place master at the right place and move row and column accordingly + column.y = wa.y + row.x = wa.x + if orientation:match('N.') then + master.y = wa.y + row.y = master.y + master.height + elseif orientation:match('S.') then + master.y = wa.y + wa.height - master.height + row.y = wa.y + end + if orientation:match('.W') then + master.x = wa.x + column.x = master.x + master.width + elseif orientation:match('.E') then + master.x = wa.x + wa.width - master.width + column.x = wa.x + end + -- At this point, master is in a corner + -- but row and column are overlayed in the opposite corner... + + -- Reduce the unprivileged slaves to remove overlay + -- and define actual width and height + if row_privileged then + row.width = wa.width + row.number_win = number_privileged_win + column.y = master.y + column.height = master.height + column.number_win = number_unprivileged_win + else + column.height = wa.height + column.number_win = number_privileged_win + row.x = master.x + row.width = master.width + row.number_win = number_unprivileged_win + end + + column.win_height = column.height/column.number_win + column.win_width = column.width + column.y_increment = column.win_height + column.win_idx = 0 + + row.win_width = row.width/row.number_win + row.win_height = row.height + row.x_increment = row.win_width + row.win_idx = 0 + + -- Extend master if there is only a few windows and "expand" policy is set + if #cls < 3 then + if row_privileged then + master.x = wa.x + master.width = wa.width + else + master.y = wa.y + master.height = wa.height + end + if #cls < 2 then + if t.master_fill_policy == "expand" then + master = wa + else + master.x = master.x + (wa.width - master.width)/2 + master.y = master.y + (wa.height - master.height)/2 + end + end + end + + for i, c in ipairs(cls) do + local g + -- Handle master window + if i == 1 then + g = { + x = master.x, + y = master.y, + width = master.width, + height = master.height + } + -- handle column windows + elseif i % 2 == 0 then + g = { + x = column.x + column.win_idx * column.x_increment, + y = column.y + column.win_idx * column.y_increment, + width = column.win_width, + height = column.win_height + } + column.win_idx = column.win_idx + 1 + else + g = { + x = row.x + row.win_idx * row.x_increment, + y = row.y + row.win_idx * row.y_increment, + width = row.win_width, + height = row.win_height + } + row.win_idx = row.win_idx + 1 + end + p.geometries[c] = g + end +end + +local corner = {} +corner.row_privileged = false + +--- Corner layout. +-- Display master client in a corner of the screen, and slaves in one +-- column and one row around the master. +-- @clientlayout awful.layout.suit.corner.nw +corner.nw = { + name = "cornernw", + arrange = function (p) return do_corner(p, "NW") end + } + +--- Corner layout. +-- Display master client in a corner of the screen, and slaves in one +-- column and one row around the master. +-- @clientlayout awful.layout.suit.corner.ne +corner.ne = { + name = "cornerne", + arrange = function (p) return do_corner(p, "NE") end + } + +--- Corner layout. +-- Display master client in a corner of the screen, and slaves in one +-- column and one row around the master. +-- @clientlayout awful.layout.suit.corner.sw +corner.sw = { + name = "cornersw", + arrange = function (p) return do_corner(p, "SW") end + } + +--- Corner layout. +-- Display master client in a corner of the screen, and slaves in one +-- column and one row around the master. +-- @clientlayout awful.layout.suit.corner.se +corner.se = { + name = "cornerse", + arrange = function (p) return do_corner(p, "SE") end + } + +return corner + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/suit/fair.lua b/lib/awful/layout/suit/fair.lua new file mode 100644 index 0000000..161b6ed --- /dev/null +++ b/lib/awful/layout/suit/fair.lua @@ -0,0 +1,108 @@ +--------------------------------------------------------------------------- +--- Fair layouts module for awful. +-- +-- @author Josh Komoroske +-- @copyright 2012 Josh Komoroske +-- @module awful.layout +--------------------------------------------------------------------------- + +-- Grab environment we need +local ipairs = ipairs +local math = math + +--- The fairh layout layoutbox icon. +-- @beautiful beautiful.layout_fairh +-- @param surface +-- @see gears.surface + +--- The fairv layout layoutbox icon. +-- @beautiful beautiful.layout_fairv +-- @param surface +-- @see gears.surface + +local fair = {} + +local function do_fair(p, orientation) + local wa = p.workarea + local cls = p.clients + + -- Swap workarea dimensions, if our orientation is "east" + if orientation == 'east' then + wa.width, wa.height = wa.height, wa.width + wa.x, wa.y = wa.y, wa.x + end + + if #cls > 0 then + local rows, cols + if #cls == 2 then + rows, cols = 1, 2 + else + rows = math.ceil(math.sqrt(#cls)) + cols = math.ceil(#cls / rows) + end + + for k, c in ipairs(cls) do + k = k - 1 + local g = {} + + local row, col + row = k % rows + col = math.floor(k / rows) + + local lrows, lcols + if k >= rows * cols - rows then + lrows = #cls - (rows * cols - rows) + lcols = cols + else + lrows = rows + lcols = cols + end + + if row == lrows - 1 then + g.height = wa.height - math.ceil(wa.height / lrows) * row + g.y = wa.height - g.height + else + g.height = math.ceil(wa.height / lrows) + g.y = g.height * row + end + + if col == lcols - 1 then + g.width = wa.width - math.ceil(wa.width / lcols) * col + g.x = wa.width - g.width + else + g.width = math.ceil(wa.width / lcols) + g.x = g.width * col + end + + g.y = g.y + wa.y + g.x = g.x + wa.x + + -- Swap window dimensions, if our orientation is "east" + if orientation == 'east' then + g.width, g.height = g.height, g.width + g.x, g.y = g.y, g.x + end + + p.geometries[c] = g + end + end +end + +--- Horizontal fair layout. +-- @param screen The screen to arrange. +fair.horizontal = {} +fair.horizontal.name = "fairh" +function fair.horizontal.arrange(p) + return do_fair(p, "east") +end + +--- Vertical fair layout. +-- @param screen The screen to arrange. +fair.name = "fairv" +function fair.arrange(p) + return do_fair(p, "south") +end + +return fair + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/suit/floating.lua b/lib/awful/layout/suit/floating.lua new file mode 100644 index 0000000..b769435 --- /dev/null +++ b/lib/awful/layout/suit/floating.lua @@ -0,0 +1,112 @@ +--------------------------------------------------------------------------- +--- Dummy function for floating layout +-- +-- @author Gregor Best +-- @copyright 2008 Gregor Best +-- @module awful.layout +--------------------------------------------------------------------------- + +-- Grab environment we need +local ipairs = ipairs +local capi = +{ + mouse = mouse, + mousegrabber = mousegrabber +} + +--- The floating layout layoutbox icon. +-- @beautiful beautiful.layout_floating +-- @param surface +-- @see gears.surface + +local floating = {} + +--- Jump mouse cursor to the client's corner when resizing it. +floating.resize_jump_to_corner = true + +function floating.mouse_resize_handler(c, corner, x, y) + local g = c:geometry() + + -- Do not allow maximized clients to be resized by mouse + local fixed_x = c.maximized_horizontal + local fixed_y = c.maximized_vertical + + local prev_coords = {} + local coordinates_delta = {x=0,y=0} + if floating.resize_jump_to_corner then + -- Warp mouse pointer + capi.mouse.coords({ x = x, y = y }) + else + local corner_x, corner_y = x, y + local mouse_coords = capi.mouse.coords() + x = mouse_coords.x + y = mouse_coords.y + coordinates_delta = {x=corner_x-x,y=corner_y-y} + end + + capi.mousegrabber.run(function (_mouse) + if not c.valid then return false end + + _mouse.x = _mouse.x + coordinates_delta.x + _mouse.y = _mouse.y + coordinates_delta.y + for _, v in ipairs(_mouse.buttons) do + if v then + local ng + prev_coords = { x =_mouse.x, y = _mouse.y } + if corner == "bottom_right" then + ng = { width = _mouse.x - g.x, + height = _mouse.y - g.y } + elseif corner == "bottom_left" then + ng = { x = _mouse.x, + width = (g.x + g.width) - _mouse.x, + height = _mouse.y - g.y } + elseif corner == "top_left" then + ng = { x = _mouse.x, + width = (g.x + g.width) - _mouse.x, + y = _mouse.y, + height = (g.y + g.height) - _mouse.y } + else + ng = { width = _mouse.x - g.x, + y = _mouse.y, + height = (g.y + g.height) - _mouse.y } + end + if ng.width <= 0 then ng.width = nil end + if ng.height <= 0 then ng.height = nil end + if fixed_x then ng.width = g.width ng.x = g.x end + if fixed_y then ng.height = g.height ng.y = g.y end + c:geometry(ng) + -- Get real geometry that has been applied + -- in case we honor size hints + -- XXX: This should be rewritten when size + -- hints are available from Lua. + local rg = c:geometry() + + if corner == "bottom_right" then + ng = {} + elseif corner == "bottom_left" then + ng = { x = (g.x + g.width) - rg.width } + elseif corner == "top_left" then + ng = { x = (g.x + g.width) - rg.width, + y = (g.y + g.height) - rg.height } + else + ng = { y = (g.y + g.height) - rg.height } + end + c:geometry({ x = ng.x, y = ng.y }) + return true + end + end + return prev_coords.x == _mouse.x and prev_coords.y == _mouse.y + end, corner .. "_corner") +end + +function floating.arrange() +end + +--- The floating layout. +-- @clientlayout awful.layout.suit. + +floating.name = "floating" + +return floating + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/suit/init.lua b/lib/awful/layout/suit/init.lua new file mode 100644 index 0000000..57a49fa --- /dev/null +++ b/lib/awful/layout/suit/init.lua @@ -0,0 +1,19 @@ +--------------------------------------------------------------------------- +--- Suits for awful +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module awful.layout +--------------------------------------------------------------------------- + +return +{ + corner = require("awful.layout.suit.corner"); + max = require("awful.layout.suit.max"); + tile = require("awful.layout.suit.tile"); + fair = require("awful.layout.suit.fair"); + floating = require("awful.layout.suit.floating"); + magnifier = require("awful.layout.suit.magnifier"); + spiral = require("awful.layout.suit.spiral"); +} + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/suit/magnifier.lua b/lib/awful/layout/suit/magnifier.lua new file mode 100644 index 0000000..f30d7ee --- /dev/null +++ b/lib/awful/layout/suit/magnifier.lua @@ -0,0 +1,147 @@ +--------------------------------------------------------------------------- +--- Magnifier layout module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module awful.layout +--------------------------------------------------------------------------- + +-- Grab environment we need +local ipairs = ipairs +local math = math +local capi = +{ + client = client, + screen = screen, + mouse = mouse, + mousegrabber = mousegrabber +} + +--- The magnifier layout layoutbox icon. +-- @beautiful beautiful.layout_magnifier +-- @param surface +-- @see gears.surface + +local magnifier = {} + +function magnifier.mouse_resize_handler(c, corner, x, y) + capi.mouse.coords({ x = x, y = y }) + + local wa = c.screen.workarea + local center_x = wa.x + wa.width / 2 + local center_y = wa.y + wa.height / 2 + local maxdist_pow = (wa.width^2 + wa.height^2) / 4 + + local prev_coords = {} + capi.mousegrabber.run(function (_mouse) + if not c.valid then return false end + + for _, v in ipairs(_mouse.buttons) do + if v then + prev_coords = { x =_mouse.x, y = _mouse.y } + local dx = center_x - _mouse.x + local dy = center_y - _mouse.y + local dist = dx^2 + dy^2 + + -- New master width factor + local mwfact = dist / maxdist_pow + c.screen.selected_tag.master_width_factor + = math.min(math.max(0.01, mwfact), 0.99) + return true + end + end + return prev_coords.x == _mouse.x and prev_coords.y == _mouse.y + end, corner .. "_corner") +end + +function magnifier.arrange(p) + -- Fullscreen? + local area = p.workarea + local cls = p.clients + local focus = p.focus or capi.client.focus + local t = p.tag or capi.screen[p.screen].selected_tag + local mwfact = t.master_width_factor + local fidx + + -- Check that the focused window is on the right screen + if focus and focus.screen ~= p.screen then focus = nil end + + -- If no window is focused or focused window is not tiled, take the first tiled one. + if (not focus or focus.floating) and #cls > 0 then + focus = cls[1] + fidx = 1 + end + + -- Abort if no clients are present + if not focus then return end + + local geometry = {} + if #cls > 1 then + geometry.width = area.width * math.sqrt(mwfact) + geometry.height = area.height * math.sqrt(mwfact) + geometry.x = area.x + (area.width - geometry.width) / 2 + geometry.y = area.y + (area.height - geometry.height) /2 + else + geometry.x = area.x + geometry.y = area.y + geometry.width = area.width + geometry.height = area.height + end + + local g = { + x = geometry.x, + y = geometry.y, + width = geometry.width, + height = geometry.height + } + p.geometries[focus] = g + + if #cls > 1 then + geometry.x = area.x + geometry.y = area.y + geometry.height = area.height / (#cls - 1) + geometry.width = area.width + + -- We don't know the focus window index. Try to find it. + if not fidx then + for k, c in ipairs(cls) do + if c == focus then + fidx = k + break + end + end + end + + -- First move clients that are before focused client. + for k = fidx + 1, #cls do + p.geometries[cls[k]] = { + x = geometry.x, + y = geometry.y, + width = geometry.width, + height = geometry.height + } + geometry.y = geometry.y + geometry.height + end + + -- Then move clients that are after focused client. + -- So the next focused window will be the one at the top of the screen. + for k = 1, fidx - 1 do + p.geometries[cls[k]] = { + x = geometry.x, + y = geometry.y, + width = geometry.width, + height = geometry.height + } + geometry.y = geometry.y + geometry.height + end + end +end + +--- The magnifier layout. +-- @clientlayout awful.layout.suit.magnifier + +magnifier.name = "magnifier" + +return magnifier + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/suit/max.lua b/lib/awful/layout/suit/max.lua new file mode 100644 index 0000000..2cd1812 --- /dev/null +++ b/lib/awful/layout/suit/max.lua @@ -0,0 +1,61 @@ +--------------------------------------------------------------------------- +--- Maximized and fullscreen layouts module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module awful.layout +--------------------------------------------------------------------------- + +-- Grab environment we need +local pairs = pairs + +local max = {} + +--- The max layout layoutbox icon. +-- @beautiful beautiful.layout_max +-- @param surface +-- @see gears.surface + +--- The fullscreen layout layoutbox icon. +-- @beautiful beautiful.layout_fullscreen +-- @param surface +-- @see gears.surface + +local function fmax(p, fs) + -- Fullscreen? + local area + if fs then + area = p.geometry + else + area = p.workarea + end + + for _, c in pairs(p.clients) do + local g = { + x = area.x, + y = area.y, + width = area.width, + height = area.height + } + p.geometries[c] = g + end +end + +--- Maximized layout. +-- @clientlayout awful.layout.suit.max.name +max.name = "max" +function max.arrange(p) + return fmax(p, false) +end + +--- Fullscreen layout. +-- @clientlayout awful.layout.suit.max.fullscreen +max.fullscreen = {} +max.fullscreen.name = "fullscreen" +function max.fullscreen.arrange(p) + return fmax(p, true) +end + +return max + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/suit/spiral.lua b/lib/awful/layout/suit/spiral.lua new file mode 100644 index 0000000..0a7eb9b --- /dev/null +++ b/lib/awful/layout/suit/spiral.lua @@ -0,0 +1,89 @@ +--------------------------------------------------------------------------- +--- Dwindle and spiral layouts +-- +-- @author Uli Schlachter <psychon@znc.in> +-- @copyright 2009 Uli Schlachter +-- @copyright 2008 Julien Danjou +-- +-- @module awful.layout +--------------------------------------------------------------------------- + +-- Grab environment we need +local ipairs = ipairs +local math = math + +--- The spiral layout layoutbox icon. +-- @beautiful beautiful.layout_spiral +-- @param surface +-- @see gears.surface + +--- The dwindle layout layoutbox icon. +-- @beautiful beautiful.layout_dwindle +-- @param surface +-- @see gears.surface + +local spiral = {} + +local function do_spiral(p, _spiral) + local wa = p.workarea + local cls = p.clients + local n = #cls + local old_width, old_height = wa.width, 2 * wa.height + + for k, c in ipairs(cls) do + if k % 2 == 0 then + wa.width, old_width = math.ceil(old_width / 2), wa.width + if k ~= n then + wa.height, old_height = math.floor(wa.height / 2), wa.height + end + else + wa.height, old_height = math.ceil(old_height / 2), wa.height + if k ~= n then + wa.width, old_width = math.floor(wa.width / 2), wa.width + end + end + + if k % 4 == 0 and _spiral then + wa.x = wa.x - wa.width + elseif k % 2 == 0 then + wa.x = wa.x + old_width + elseif k % 4 == 3 and k < n and _spiral then + wa.x = wa.x + math.ceil(old_width / 2) + end + + if k % 4 == 1 and k ~= 1 and _spiral then + wa.y = wa.y - wa.height + elseif k % 2 == 1 and k ~= 1 then + wa.y = wa.y + old_height + elseif k % 4 == 0 and k < n and _spiral then + wa.y = wa.y + math.ceil(old_height / 2) + end + + local g = { + x = wa.x, + y = wa.y, + width = wa.width, + height = wa.height + } + p.geometries[c] = g + end +end + +--- Dwindle layout. +-- @clientlayout awful.layout.suit.spiral.dwindle +spiral.dwindle = {} +spiral.dwindle.name = "dwindle" +function spiral.dwindle.arrange(p) + return do_spiral(p, false) +end + +--- Spiral layout. +-- @clientlayout awful.layout.suit.spiral.name +spiral.name = "spiral" +function spiral.arrange(p) + return do_spiral(p, true) +end + +return spiral + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/layout/suit/tile.lua b/lib/awful/layout/suit/tile.lua new file mode 100644 index 0000000..9fa263c --- /dev/null +++ b/lib/awful/layout/suit/tile.lua @@ -0,0 +1,348 @@ +--------------------------------------------------------------------------- +--- Tiled layouts module for awful +-- +-- @author Donald Ephraim Curtis <dcurtis@cs.uiowa.edu> +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Donald Ephraim Curtis +-- @copyright 2008 Julien Danjou +-- @module awful.layout +--------------------------------------------------------------------------- + +-- Grab environment we need +local tag = require("awful.tag") +local client = require("awful.client") +local ipairs = ipairs +local math = math +local capi = +{ + mouse = mouse, + screen = screen, + mousegrabber = mousegrabber +} + +local tile = {} + +--- The tile layout layoutbox icon. +-- @beautiful beautiful.layout_tile +-- @param surface +-- @see gears.surface + +--- The tile top layout layoutbox icon. +-- @beautiful beautiful.layout_tiletop +-- @param surface +-- @see gears.surface + +--- The tile bottom layout layoutbox icon. +-- @beautiful beautiful.layout_tilebottom +-- @param surface +-- @see gears.surface + +--- The tile left layout layoutbox icon. +-- @beautiful beautiful.layout_tileleft +-- @param surface +-- @see gears.surface + +--- Jump mouse cursor to the client's corner when resizing it. +tile.resize_jump_to_corner = true + +local function mouse_resize_handler(c, _, _, _, orientation) + orientation = orientation or "tile" + local wa = c.screen.workarea + local mwfact = c.screen.selected_tag.master_width_factor + local cursor + local g = c:geometry() + local offset = 0 + local corner_coords + local coordinates_delta = {x=0,y=0} + + if orientation == "tile" then + cursor = "cross" + if g.height+15 > wa.height then + offset = g.height * .5 + cursor = "sb_h_double_arrow" + elseif not (g.y+g.height+15 > wa.y+wa.height) then + offset = g.height + end + corner_coords = { x = wa.x + wa.width * mwfact, y = g.y + offset } + elseif orientation == "left" then + cursor = "cross" + if g.height+15 >= wa.height then + offset = g.height * .5 + cursor = "sb_h_double_arrow" + elseif not (g.y+g.height+15 > wa.y+wa.height) then + offset = g.height + end + corner_coords = { x = wa.x + wa.width * (1 - mwfact), y = g.y + offset } + elseif orientation == "bottom" then + cursor = "cross" + if g.width+15 >= wa.width then + offset = g.width * .5 + cursor = "sb_v_double_arrow" + elseif not (g.x+g.width+15 > wa.x+wa.width) then + offset = g.width + end + corner_coords = { y = wa.y + wa.height * mwfact, x = g.x + offset} + else + cursor = "cross" + if g.width+15 >= wa.width then + offset = g.width * .5 + cursor = "sb_v_double_arrow" + elseif not (g.x+g.width+15 > wa.x+wa.width) then + offset = g.width + end + corner_coords = { y = wa.y + wa.height * (1 - mwfact), x= g.x + offset } + end + if tile.resize_jump_to_corner then + capi.mouse.coords(corner_coords) + else + local mouse_coords = capi.mouse.coords() + coordinates_delta = { + x = corner_coords.x - mouse_coords.x, + y = corner_coords.y - mouse_coords.y, + } + end + + local prev_coords = {} + capi.mousegrabber.run(function (_mouse) + if not c.valid then return false end + + _mouse.x = _mouse.x + coordinates_delta.x + _mouse.y = _mouse.y + coordinates_delta.y + for _, v in ipairs(_mouse.buttons) do + if v then + prev_coords = { x =_mouse.x, y = _mouse.y } + local fact_x = (_mouse.x - wa.x) / wa.width + local fact_y = (_mouse.y - wa.y) / wa.height + local new_mwfact + + local geom = c:geometry() + + -- we have to make sure we're not on the last visible client where we have to use different settings. + local wfact + local wfact_x, wfact_y + if (geom.y+geom.height+15) > (wa.y+wa.height) then + wfact_y = (geom.y + geom.height - _mouse.y) / wa.height + else + wfact_y = (_mouse.y - geom.y) / wa.height + end + + if (geom.x+geom.width+15) > (wa.x+wa.width) then + wfact_x = (geom.x + geom.width - _mouse.x) / wa.width + else + wfact_x = (_mouse.x - geom.x) / wa.width + end + + + if orientation == "tile" then + new_mwfact = fact_x + wfact = wfact_y + elseif orientation == "left" then + new_mwfact = 1 - fact_x + wfact = wfact_y + elseif orientation == "bottom" then + new_mwfact = fact_y + wfact = wfact_x + else + new_mwfact = 1 - fact_y + wfact = wfact_x + end + + c.screen.selected_tag.master_width_factor + = math.min(math.max(new_mwfact, 0.01), 0.99) + client.setwfact(math.min(math.max(wfact,0.01), 0.99), c) + return true + end + end + return prev_coords.x == _mouse.x and prev_coords.y == _mouse.y + end, cursor) +end + +local function tile_group(gs, cls, wa, orientation, fact, group) + -- get our orientation right + local height = "height" + local width = "width" + local x = "x" + local y = "y" + if orientation == "top" or orientation == "bottom" then + height = "width" + width = "height" + x = "y" + y = "x" + end + + -- make this more generic (not just width) + local available = wa[width] - (group.coord - wa[x]) + + -- find our total values + local total_fact = 0 + local min_fact = 1 + local size = group.size + for c = group.first,group.last do + -- determine the width/height based on the size_hint + local i = c - group.first +1 + local size_hints = cls[c].size_hints + local size_hint = size_hints["min_"..width] or size_hints["base_"..width] or 0 + size = math.max(size_hint, size) + + -- calculate the height + if not fact[i] then + fact[i] = min_fact + else + min_fact = math.min(fact[i],min_fact) + end + total_fact = total_fact + fact[i] + end + size = math.max(1, math.min(size, available)) + + local coord = wa[y] + local used_size = 0 + local unused = wa[height] + for c = group.first,group.last do + local geom = {} + local hints = {} + local i = c - group.first +1 + geom[width] = size + geom[height] = math.max(1, math.floor(unused * fact[i] / total_fact)) + geom[x] = group.coord + geom[y] = coord + gs[cls[c]] = geom + hints.width, hints.height = cls[c]:apply_size_hints(geom.width, geom.height) + coord = coord + hints[height] + unused = unused - hints[height] + total_fact = total_fact - fact[i] + used_size = math.max(used_size, hints[width]) + end + + return used_size +end + +local function do_tile(param, orientation) + local t = param.tag or capi.screen[param.screen].selected_tag + orientation = orientation or "right" + + -- This handles all different orientations. + local width = "width" + local x = "x" + if orientation == "top" or orientation == "bottom" then + width = "height" + x = "y" + end + + local gs = param.geometries + local cls = param.clients + local nmaster = math.min(t.master_count, #cls) + local nother = math.max(#cls - nmaster,0) + + local mwfact = t.master_width_factor + local wa = param.workarea + local ncol = t.column_count + + local data = tag.getdata(t).windowfact + + if not data then + data = {} + tag.getdata(t).windowfact = data + end + + local coord = wa[x] + local place_master = true + if orientation == "left" or orientation == "top" then + -- if we are on the left or top we need to render the other windows first + place_master = false + end + + local grow_master = t.master_fill_policy == "expand" + -- this was easier than writing functions because there is a lot of data we need + for _ = 1,2 do + if place_master and nmaster > 0 then + local size = wa[width] + if nother > 0 or not grow_master then + size = math.min(wa[width] * mwfact, wa[width] - (coord - wa[x])) + end + if nother == 0 and not grow_master then + coord = coord + (wa[width] - size)/2 + end + if not data[0] then + data[0] = {} + end + coord = coord + tile_group(gs, cls, wa, orientation, data[0], {first=1, last=nmaster, coord = coord, size = size}) + end + + if not place_master and nother > 0 then + local last = nmaster + + -- we have to modify the work area size to consider left and top views + local wasize = wa[width] + if nmaster > 0 and (orientation == "left" or orientation == "top") then + wasize = wa[width] - wa[width]*mwfact + end + for i = 1,ncol do + -- Try to get equal width among remaining columns + local size = math.min( (wasize - (coord - wa[x])) / (ncol - i + 1) ) + local first = last + 1 + last = last + math.floor((#cls - last)/(ncol - i + 1)) + -- tile the column and update our current x coordinate + if not data[i] then + data[i] = {} + end + coord = coord + tile_group(gs, cls, wa, orientation, data[i], { first = first, last = last, coord = coord, size = size }) + end + end + place_master = not place_master + end + +end + +--- The main tile algo, on the right. +-- @param screen The screen number to tile. +-- @clientlayout awful.layout.suit.tile.top +tile.right = {} +tile.right.name = "tile" +tile.right.arrange = do_tile +function tile.right.mouse_resize_handler(c, corner, x, y) + return mouse_resize_handler(c, corner, x, y) +end + +--- The main tile algo, on the left. +-- @param screen The screen number to tile. +-- @clientlayout awful.layout.suit.tile.left +tile.left = {} +tile.left.name = "tileleft" +function tile.left.arrange(p) + return do_tile(p, "left") +end +function tile.left.mouse_resize_handler(c, corner, x, y) + return mouse_resize_handler(c, corner, x, y, "left") +end + +--- The main tile algo, on the bottom. +-- @param screen The screen number to tile. +-- @clientlayout awful.layout.suit.tile.bottom +tile.bottom = {} +tile.bottom.name = "tilebottom" +function tile.bottom.arrange(p) + return do_tile(p, "bottom") +end +function tile.bottom.mouse_resize_handler(c, corner, x, y) + return mouse_resize_handler(c, corner, x, y, "bottom") +end + +--- The main tile algo, on the top. +-- @param screen The screen number to tile. +-- @clientlayout awful.layout.suit.tile.top +tile.top = {} +tile.top.name = "tiletop" +function tile.top.arrange(p) + return do_tile(p, "top") +end +function tile.top.mouse_resize_handler(c, corner, x, y) + return mouse_resize_handler(c, corner, x, y, "top") +end + +tile.arrange = tile.right.arrange +tile.mouse_resize_handler = tile.right.mouse_resize_handler +tile.name = tile.right.name + +return tile + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/menu.lua b/lib/awful/menu.lua new file mode 100644 index 0000000..dcbc5fa --- /dev/null +++ b/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 "" .. l .. "" + 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 diff --git a/lib/awful/mouse/drag_to_tag.lua b/lib/awful/mouse/drag_to_tag.lua new file mode 100644 index 0000000..141456b --- /dev/null +++ b/lib/awful/mouse/drag_to_tag.lua @@ -0,0 +1,58 @@ +--------------------------------------------------------------------------- +--- When the the mouse reach the end of the screen, then switch tag instead +-- of screens. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @submodule mouse +--------------------------------------------------------------------------- + +local capi = {screen = screen, mouse = mouse} +local util = require("awful.util") +local tag = require("awful.tag") +local resize = require("awful.mouse.resize") + +local module = {} + +function module.drag_to_tag(c) + if (not c) or (not c.valid) then return end + + local coords = capi.mouse.coords() + + local dir = nil + + local wa = c.screen.workarea + + if coords.x >= wa.x + wa.width - 1 then + capi.mouse.coords({ x = wa.x + 2 }, true) + dir = "right" + elseif coords.x <= wa.x + 1 then + capi.mouse.coords({ x = wa.x + wa.width - 2 }, true) + dir = "left" + end + + local tags = c.screen.tags + local t = c.screen.selected_tag + local idx = t.index + + if dir then + + if dir == "right" then + local newtag = tags[util.cycle(#tags, idx + 1)] + c:move_to_tag(newtag) + tag.viewnext() + elseif dir == "left" then + local newtag = tags[util.cycle(#tags, idx - 1)] + c:move_to_tag(newtag) + tag.viewprev() + end + end +end + +resize.add_move_callback(function(c, _, _) + if module.enabled then + module.drag_to_tag(c) + end +end, "mouse.move") + +return setmetatable(module, {__call = function(_, ...) return module.drag_to_tag(...) end}) diff --git a/lib/awful/mouse/init.lua b/lib/awful/mouse/init.lua new file mode 100644 index 0000000..03f7e89 --- /dev/null +++ b/lib/awful/mouse/init.lua @@ -0,0 +1,437 @@ +--------------------------------------------------------------------------- +--- Mouse module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module mouse +--------------------------------------------------------------------------- + +-- Grab environment we need +local layout = require("awful.layout") +local aplace = require("awful.placement") +local util = require("awful.util") +local type = type +local ipairs = ipairs +local capi = +{ + root = root, + mouse = mouse, + screen = screen, + client = client, + mousegrabber = mousegrabber, +} + +local mouse = { + resize = require("awful.mouse.resize"), + snap = require("awful.mouse.snap"), + drag_to_tag = require("awful.mouse.drag_to_tag") +} + +mouse.object = {} +mouse.client = {} +mouse.wibox = {} + +--- The default snap distance. +-- @tfield integer awful.mouse.snap.default_distance +-- @tparam[opt=8] integer default_distance +-- @see awful.mouse.snap + +--- Enable screen edges snapping. +-- @tfield[opt=true] boolean awful.mouse.snap.edge_enabled + +--- Enable client to client snapping. +-- @tfield[opt=true] boolean awful.mouse.snap.client_enabled + +--- Enable changing tag when a client is dragged to the edge of the screen. +-- @tfield[opt=false] integer awful.mouse.drag_to_tag.enabled + +--- The snap outline background color. +-- @beautiful beautiful.snap_bg +-- @tparam color|string|gradient|pattern color + +--- The snap outline width. +-- @beautiful beautiful.snap_border_width +-- @param integer + +--- The snap outline shape. +-- @beautiful beautiful.snap_shape +-- @tparam function shape A `gears.shape` compatible function + +--- Get the client object under the pointer. +-- @deprecated awful.mouse.client_under_pointer +-- @return The client object under the pointer, if one can be found. +-- @see current_client +function mouse.client_under_pointer() + util.deprecate("Use mouse.current_client instead of awful.mouse.client_under_pointer()") + + return mouse.object.get_current_client() +end + +--- Move a client. +-- @function awful.mouse.client.move +-- @param c The client to move, or the focused one if nil. +-- @param snap The pixel to snap clients. +-- @param finished_cb Deprecated, do not use +function mouse.client.move(c, snap, finished_cb) --luacheck: no unused args + if finished_cb then + util.deprecate("The mouse.client.move `finished_cb` argument is no longer".. + " used, please use awful.mouse.resize.add_leave_callback(f, 'mouse.move')") + end + + c = c or capi.client.focus + + if not c + or c.fullscreen + or c.type == "desktop" + or c.type == "splash" + or c.type == "dock" then + return + end + + -- Compute the offset + local coords = capi.mouse.coords() + local geo = aplace.centered(capi.mouse,{parent=c, pretend=true}) + + local offset = { + x = geo.x - coords.x, + y = geo.y - coords.y, + } + + mouse.resize(c, "mouse.move", { + placement = aplace.under_mouse, + offset = offset, + snap = snap + }) +end + +mouse.client.dragtotag = { } + +--- Move a client to a tag by dragging it onto the left / right side of the screen. +-- @deprecated awful.mouse.client.dragtotag.border +-- @param c The client to move +function mouse.client.dragtotag.border(c) + util.deprecate("Use awful.mouse.snap.drag_to_tag_enabled = true instead ".. + "of awful.mouse.client.dragtotag.border(c). It will now be enabled.") + + -- Enable drag to border + mouse.snap.drag_to_tag_enabled = true + + return mouse.client.move(c) +end + +--- Move the wibox under the cursor. +-- @function awful.mouse.wibox.move +--@tparam wibox w The wibox to move, or none to use that under the pointer +function mouse.wibox.move(w) + w = w or mouse.wibox_under_pointer() + if not w then return end + + if not w + or w.type == "desktop" + or w.type == "splash" + or w.type == "dock" then + return + end + + -- Compute the offset + local coords = capi.mouse.coords() + local geo = aplace.centered(capi.mouse,{parent=w, pretend=true}) + + local offset = { + x = geo.x - coords.x, + y = geo.y - coords.y, + } + + mouse.resize(w, "mouse.move", { + placement = aplace.under_mouse, + offset = offset + }) +end + +--- Get a client corner coordinates. +-- @deprecated awful.mouse.client.corner +-- @tparam[opt=client.focus] client c The client to get corner from, focused one by default. +-- @tparam string corner The corner to use: auto, top_left, top_right, bottom_left, +-- bottom_right, left, right, top bottom. Default is auto, and auto find the +-- nearest corner. +-- @treturn string The corner name +-- @treturn number x The horizontal position +-- @treturn number y The vertical position +function mouse.client.corner(c, corner) + util.deprecate( + "Use awful.placement.closest_corner(mouse) or awful.placement[corner](mouse)".. + " instead of awful.mouse.client.corner" + ) + + c = c or capi.client.focus + if not c then return end + + local ngeo = nil + + if (not corner) or corner == "auto" then + ngeo, corner = aplace.closest_corner(mouse, {parent = c}) + elseif corner and aplace[corner] then + ngeo = aplace[corner](mouse, {parent = c}) + end + + return corner, ngeo and ngeo.x or nil, ngeo and ngeo.y or nil +end + +--- Resize a client. +-- @function awful.mouse.client.resize +-- @param c The client to resize, or the focused one by default. +-- @tparam string corner The corner to grab on resize. Auto detected by default. +-- @tparam[opt={}] table args A set of `awful.placement` arguments +-- @treturn string The corner (or side) name +function mouse.client.resize(c, corner, args) + c = c or capi.client.focus + + if not c then return end + + if c.fullscreen + or c.type == "desktop" + or c.type == "splash" + or c.type == "dock" then + return + end + + -- Set some default arguments + local new_args = setmetatable( + { + include_sides = (not args) or args.include_sides ~= false + }, + { + __index = args or {} + } + ) + + -- Move the mouse to the corner + if corner and aplace[corner] then + aplace[corner](capi.mouse, {parent=c}) + else + local _ + _, corner = aplace.closest_corner(capi.mouse, { + parent = c, + include_sides = new_args.include_sides ~= false, + }) + end + + new_args.corner = corner + + mouse.resize(c, "mouse.resize", new_args) + + return corner +end + +--- Default handler for `request::geometry` signals with "mouse.resize" context. +-- @signalhandler awful.mouse.resize_handler +-- @tparam client c The client +-- @tparam string context The context +-- @tparam[opt={}] table hints The hints to pass to the handler +function mouse.resize_handler(c, context, hints) + if hints and context and context:find("mouse.*") then + -- This handler only handle the floating clients. If the client is tiled, + -- then it let the layouts handle it. + local t = c.screen.selected_tag + local lay = t and t.layout or nil + + if (lay and lay == layout.suit.floating) or c.floating then + c:geometry { + x = hints.x, + y = hints.y, + width = hints.width, + height = hints.height, + } + elseif lay and lay.resize_handler then + lay.resize_handler(c, context, hints) + end + end +end + +-- Older layouts implement their own mousegrabber. +-- @tparam client c The client +-- @tparam table args Additional arguments +-- @treturn boolean This return false when the resize need to be aborted +mouse.resize.add_enter_callback(function(c, args) --luacheck: no unused args + if c.floating then return end + + local l = c.screen.selected_tag and c.screen.selected_tag.layout or nil + if l == layout.suit.floating then return end + + if l ~= layout.suit.floating and l.mouse_resize_handler then + capi.mousegrabber.stop() + + local geo, corner = aplace.closest_corner(capi.mouse, {parent=c}) + + l.mouse_resize_handler(c, corner, geo.x, geo.y) + + return false + end +end, "mouse.resize") + +--- Get the client currently under the mouse cursor. +-- @property current_client +-- @tparam client|nil The client + +function mouse.object.get_current_client() + local obj = capi.mouse.object_under_pointer() + if type(obj) == "client" then + return obj + end +end + +--- Get the wibox currently under the mouse cursor. +-- @property current_wibox +-- @tparam wibox|nil The wibox + +function mouse.object.get_current_wibox() + local obj = capi.mouse.object_under_pointer() + if type(obj) == "drawin" and obj.get_wibox then + return obj:get_wibox() + end +end + +--- Get the widgets currently under the mouse cursor. +-- +-- @property current_widgets +-- @tparam nil|table list The widget list +-- @treturn table The list of widgets.The first element is the biggest +-- container while the last is the topmost widget. The table contains *x*, *y*, +-- *width*, *height* and *widget*. +-- @see wibox.find_widgets + +function mouse.object.get_current_widgets() + local w = mouse.object.get_current_wibox() + if w then + local geo, coords = w:geometry(), capi.mouse:coords() + + local list = w:find_widgets(coords.x - geo.x, coords.y - geo.y) + + local ret = {} + + for k, v in ipairs(list) do + ret[k] = v.widget + end + + return ret, list + end +end + +--- Get the topmost widget currently under the mouse cursor. +-- @property current_widget +-- @tparam widget|nil widget The widget +-- @treturn ?widget The widget +-- @see wibox.find_widgets +-- @see current_widget_geometry + +function mouse.object.get_current_widget() + local wdgs, geos = mouse.object.get_current_widgets() + + if wdgs then + return wdgs[#wdgs], geos[#geos] + end +end + +--- Get the current widget geometry. +-- @property current_widget_geometry +-- @tparam ?table The geometry. +-- @see current_widget + +function mouse.object.get_current_widget_geometry() + local _, ret = mouse.object.get_current_widget() + + return ret +end + +--- Get the current widget geometries. +-- @property current_widget_geometries +-- @tparam ?table A list of geometry tables. +-- @see current_widgets + +function mouse.object.get_current_widget_geometries() + local _, ret = mouse.object.get_current_widgets() + + return ret +end + +--- True if the left mouse button is pressed. +-- @property is_left_mouse_button_pressed +-- @param boolean + +--- True if the right mouse button is pressed. +-- @property is_right_mouse_button_pressed +-- @param boolean + +--- True if the middle mouse button is pressed. +-- @property is_middle_mouse_button_pressed +-- @param boolean + +for _, b in ipairs {"left", "right", "middle"} do + mouse.object["is_".. b .."_mouse_button_pressed"] = function() + return capi.mouse.coords().buttons[1] + end +end + +capi.client.connect_signal("request::geometry", mouse.resize_handler) + +-- Set the cursor at startup +capi.root.cursor("left_ptr") + +-- Implement the custom property handler +local props = {} + +capi.mouse.set_newindex_miss_handler(function(_,key,value) + if mouse.object["set_"..key] then + mouse.object["set_"..key](value) + elseif not mouse.object["get_"..key] then + props[key] = value + else + -- If there is a getter, but no setter, then the property is read-only + error("Cannot set '" .. tostring(key) .. " because it is read-only") + end +end) + +capi.mouse.set_index_miss_handler(function(_,key) + if mouse.object["get_"..key] then + return mouse.object["get_"..key]() + else + return props[key] + end +end) + +--- Get or set the mouse coords. +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_mouse_coords.svg) +-- +--**Usage example output**: +-- +-- 235 +-- +-- +-- @usage +-- -- Get the position +--print(mouse.coords().x) +-- -- Change the position +--mouse.coords { +-- x = 185, +-- y = 10 +--} +-- +-- @tparam[opt=nil] table coords_table None or a table with x and y keys as mouse +-- coordinates. +-- @tparam[opt=nil] integer coords_table.x The mouse horizontal position +-- @tparam[opt=nil] integer coords_table.y The mouse vertical position +-- @tparam[opt=false] boolean silent Disable mouse::enter or mouse::leave events that +-- could be triggered by the pointer when moving. +-- @treturn integer table.x The horizontal position +-- @treturn integer table.y The vertical position +-- @treturn table table.buttons Table containing the status of buttons, e.g. field [1] is true +-- when button 1 is pressed. +-- @function mouse.coords + + +return mouse + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/mouse/resize.lua b/lib/awful/mouse/resize.lua new file mode 100644 index 0000000..edd278d --- /dev/null +++ b/lib/awful/mouse/resize.lua @@ -0,0 +1,229 @@ +--------------------------------------------------------------------------- +--- An extandable mouse resizing handler. +-- +-- This module offer a resizing and moving mechanism for drawable such as +-- clients and wiboxes. +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2016 Emmanuel Lepage Vallee +-- @submodule mouse +--------------------------------------------------------------------------- + +local aplace = require("awful.placement") +local capi = {mousegrabber = mousegrabber} +local beautiful = require("beautiful") + +local module = {} + +local mode = "live" +local req = "request::geometry" +local callbacks = {enter={}, move={}, leave={}} + +local cursors = { + ["mouse.move" ] = "fleur", + ["mouse.resize" ] = "cross", + ["mouse.resize_left" ] = "sb_h_double_arrow", + ["mouse.resize_right" ] = "sb_h_double_arrow", + ["mouse.resize_top" ] = "sb_v_double_arrow", + ["mouse.resize_bottom" ] = "sb_v_double_arrow", + ["mouse.resize_top_left" ] = "top_left_corner", + ["mouse.resize_top_right" ] = "top_right_corner", + ["mouse.resize_bottom_left" ] = "bottom_left_corner", + ["mouse.resize_bottom_right"] = "bottom_right_corner", +} + +--- The resize cursor name. +-- @beautiful beautiful.cursor_mouse_resize +-- @tparam[opt=cross] string cursor + +--- The move cursor name. +-- @beautiful beautiful.cursor_mouse_move +-- @tparam[opt=fleur] string cursor + +--- Set the resize mode. +-- The available modes are: +-- +-- * **live**: Resize the layout everytime the mouse move +-- * **after**: Resize the layout only when the mouse is released +-- +-- Some clients, such as XTerm, may lose information if resized too often. +-- +-- @function awful.mouse.resize.set_mode +-- @tparam string m The mode +function module.set_mode(m) + assert(m == "live" or m == "after") + mode = m +end + +--- Add an initialization callback. +-- This callback will be executed before the mouse grabbing starts. +-- @function awful.mouse.resize.add_enter_callback +-- @tparam function cb The callback (or nil) +-- @tparam[default=other] string context The callback context +function module.add_enter_callback(cb, context) + context = context or "other" + callbacks.enter[context] = callbacks.enter[context] or {} + table.insert(callbacks.enter[context], cb) +end + +--- Add a "move" callback. +-- This callback is executed in "after" mode (see `set_mode`) instead of +-- applying the operation. +-- @function awful.mouse.resize.add_move_callback +-- @tparam function cb The callback (or nil) +-- @tparam[default=other] string context The callback context +function module.add_move_callback(cb, context) + context = context or "other" + callbacks.move[context] = callbacks.move[context] or {} + table.insert(callbacks.move[context], cb) +end + +--- Add a "leave" callback +-- This callback is executed just before the `mousegrabber` stop +-- @function awful.mouse.resize.add_leave_callback +-- @tparam function cb The callback (or nil) +-- @tparam[default=other] string context The callback context +function module.add_leave_callback(cb, context) + context = context or "other" + callbacks.leave[context] = callbacks.leave[context] or {} + table.insert(callbacks.leave[context], cb) +end + +-- Resize, the drawable. +-- +-- Valid `args` are: +-- +-- * *enter_callback*: A function called before the `mousegrabber` start +-- * *move_callback*: A function called when the mouse move +-- * *leave_callback*: A function called before the `mousegrabber` is released +-- * *mode*: The resize mode +-- +-- @function awful.mouse.resize +-- @tparam client client A client +-- @tparam[default=mouse.resize] string context The resizing context +-- @tparam[opt={}] table args A set of `awful.placement` arguments + +local function handler(_, client, context, args) --luacheck: no unused_args + args = args or {} + context = context or "mouse.resize" + + local placement = args.placement + + if type(placement) == "string" and aplace[placement] then + placement = aplace[placement] + end + + -- Extend the table with the default arguments + args = setmetatable( + { + placement = placement or aplace.resize_to_mouse, + mode = args.mode or mode, + pretend = true, + }, + {__index = args or {}} + ) + + local geo + + for _, cb in ipairs(callbacks.enter[context] or {}) do + geo = cb(client, args) + + if geo == false then + return false + end + end + + if args.enter_callback then + geo = args.enter_callback(client, args) + + if geo == false then + return false + end + end + + geo = nil + + -- Select the cursor + local tcontext = context:gsub('[.]', '_') + local corner = args.corner and ("_".. args.corner) or "" + + local cursor = beautiful["cursor_"..tcontext] + or cursors[context..corner] + or cursors[context] + or "fleur" + + -- Execute the placement function and use request::geometry + capi.mousegrabber.run(function (_mouse) + if not client.valid then return end + + -- Resize everytime the mouse move (default behavior) + if args.mode == "live" then + -- Get the new geometry + geo = setmetatable(args.placement(client, args),{__index=args}) + end + + -- Execute the move callbacks. This can be used to add features such as + -- snap or adding fancy graphical effects. + for _, cb in ipairs(callbacks.move[context] or {}) do + -- If something is returned, assume it is a modified geometry + geo = cb(client, geo, args) or geo + + if geo == false then + return false + end + end + + if args.move_callback then + geo = args.move_callback(client, geo, args) + + if geo == false then + return false + end + end + + -- In case it was modified + setmetatable(geo,{__index=args}) + + if args.mode == "live" then + -- Ask the resizing handler to resize the client + client:emit_signal( req, context, geo) + end + + -- Quit when the button is released + for _,v in pairs(_mouse.buttons) do + if v then return true end + end + + -- Only resize after the mouse is released, this avoid losing content + -- in resize sensitive apps such as XTerm or allow external modules + -- to implement custom resizing. + if args.mode == "after" then + -- Get the new geometry + geo = args.placement(client, args) + + -- Ask the resizing handler to resize the client + client:emit_signal( req, context, geo) + end + + geo = nil + + for _, cb in ipairs(callbacks.leave[context] or {}) do + geo = cb(client, geo, args) + end + + if args.leave_callback then + geo = args.leave_callback(client, geo, args) + end + + if not geo then return false end + + -- In case it was modified + setmetatable(geo,{__index=args}) + + client:emit_signal( req, context, geo) + + return false + end, cursor) +end + +return setmetatable(module, {__call=handler}) diff --git a/lib/awful/mouse/snap.lua b/lib/awful/mouse/snap.lua new file mode 100644 index 0000000..048a679 --- /dev/null +++ b/lib/awful/mouse/snap.lua @@ -0,0 +1,266 @@ +--------------------------------------------------------------------------- +--- Mouse snapping related functions +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @submodule mouse +--------------------------------------------------------------------------- + +local aclient = require("awful.client") +local resize = require("awful.mouse.resize") +local aplace = require("awful.placement") +local wibox = require("wibox") +local beautiful = require("beautiful") +local color = require("gears.color") +local shape = require("gears.shape") +local cairo = require("lgi").cairo + +local capi = { + root = root, + mouse = mouse, + screen = screen, + client = client, + mousegrabber = mousegrabber, +} + +local module = { + default_distance = 8 +} + +local placeholder_w = nil + +local function show_placeholder(geo) + if not geo then + if placeholder_w then + placeholder_w.visible = false + end + return + end + + placeholder_w = placeholder_w or wibox { + ontop = true, + bg = color(beautiful.snap_bg or beautiful.bg_urgent or "#ff0000"), + } + + placeholder_w:geometry(geo) + + local img = cairo.ImageSurface(cairo.Format.A1, geo.width, geo.height) + local cr = cairo.Context(img) + + cr:set_operator(cairo.Operator.CLEAR) + cr:set_source_rgba(0,0,0,1) + cr:paint() + cr:set_operator(cairo.Operator.SOURCE) + cr:set_source_rgba(1,1,1,1) + + local line_width = beautiful.snap_border_width or 5 + cr:set_line_width(beautiful.xresources.apply_dpi(line_width)) + + local f = beautiful.snap_shape or function() + cr:translate(line_width,line_width) + shape.rounded_rect(cr,geo.width-2*line_width,geo.height-2*line_width, 10) + end + + f(cr, geo.width, geo.height) + + cr:stroke() + + placeholder_w.shape_bounding = img._native + + placeholder_w.visible = true +end + +local function build_placement(snap, axis) + return aplace.scale + + aplace[snap] + + ( + axis and aplace["maximize_"..axis] or nil + ) +end + +local function detect_screen_edges(c, snap) + local coords = capi.mouse.coords() + + local sg = c.screen.geometry + + local v, h = nil + + if math.abs(coords.x) <= snap + sg.x and coords.x >= sg.x then + h = "left" + elseif math.abs((sg.x + sg.width) - coords.x) <= snap then + h = "right" + end + + if math.abs(coords.y) <= snap + sg.y and coords.y >= sg.y then + v = "top" + elseif math.abs((sg.y + sg.height) - coords.y) <= snap then + v = "bottom" + end + + return v, h +end + +local current_snap, current_axis = nil + +local function detect_areasnap(c, distance) + local old_snap = current_snap + local v, h = detect_screen_edges(c, distance) + + if v and h then + current_snap = v.."_"..h + else + current_snap = v or h or nil + end + + if old_snap == current_snap then return end + + current_axis = ((v and not h) and "horizontally") + or ((h and not v) and "vertically") + or nil + + -- Show the expected geometry outline + show_placeholder( + current_snap and build_placement(current_snap, current_axis)(c, { + to_percent = 0.5, + honor_workarea = true, + pretend = true + }) or nil + ) + +end + +local function apply_areasnap(c, args) + if not current_snap then return end + + -- Remove the move offset + args.offset = {} + + placeholder_w.visible = false + + return build_placement(current_snap, current_axis)(c,{ + to_percent = 0.5, + honor_workarea = true, + }) +end + +local function snap_outside(g, sg, snap) + if g.x < snap + sg.x + sg.width and g.x > sg.x + sg.width then + g.x = sg.x + sg.width + elseif g.x + g.width < sg.x and g.x + g.width > sg.x - snap then + g.x = sg.x - g.width + end + if g.y < snap + sg.y + sg.height and g.y > sg.y + sg.height then + g.y = sg.y + sg.height + elseif g.y + g.height < sg.y and g.y + g.height > sg.y - snap then + g.y = sg.y - g.height + end + return g +end + +local function snap_inside(g, sg, snap) + local edgev = 'none' + local edgeh = 'none' + if math.abs(g.x) < snap + sg.x and g.x > sg.x then + edgev = 'left' + g.x = sg.x + elseif math.abs((sg.x + sg.width) - (g.x + g.width)) < snap then + edgev = 'right' + g.x = sg.x + sg.width - g.width + end + if math.abs(g.y) < snap + sg.y and g.y > sg.y then + edgeh = 'top' + g.y = sg.y + elseif math.abs((sg.y + sg.height) - (g.y + g.height)) < snap then + edgeh = 'bottom' + g.y = sg.y + sg.height - g.height + end + + -- What is the dominant dimension? + if g.width > g.height then + return g, edgeh + else + return g, edgev + end +end + +--- Snap a client to the closest client or screen edge. +-- @function awful.mouse.snap +-- @param c The client to snap. +-- @param snap The pixel to snap clients. +-- @param x The client x coordinate. +-- @param y The client y coordinate. +-- @param fixed_x True if the client isn't allowed to move in the x direction. +-- @param fixed_y True if the client isn't allowed to move in the y direction. +function module.snap(c, snap, x, y, fixed_x, fixed_y) + snap = snap or module.default_distance + c = c or capi.client.focus + local cur_geom = c:geometry() + local geom = c:geometry() + geom.width = geom.width + (2 * c.border_width) + geom.height = geom.height + (2 * c.border_width) + local edge + geom.x = x or geom.x + geom.y = y or geom.y + + geom, edge = snap_inside(geom, c.screen.geometry, snap) + geom = snap_inside(geom, c.screen.workarea, snap) + + -- Allow certain windows to snap to the edge of the workarea. + -- Only allow docking to workarea for consistency/to avoid problems. + if c.dockable then + local struts = c:struts() + struts['left'] = 0 + struts['right'] = 0 + struts['top'] = 0 + struts['bottom'] = 0 + if edge ~= "none" and c.floating then + if edge == "left" or edge == "right" then + struts[edge] = cur_geom.width + elseif edge == "top" or edge == "bottom" then + struts[edge] = cur_geom.height + end + end + c:struts(struts) + end + + for _, snapper in ipairs(aclient.visible(c.screen)) do + if snapper ~= c then + local snapper_geom = snapper:geometry() + snapper_geom.width = snapper_geom.width + (2 * snapper.border_width) + snapper_geom.height = snapper_geom.height + (2 * snapper.border_width) + geom = snap_outside(geom, snapper_geom, snap) + end + end + + geom.width = geom.width - (2 * c.border_width) + geom.height = geom.height - (2 * c.border_width) + + -- It's easiest to undo changes afterwards if they're not allowed + if fixed_x then geom.x = cur_geom.x end + if fixed_y then geom.y = cur_geom.y end + + return geom +end + +-- Enable edge snapping +resize.add_move_callback(function(c, geo, args) + -- Screen edge snapping (areosnap) + if (module.edge_enabled ~= false) + and args and (args.snap == nil or args.snap) then + detect_areasnap(c, 16) + end + + -- Snapping between clients + if (module.client_enabled ~= false) + and args and (args.snap == nil or args.snap) then + return module.snap(c, args.snap, geo.x, geo.y) + end +end, "mouse.move") + +-- Apply the aerosnap +resize.add_leave_callback(function(c, _, args) + if module.edge_enabled == false then return end + return apply_areasnap(c, args) +end, "mouse.move") + +return setmetatable(module, {__call = function(_, ...) return module.snap(...) end}) diff --git a/lib/awful/placement.lua b/lib/awful/placement.lua new file mode 100644 index 0000000..d288f49 --- /dev/null +++ b/lib/awful/placement.lua @@ -0,0 +1,1694 @@ +--------------------------------------------------------------------------- +--- Algorithms used to place various drawables. +-- +-- The functions provided by this module all follow the same arguments +-- conventions. This allow: +-- +-- * To use them in various other module as +-- [visitor objects](https://en.wikipedia.org/wiki/Visitor_pattern) +-- * Turn each function into an API with various common customization parameters. +-- * Re-use the same functions for the `mouse`, `client`s, `screen`s and `wibox`es +-- +-- +--

Compositing

+-- +-- It is possible to compose placement function using the `+` or `*` operator: +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_compose.svg) +-- +-- +-- -- 'right' will be replaced by 'left' +-- local f = (awful.placement.right + awful.placement.left) +-- f(client.focus) +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_compose2.svg) +-- +-- +-- -- Simulate Windows 7 'edge snap' (also called aero snap) feature +-- local axis = 'vertically' +-- local f = awful.placement.scale +-- + awful.placement.left +-- + (axis and awful.placement['maximize_'..axis] or nil) +-- local geo = f(client.focus, {honor_workarea=true, to_percent = 0.5}) +-- +--

Common arguments

+-- +-- **pretend** (*boolean*): +-- +-- Do not apply the new geometry. This is useful if only the return values is +-- necessary. +-- +-- **honor_workarea** (*boolean*): +-- +-- Take workarea into account when placing the drawable (default: false) +-- +-- **honor_padding** (*boolean*): +-- +-- Take the screen padding into account (see `screen.padding`) +-- +-- **tag** (*tag*): +-- +-- Use a tag geometry +-- +-- **margins** (*number* or *table*): +-- +-- A table with left, right, top, bottom keys or a number +-- +-- **parent** (client, wibox, mouse or screen): +-- +-- A parent drawable to use a base geometry +-- +-- **bounding_rect** (table): +-- +-- A bounding rectangle +-- +-- **attach** (*boolean*): +-- +-- When the parent geometry (like the screen) changes, re-apply the placement +-- function. This will add a `detach_callback` function to the drawable. Call +-- this to detach the function. This will be called automatically when a new +-- attached function is set. +-- +-- **offset** (*table or number*): +-- +-- The offset(s) to apply to the new geometry. +-- +-- **store_geometry** (*boolean*): +-- +-- Keep a single history of each type of placement. It can be restored using +-- `awful.placement.restore` by setting the right `context` argument. +-- +-- When either the parent or the screen geometry change, call the placement +-- function again. +-- +-- **update_workarea** (*boolean*): +-- +-- If *attach* is true, also update the screen workarea. +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou, Emmanuel Lepage Vallee 2016 +-- @module awful.placement +--------------------------------------------------------------------------- + +-- Grab environment we need +local ipairs = ipairs +local pairs = pairs +local math = math +local table = table +local capi = +{ + screen = screen, + mouse = mouse, + client = client +} +local client = require("awful.client") +local layout = require("awful.layout") +local a_screen = require("awful.screen") +local grect = require("gears.geometry").rectangle +local util = require("awful.util") +local cairo = require( "lgi" ).cairo +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +local function get_screen(s) + return s and capi.screen[s] +end + +local wrap_client = nil +local placement + +-- Store function -> keys +local reverse_align_map = {} + +-- Forward declarations +local area_common +local wibox_update_strut +local attach + +--- Allow multiple placement functions to be daisy chained. +-- This also allow the functions to be aware they are being chained and act +-- upon the previous nodes results to avoid unnecessary processing or deduce +-- extra paramaters/arguments. +local function compose(...) + local queue = {} + + local nodes = {...} + + -- Allow placement.foo + (var == 42 and placement.bar) + if not nodes[2] then + return nodes[1] + end + + -- nodes[1] == self, nodes[2] == other + for _, w in ipairs(nodes) do + -- Build an execution queue + if w.context and w.context == "compose" then + for _, elem in ipairs(w.queue or {}) do + table.insert(queue, elem) + end + else + table.insert(queue, w) + end + end + + local ret + ret = wrap_client(function(d, args, ...) + local rets = {} + local last_geo = nil + + -- As some functions may have to take into account results from + -- previously execued ones, add the `composition_results` hint. + args = setmetatable({composition_results=rets}, {__index=args}) + + -- Only apply the geometry once, not once per chain node, to do this, + -- Force the "pretend" argument and restore the original value for + -- the last node. + local attach_real = args.attach + args.pretend = true + args.attach = false + args.offset = {} + + for k, f in ipairs(queue) do + if k == #queue then + -- Let them fallback to the parent table + args.pretend = nil + args.offset = nil + end + + local r = {f(d, args, ...)} + last_geo = r[1] or last_geo + args.override_geometry = last_geo + + -- Keep the return value, store one per context + if f.context then + -- When 2 composition queue are executed, merge the return values + if f.context == "compose" then + for k2,v in pairs(r) do + rets[k2] = v + end + else + rets[f.context] = r + end + end + end + + if attach_real then + args.attach = true + attach(d, ret, args) + end + + return last_geo, rets + end, "compose") + + ret.queue = queue + + return ret +end + +wrap_client = function(f, context) + return setmetatable( + { + is_placement= true, + context = context, + }, + { + __call = function(_,...) return f(...) end, + __add = compose, -- Composition is usually defined as + + __mul = compose -- Make sense if you think of the functions as matrices + } + ) +end + +local placement_private = {} + +-- The module is a proxy in front of the "real" functions. +-- This allow syntax like: +-- +-- (awful.placement.no_overlap + awful.placement.no_offscreen)(c) +-- +placement = setmetatable({}, { + __index = placement_private, + __newindex = function(_, k, f) + placement_private[k] = wrap_client(f, k) + end +}) + +-- 3x3 matrix of the valid sides and corners +local corners3x3 = {{"top_left" , "top" , "top_right" }, + {"left" , nil , "right" }, + {"bottom_left", "bottom" , "bottom_right"}} + +-- 2x2 matrix of the valid sides and corners +local corners2x2 = {{"top_left" , "top_right" }, + {"bottom_left", "bottom_right"}} + +-- Compute the new `x` and `y`. +-- The workarea position need to be applied by the caller +local align_map = { + top_left = function(_ , _ , _ , _ ) return {x=0 , y=0 } end, + top_right = function(sw, _ , dw, _ ) return {x=sw-dw , y=0 } end, + bottom_left = function(_ , sh, _ , dh) return {x=0 , y=sh-dh } end, + bottom_right = function(sw, sh, dw, dh) return {x=sw-dw , y=sh-dh } end, + left = function(_ , sh, _ , dh) return {x=0 , y=sh/2-dh/2} end, + right = function(sw, sh, dw, dh) return {x=sw-dw , y=sh/2-dh/2} end, + top = function(sw, _ , dw, _ ) return {x=sw/2-dw/2, y=0 } end, + bottom = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=sh-dh } end, + centered = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=sh/2-dh/2} end, + center_vertical = function(_ , sh, _ , dh) return {x= nil , y=sh-dh } end, + center_horizontal = function(sw, _ , dw, _ ) return {x=sw/2-dw/2, y= nil } end, +} + +-- Some parameters to correctly compute the final size +local resize_to_point_map = { + -- Corners + top_left = {p1= nil , p2={1,1}, x_only=false, y_only=false, align="bottom_right"}, + top_right = {p1={0,1} , p2= nil , x_only=false, y_only=false, align="bottom_left" }, + bottom_left = {p1= nil , p2={1,0}, x_only=false, y_only=false, align="top_right" }, + bottom_right = {p1={0,0} , p2= nil , x_only=false, y_only=false, align="top_left" }, + + -- Sides + left = {p1= nil , p2={1,1}, x_only=true , y_only=false, align="top_right" }, + right = {p1={0,0} , p2= nil , x_only=true , y_only=false, align="top_left" }, + top = {p1= nil , p2={1,1}, x_only=false, y_only=true , align="bottom_left" }, + bottom = {p1={0,0} , p2= nil , x_only=false, y_only=true , align="top_left" }, +} + +-- Outer position matrix +-- 1=best case, 2=fallback +local outer_positions = { + left1 = function(r, w, _) return {x=r.x-w , y=r.y }, "down" end, + left2 = function(r, w, h) return {x=r.x-w , y=r.y-h+r.height }, "up" end, + right1 = function(r, _, _) return {x=r.x , y=r.y }, "down" end, + right2 = function(r, _, h) return {x=r.x , y=r.y-h+r.height }, "up" end, + top1 = function(r, _, h) return {x=r.x , y=r.y-h }, "right" end, + top2 = function(r, w, h) return {x=r.x-w+r.width, y=r.y-h }, "left" end, + bottom1 = function(r, _, _) return {x=r.x , y=r.y }, "right" end, + bottom2 = function(r, w, _) return {x=r.x-w+r.width, y=r.y }, "left" end, +} + +--- Add a context to the arguments. +-- This function extend the argument table. The context is used by some +-- internal helper methods. If there already is a context, it has priority and +-- is kept. +local function add_context(args, context) + return setmetatable({context = (args or {}).context or context }, {__index=args}) +end + +local data = setmetatable({}, { __mode = 'k' }) + +--- Store a drawable geometry (per context) in a weak table. +-- @param d The drawin +-- @tparam string reqtype The context. +local function store_geometry(d, reqtype) + if not data[d] then data[d] = {} end + if not data[d][reqtype] then data[d][reqtype] = {} end + data[d][reqtype] = d:geometry() + data[d][reqtype].screen = d.screen + data[d][reqtype].border_width = d.border_width +end + +--- Get the margins and offset +-- @tparam table args The arguments +-- @treturn table The margins +-- @treturn table The offsets +local function get_decoration(args) + local offset = args.offset + + -- Offset are "blind" values added to the output + offset = type(offset) == "number" and { + x = offset, + y = offset, + width = offset, + height = offset, + } or args.offset or {} + + -- Margins are distances on each side to substract from the area` + local m = type(args.margins) == "table" and args.margins or { + left = args.margins or 0 , right = args.margins or 0, + top = args.margins or 0 , bottom = args.margins or 0 + } + + return m, offset +end + +--- Apply some modifications before applying the new geometry. +-- @tparam table new_geo The new geometry +-- @tparam table args The common arguments +-- @tparam boolean force Always ajust the geometry, even in pretent mode. This +-- should only be used when returning the final geometry as it would otherwise +-- mess the pipeline. +-- @treturn table|nil The new geometry +local function fix_new_geometry(new_geo, args, force) + if (args.pretend and not force) or not new_geo then return nil end + + local m, offset = get_decoration(args) + + return { + x = new_geo.x and (new_geo.x + (offset.x or 0) + (m.left or 0) ), + y = new_geo.y and (new_geo.y + (offset.y or 0) + (m.top or 0) ), + width = new_geo.width and math.max( + 1, (new_geo.width + (offset.width or 0) - (m.left or 0) - (m.right or 0) ) + ), + height = new_geo.height and math.max( + 1, (new_geo.height + (offset.height or 0) - (m.top or 0) - (m.bottom or 0) ) + ), + } +end + +-- Get the area covered by a drawin. +-- @param d The drawin +-- @tparam[opt=nil] table new_geo A new geometry +-- @tparam[opt=false] boolean ignore_border_width Ignore the border +-- @tparam table args the method arguments +-- @treturn The drawin's area. +area_common = function(d, new_geo, ignore_border_width, args) + -- The C side expect no arguments, nil isn't valid + local geometry = new_geo and d:geometry(new_geo) or d:geometry() + local border = ignore_border_width and 0 or d.border_width or 0 + + -- When using the placement composition along with the "pretend" + -- option, it is necessary to keep a "virtual" geometry. + if args and args.override_geometry then + geometry = util.table.clone(args.override_geometry) + end + + geometry.width = geometry.width + 2 * border + geometry.height = geometry.height + 2 * border + return geometry +end + +--- Get (and optionally set) an object geometry. +-- Some elements, such as `mouse` and `screen` don't have a `:geometry()` +-- methods. +-- @param obj An object +-- @tparam table args the method arguments +-- @tparam[opt=nil] table new_geo A new geometry to replace the existing one +-- @tparam[opt=false] boolean ignore_border_width Ignore the border +-- @treturn table A table with *x*, *y*, *width* and *height*. +local function geometry_common(obj, args, new_geo, ignore_border_width) + -- Store the current geometry in a singleton-memento + if args.store_geometry and new_geo and args.context then + store_geometry(obj, args.context) + end + + -- It's a mouse + if obj.coords then + local coords = fix_new_geometry(new_geo, args) + and obj.coords(new_geo) or obj.coords() + return {x=coords.x, y=coords.y, width=0, height=0} + elseif obj.geometry then + local geo = obj.geometry + + -- It is either a drawable or something that implement its API + if type(geo) == "function" then + local dgeo = area_common( + obj, fix_new_geometry(new_geo, args), ignore_border_width, args + ) + + -- Apply the margins + if args.margins then + local delta = get_decoration(args) + + return { + x = dgeo.x - (delta.left or 0), + y = dgeo.y - (delta.top or 0), + width = dgeo.width + (delta.left or 0) + (delta.right or 0), + height = dgeo.height + (delta.top or 0) + (delta.bottom or 0), + } + end + + return dgeo + end + + -- It is a screen, it doesn't support setting new sizes. + return obj:get_bounding_geometry(args) + else + assert(false, "Invalid object") + end +end + +--- Get the parent geometry from the standardized arguments API shared by all +-- `awful.placement` methods. +-- @param obj A screen or a drawable +-- @tparam table args the method arguments +-- @treturn table A table with *x*, *y*, *width* and *height*. +local function get_parent_geometry(obj, args) + -- Didable override_geometry, context and other to avoid mutating the state + -- or using the wrong geo. + + if args.bounding_rect then + return args.bounding_rect + elseif args.parent then + return geometry_common(args.parent, {}) + elseif obj.screen then + return geometry_common(obj.screen, { + honor_padding = args.honor_padding, + honor_workarea = args.honor_workarea + }) + else + return geometry_common(capi.screen[capi.mouse.screen], args) + end +end + +--- Move a point into an area. +-- This doesn't change the *width* and *height* values, allowing the target +-- area to be smaller than the source one. +-- @tparam table source The (larger) geometry to move `target` into +-- @tparam table target The area to move into `source` +-- @treturn table A table with *x* and *y* keys +local function move_into_geometry(source, target) + local ret = {x = target.x, y = target.y} + + -- Horizontally + if ret.x < source.x then + ret.x = source.x + elseif ret.x > source.x + source.width then + ret.x = source.x + source.width - 1 + end + + -- Vertically + if ret.y < source.y then + ret.y = source.y + elseif ret.y > source.y + source.height then + ret.y = source.y + source.height - 1 + end + + return ret +end + +-- Update the workarea +wibox_update_strut = function(d, position, args) + -- If the drawable isn't visible, remove the struts + if not d.visible then + d:struts { left = 0, right = 0, bottom = 0, top = 0 } + return + end + + -- Detect horizontal or vertical drawables + local geo = area_common(d) + local vertical = geo.width < geo.height + + -- Look into the `position` string to find the relevants sides to crop from + -- the workarea + local struts = { left = 0, right = 0, bottom = 0, top = 0 } + + local m = get_decoration(args) + + if vertical then + for _, v in ipairs {"right", "left"} do + if (not position) or position:match(v) then + struts[v] = geo.width + m[v] + end + end + else + for _, v in ipairs {"top", "bottom"} do + if (not position) or position:match(v) then + struts[v] = geo.height + m[v] + end + end + end + + -- Update the workarea + d:struts(struts) +end + +-- Pin a drawable to a placement function. +-- Automatically update the position when the size change. +-- All other arguments will be passed to the `position` function (if any) +-- @tparam[opt=client.focus] drawable d A drawable (like `client`, `mouse` +-- or `wibox`) +-- @param position_f A position name (see `align`) or a position function +-- @tparam[opt={}] table args Other arguments +attach = function(d, position_f, args) + args = args or {} + + if args.pretend then return end + + if not args.attach then return end + + -- Avoid a connection loop + args = setmetatable({attach=false}, {__index=args}) + + d = d or capi.client.focus + if not d then return end + + if type(position_f) == "string" then + position_f = placement[position_f] + end + + if not position_f then return end + + -- If there is multiple attached function, there is an high risk of infinite + -- loop. While some combinaisons are harmless, other are very hard to debug. + -- + -- Use the placement composition to build explicit multi step attached + -- placement functions. + if d.detach_callback then + d.detach_callback() + d.detach_callback = nil + end + + local function tracker() + position_f(d, args) + end + + d:connect_signal("property::width" , tracker) + d:connect_signal("property::height" , tracker) + d:connect_signal("property::border_width", tracker) + + local function tracker_struts() + --TODO this is too fragile and doesn't work with all methods. + wibox_update_strut(d, d.position or reverse_align_map[position_f], args) + end + + local parent = args.parent or d.screen + + if args.update_workarea then + d:connect_signal("property::geometry" , tracker_struts) + d:connect_signal("property::visible" , tracker_struts) + capi.client.connect_signal("property::struts", tracker_struts) + + tracker_struts() + elseif parent == d.screen then + if args.honor_workarea then + parent:connect_signal("property::workarea", tracker) + end + + if args.honor_padding then + parent:connect_signal("property::padding", tracker) + end + end + + -- If there is a parent drawable, screen, also track it. + -- Note that tracking the mouse is not supported + if parent and parent.connect_signal then + parent:connect_signal("property::geometry" , tracker) + end + + -- Create a way to detach a placement function + function d.detach_callback() + d:disconnect_signal("property::width" , tracker) + d:disconnect_signal("property::height" , tracker) + d:disconnect_signal("property::border_width", tracker) + if parent then + parent:disconnect_signal("property::geometry" , tracker) + + if parent == d.screen then + if args.honor_workarea then + parent:disconnect_signal("property::workarea", tracker) + end + + if args.honor_padding then + parent:disconnect_signal("property::padding", tracker) + end + end + end + + if args.update_workarea then + d:disconnect_signal("property::geometry" , tracker_struts) + d:disconnect_signal("property::visible" , tracker_struts) + capi.client.disconnect_signal("property::struts", tracker_struts) + end + end +end + +-- Convert 2 points into a rectangle +local function rect_from_points(p1x, p1y, p2x, p2y) + return { + x = p1x, + y = p1y, + width = p2x - p1x, + height = p2y - p1y, + } +end + +-- Convert a rectangle and matrix info into a point +local function rect_to_point(rect, corner_i, corner_j) + return { + x = rect.x + corner_i * math.floor(rect.width ), + y = rect.y + corner_j * math.floor(rect.height), + } +end + +-- Create a pair of rectangles used to set the relative areas. +-- v=vertical, h=horizontal +local function get_cross_sections(abs_geo, mode) + if not mode or mode == "cursor" then + -- A 1px cross section centered around the mouse position + local coords = capi.mouse.coords() + return { + h = { + x = abs_geo.drawable_geo.x , + y = coords.y , + width = abs_geo.drawable_geo.width , + height = 1 , + }, + v = { + x = coords.x , + y = abs_geo.drawable_geo.y , + width = 1 , + height = abs_geo.drawable_geo.height, + } + } + elseif mode == "geometry" then + -- The widget geometry extended to reach the end of the drawable + + return { + h = { + x = abs_geo.drawable_geo.x , + y = abs_geo.y , + width = abs_geo.drawable_geo.width , + height = abs_geo.height , + }, + v = { + x = abs_geo.x , + y = abs_geo.drawable_geo.y , + width = abs_geo.width , + height = abs_geo.drawable_geo.height, + } + } + elseif mode == "cursor_inside" then + -- A 1x1 rectangle centered around the mouse position + + local coords = capi.mouse.coords() + coords.width,coords.height = 1,1 + return {h=coords, v=coords} + elseif mode == "geometry_inside" then + -- The widget absolute geometry, unchanged + + return {h=abs_geo, v=abs_geo} + end +end + +-- When a rectangle is embedded into a bigger one, get the regions around +-- the outline of the bigger rectangle closest to the smaller one (on each side) +local function get_relative_regions(geo, mode, is_absolute) + + -- Use the mouse position and the wibox/client under it + if not geo then + local draw = capi.mouse.current_wibox + geo = draw and draw:geometry() or capi.mouse.coords() + geo.drawable = draw + elseif is_absolute then + -- Some signals are a bit inconsistent in their arguments convention. + -- This little hack tries to mitigate the issue. + + geo.drawable = geo -- is a wibox or client, geometry and object are one + -- and the same. + elseif (not geo.drawable) and geo.x and geo.width then + local coords = capi.mouse.coords() + + -- Check if the mouse is in the rect + if coords.x > geo.x and coords.x < geo.x+geo.width and + coords.y > geo.y and coords.y < geo.y+geo.height then + geo.drawable = capi.mouse.current_wibox + end + + -- Maybe there is a client + if (not geo.drawable) and capi.mouse.current_client then + geo.drawable = capi.mouse.current_client + end + end + + -- Get the drawable geometry + local dpos = geo.drawable and ( + geo.drawable.drawable and + geo.drawable.drawable:geometry() + or geo.drawable:geometry() + ) or {x=0, y=0} + + -- Compute the absolute widget geometry + local abs_widget_geo = is_absolute and geo or { + x = dpos.x + geo.x , + y = dpos.y + geo.y , + width = geo.width , + height = geo.height , + drawable = geo.drawable , + } + + abs_widget_geo.drawable_geo = geo.drawable and dpos or geo + + -- Get the point for comparison. + local center_point = mode:match("cursor") and capi.mouse.coords() or { + x = abs_widget_geo.x + abs_widget_geo.width / 2, + y = abs_widget_geo.y + abs_widget_geo.height / 2, + } + + -- Get widget regions for both axis + local cs = get_cross_sections(abs_widget_geo, mode) + + -- Get the 4 closest points from `center_point` around the wibox + local regions = { + left = {x = cs.h.x , y = cs.h.y }, + right = {x = cs.h.x+cs.h.width, y = cs.h.y }, + top = {x = cs.v.x , y = cs.v.y }, + bottom = {x = cs.v.x , y = cs.v.y+cs.v.height}, + } + + -- Assume the section is part of a single screen until someone complains. + -- It is much faster to compute and getting it wrong probably has no side + -- effects. + local s = geo.drawable and geo.drawable.screen or a_screen.getbycoord( + center_point.x, + center_point.y + ) + + -- Compute the distance (dp) between the `center_point` and the sides. + -- This is only relevant for "cursor" and "cursor_inside" modes. + for _, v in pairs(regions) do + local dx, dy = v.x - center_point.x, v.y - center_point.y + + v.distance = math.sqrt(dx*dx + dy*dy) + v.width = cs.v.width + v.height = cs.h.height + v.screen = capi.screen[s] + end + + return regions +end + +-- Check if the proposed geometry fits the screen +local function fit_in_bounding(obj, geo, args) + local sgeo = get_parent_geometry(obj, args) + local region = cairo.Region.create_rectangle(cairo.RectangleInt(sgeo)) + + region:intersect(cairo.Region.create_rectangle( + cairo.RectangleInt(geo) + )) + + local geo2 = region:get_rectangle(0) + + -- If the geometry is the same then it fits, otherwise it will be cropped. + return geo2.width == geo.width and geo2.height == geo.height +end + +--- Move a drawable to the closest corner of the parent geometry (such as the +-- screen). +-- +-- Valid arguments include the common ones and: +-- +-- * **include_sides**: Also include the left, right, top and bottom positions +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_closest_mouse.svg) +-- +--**Usage example output**: +-- +-- Closest corner: top_left +-- +-- +-- @usage +-- -- Move the mouse to the closest corner of the focused client +--awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) +-- -- It is possible to emulate the mouse API to get the closest corner of +-- -- random area +--local _, corner = awful.placement.closest_corner( +-- {coords=function() return {x = 100, y=100} end}, +-- {include_sides = true, bounding_rect = {x=0, y=0, width=200, height=200}} +--) +--print('Closest corner:', corner) +-- @tparam[opt=client.focus] drawable d A drawable (like `client`, `mouse` +-- or `wibox`) +-- @tparam[opt={}] table args The arguments +-- @treturn table The new geometry +-- @treturn string The corner name +function placement.closest_corner(d, args) + args = add_context(args, "closest_corner") + d = d or capi.client.focus + + local sgeo = get_parent_geometry(d, args) + local dgeo = geometry_common(d, args) + + local pos = move_into_geometry(sgeo, dgeo) + + local corner_i, corner_j, n + + -- Use the product of 3 to get the closest point in a NxN matrix + local function f(_n, mat) + n = _n + -- The +1 is required to avoid a rounding error when + -- pos.x == sgeo.x+sgeo.width + corner_i = -math.ceil( ( (sgeo.x - pos.x) * n) / (sgeo.width + 1)) + corner_j = -math.ceil( ( (sgeo.y - pos.y) * n) / (sgeo.height + 1)) + return mat[corner_j + 1][corner_i + 1] + end + + -- Turn the area into a grid and snap to the cloest point. This size of the + -- grid will increase the accuracy. A 2x2 matrix only include the corners, + -- at 3x3, this include the sides too technically, a random size would work, + -- but without corner names. + local grid_size = args.include_sides and 3 or 2 + + -- If the point is in the center, use the closest corner + local corner = grid_size == 3 and f(3, corners3x3) or f(2, corners2x2) + + -- Transpose the corner back to the original size + local new_args = setmetatable({position = corner}, {__index=args}) + local ngeo = placement_private.align(d, new_args) + + return fix_new_geometry(ngeo, args, true), corner +end + +--- Place the client so no part of it will be outside the screen (workarea). +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_no_offscreen.svg) +-- +--**Usage example output**: +-- +-- Before: x=-30, y=-30, width=100, height=100 +-- After: x=10, y=10, width=100, height=100 +-- +-- +-- @usage +--awful.placement.no_offscreen(c)--, {honor_workarea=true, margins=40}) +-- @client c The client. +-- @tparam[opt=client's screen] integer screen The screen. +-- @treturn table The new client geometry. +function placement.no_offscreen(c, screen) + --HACK necessary for composition to work. The API will be changed soon + if type(screen) == "table" then + screen = nil + end + + c = c or capi.client.focus + local geometry = area_common(c) + screen = get_screen(screen or c.screen or a_screen.getbycoord(geometry.x, geometry.y)) + local screen_geometry = screen.workarea + + if geometry.x + geometry.width > screen_geometry.x + screen_geometry.width then + geometry.x = screen_geometry.x + screen_geometry.width - geometry.width + end + if geometry.x < screen_geometry.x then + geometry.x = screen_geometry.x + end + + if geometry.y + geometry.height > screen_geometry.y + screen_geometry.height then + geometry.y = screen_geometry.y + screen_geometry.height - geometry.height + end + if geometry.y < screen_geometry.y then + geometry.y = screen_geometry.y + end + + return c:geometry { + x = geometry.x, + y = geometry.y + } +end + +--- Place the client where there's place available with minimum overlap. +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_no_overlap.svg) +-- +-- @usage +--awful.placement.no_overlap(client.focus) +--local x,y = screen[4].geometry.x, screen[4].geometry.y +-- @param c The client. +-- @treturn table The new geometry +function placement.no_overlap(c) + c = c or capi.client.focus + local geometry = area_common(c) + local screen = get_screen(c.screen or a_screen.getbycoord(geometry.x, geometry.y)) + local cls = client.visible(screen) + local curlay = layout.get() + local areas = { screen.workarea } + for _, cl in pairs(cls) do + if cl ~= c and cl.type ~= "desktop" and (cl.floating or curlay == layout.suit.floating) then + areas = grect.area_remove(areas, area_common(cl)) + end + end + + -- Look for available space + local found = false + local new = { x = geometry.x, y = geometry.y, width = 0, height = 0 } + for _, r in ipairs(areas) do + if r.width >= geometry.width + and r.height >= geometry.height + and r.width * r.height > new.width * new.height then + found = true + new = r + -- Check if the client's current position is available + -- and prefer that one (why move it around pointlessly?) + if geometry.x >= r.x + and geometry.y >= r.y + and geometry.x + geometry.width <= r.x + r.width + and geometry.y + geometry.height <= r.y + r.height then + new.x = geometry.x + new.y = geometry.y + end + end + end + + -- We did not find an area with enough space for our size: + -- just take the biggest available one and go in. + -- This makes sure to have the whole screen's area in case it has been + -- removed. + if not found then + if #areas == 0 then + areas = { screen.workarea } + end + for _, r in ipairs(areas) do + if r.width * r.height > new.width * new.height then + new = r + end + end + end + + -- Restore height and width + new.width = geometry.width + new.height = geometry.height + + return c:geometry({ x = new.x, y = new.y }) +end + +--- Place the client under the mouse. +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_under_mouse.svg) +-- +-- @usage +--awful.placement.under_mouse(client.focus) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +function placement.under_mouse(d, args) + args = add_context(args, "under_mouse") + d = d or capi.client.focus + + local m_coords = capi.mouse.coords() + + local ngeo = geometry_common(d, args) + ngeo.x = math.floor(m_coords.x - ngeo.width / 2) + ngeo.y = math.floor(m_coords.y - ngeo.height / 2) + + local bw = d.border_width or 0 + ngeo.width = ngeo.width - 2*bw + ngeo.height = ngeo.height - 2*bw + + geometry_common(d, args, ngeo) + + return fix_new_geometry(ngeo, args, true) +end + +--- Place the client next to the mouse. +-- +-- It will place `c` next to the mouse pointer, trying the following positions +-- in this order: right, left, above and below. +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_next_to_mouse.svg) +-- +-- @usage +--awful.placement.next_to_mouse(client.focus) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +function placement.next_to_mouse(d, args) + if type(args) == "number" then + util.deprecate( + "awful.placement.next_to_mouse offset argument is deprecated".. + " use awful.placement.next_to_mouse(c, {offset={x=...}})" + ) + args = nil + end + + local old_args = args or {} + + args = add_context(args, "next_to_mouse") + d = d or capi.client.focus + + local sgeo = get_parent_geometry(d, args) + + args.pretend = true + args.parent = capi.mouse + + local ngeo = placement.left(d, args) + + if ngeo.x + ngeo.width > sgeo.x+sgeo.width then + ngeo = placement.right(d, args) + else + -- It is _next_ to mouse, not under_mouse + ngeo.x = ngeo.x+1 + end + + args.pretend = old_args.pretend + + geometry_common(d, args, ngeo) + + attach(d, placement.next_to_mouse, old_args) + + return fix_new_geometry(ngeo, args, true) +end + +--- Resize the drawable to the cursor. +-- +-- Valid args: +-- +-- * *axis*: The axis (vertical or horizontal). If none is +-- specified, then the drawable will be resized on both axis. +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_resize_to_mouse.svg) +-- +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +function placement.resize_to_mouse(d, args) + d = d or capi.client.focus + args = add_context(args, "resize_to_mouse") + + local coords = capi.mouse.coords() + local ngeo = geometry_common(d, args) + local h_only = args.axis == "horizontal" + local v_only = args.axis == "vertical" + + -- To support both growing and shrinking the drawable, it is necessary + -- to decide to use either "north or south" and "east or west" directions. + -- Otherwise, the result will always be 1x1 + local _, closest_corner = placement.closest_corner(capi.mouse, { + parent = d, + pretend = true, + include_sides = args.include_sides or false, + }) + + -- Given "include_sides" wasn't set, it will always return a name + -- with the 2 axis. If only one axis is needed, adjust the result + if h_only then + closest_corner = closest_corner:match("left") or closest_corner:match("right") + elseif v_only then + closest_corner = closest_corner:match("top") or closest_corner:match("bottom") + end + + -- Use p0 (mouse), p1 and p2 to create a rectangle + local pts = resize_to_point_map[closest_corner] + local p1 = pts.p1 and rect_to_point(ngeo, pts.p1[1], pts.p1[2]) or coords + local p2 = pts.p2 and rect_to_point(ngeo, pts.p2[1], pts.p2[2]) or coords + + -- Create top_left and bottom_right points, convert to rectangle + ngeo = rect_from_points( + pts.y_only and ngeo.x or math.min(p1.x, p2.x), + pts.x_only and ngeo.y or math.min(p1.y, p2.y), + pts.y_only and ngeo.x + ngeo.width or math.max(p2.x, p1.x), + pts.x_only and ngeo.y + ngeo.height or math.max(p2.y, p1.y) + ) + + local bw = d.border_width or 0 + + for _, a in ipairs {"width", "height"} do + ngeo[a] = ngeo[a] - 2*bw + end + + -- Now, correct the geometry by the given size_hints offset + if d.apply_size_hints then + local w, h = d:apply_size_hints( + ngeo.width, + ngeo.height + ) + local offset = align_map[pts.align](w, h, ngeo.width, ngeo.height) + ngeo.x = ngeo.x - offset.x + ngeo.y = ngeo.y - offset.y + end + + geometry_common(d, args, ngeo) + + return fix_new_geometry(ngeo, args, true) +end + +--- Move the drawable (client or wibox) `d` to a screen position or side. +-- +-- Supported args.positions are: +-- +-- * top_left +-- * top_right +-- * bottom_left +-- * bottom_right +-- * left +-- * right +-- * top +-- * bottom +-- * centered +-- * center_vertical +-- * center_horizontal +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_align.svg) +-- +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +function placement.align(d, args) + args = add_context(args, "align") + d = d or capi.client.focus + + if not d or not args.position then return end + + local sgeo = get_parent_geometry(d, args) + local dgeo = geometry_common(d, args) + local bw = d.border_width or 0 + + local pos = align_map[args.position]( + sgeo.width , + sgeo.height, + dgeo.width , + dgeo.height + ) + + local ngeo = { + x = (pos.x and math.ceil(sgeo.x + pos.x) or dgeo.x) , + y = (pos.y and math.ceil(sgeo.y + pos.y) or dgeo.y) , + width = math.ceil(dgeo.width ) - 2*bw, + height = math.ceil(dgeo.height ) - 2*bw, + } + + geometry_common(d, args, ngeo) + + attach(d, placement[args.position], args) + + return fix_new_geometry(ngeo, args, true) +end + +-- Add the alias functions +for k in pairs(align_map) do + placement[k] = function(d, args) + args = add_context(args, k) + args.position = k + return placement_private.align(d, args) + end + reverse_align_map[placement[k]] = k +end + +-- Add the documentation for align alias + +--- +-- Align a client to the top left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_top_left.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name top_left +-- @class function +-- +-- @usage +--awful.placement.top_left(client.focus) + +--- +-- Align a client to the top right of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_top_right.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name top_right +-- @class function +-- +-- @usage +--awful.placement.top_right(client.focus) + +--- +-- Align a client to the bottom left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_bottom_left.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name bottom_left +-- @class function +-- +-- @usage +--awful.placement.bottom_left(client.focus) + +--- +-- Align a client to the bottom right of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_bottom_right.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name bottom_right +-- @class function +-- +-- @usage +--awful.placement.bottom_right(client.focus) + +--- +-- Align a client to the left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_left.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name left +-- @class function +-- +-- @usage +--awful.placement.left(client.focus) + +--- +-- Align a client to the right of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_right.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name right +-- @class function +-- +-- @usage +--awful.placement.right(client.focus) + +--- +-- Align a client to the top of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_top.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name top +-- @class function +-- +-- @usage +--awful.placement.top(client.focus) +--assert(c.x == screen[1].geometry.width/2-40/2-c.border_width) + +--- +-- Align a client to the bottom of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_bottom.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name bottom +-- @class function +-- +-- @usage +--awful.placement.bottom(client.focus) + +--- +-- Align a client to the center of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_centered.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name centered +-- @class function +-- +-- @usage +--awful.placement.centered(client.focus) + +--- +-- Align a client to the vertical center of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_center_vertical.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @name center_vertical +-- @class function +-- +-- @usage +--awful.placement.center_vertical(client.focus) + +--- +-- Align a client to the horizontal center left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_center_horizontal.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name center_horizontal +-- @class function +-- +-- @usage +--awful.placement.center_horizontal(client.focus) + +--- Stretch a drawable in a specific direction. +-- Valid args: +-- +-- * **direction**: The stretch direction (*left*, *right*, *up*, *down*) or +-- a table with multiple directions. +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch.svg) +-- +-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args The arguments +-- @treturn table The new geometry +function placement.stretch(d, args) + args = add_context(args, "stretch") + + d = d or capi.client.focus + if not d or not args.direction then return end + + -- In case there is multiple directions, call `stretch` for each of them + if type(args.direction) == "table" then + for _, dir in ipairs(args.direction) do + args.direction = dir + placement_private.stretch(dir, args) + end + return + end + + local sgeo = get_parent_geometry(d, args) + local dgeo = geometry_common(d, args) + local ngeo = geometry_common(d, args, nil, true) + local bw = d.border_width or 0 + + if args.direction == "left" then + ngeo.x = sgeo.x + ngeo.width = dgeo.width + (dgeo.x - ngeo.x) + elseif args.direction == "right" then + ngeo.width = sgeo.width - ngeo.x - 2*bw + elseif args.direction == "up" then + ngeo.y = sgeo.y + ngeo.height = dgeo.height + (dgeo.y - ngeo.y) + elseif args.direction == "down" then + ngeo.height = sgeo.height - dgeo.y - 2*bw + else + assert(false) + end + + -- Avoid negative sizes if args.parent isn't compatible + ngeo.width = math.max(args.minimim_width or 1, ngeo.width ) + ngeo.height = math.max(args.minimim_height or 1, ngeo.height) + + geometry_common(d, args, ngeo) + + attach(d, placement["stretch_"..args.direction], args) + + return fix_new_geometry(ngeo, args, true) +end + +-- Add the alias functions +for _,v in ipairs {"left", "right", "up", "down"} do + placement["stretch_"..v] = function(d, args) + args = add_context(args, "stretch_"..v) + args.direction = v + return placement_private.stretch(d, args) + end +end + +--- +-- Stretch the drawable to the left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch_left.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +-- @name stretch_left +-- @class function +-- +-- @usage +--placement.stretch_left(client.focus) + +--- +-- Stretch the drawable to the right of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch_right.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +-- @name stretch_right +-- @class function +-- +-- @usage +--placement.stretch_right(client.focus) + +--- +-- Stretch the drawable to the top of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch_up.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +-- @name stretch_up +-- @class function +-- +-- @usage +--placement.stretch_up(client.focus) + +--- +-- Stretch the drawable to the bottom of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch_down.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +-- @name stretch_down +-- @class function +-- +-- @usage +--placement.stretch_down(client.focus) + +--- Maximize a drawable horizontally, vertically or both. +-- Valid args: +-- +-- * *axis*:The axis (vertical or horizontal). If none is +-- specified, then the drawable will be maximized on both axis. +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_maximize.svg) +-- +-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args The arguments +-- @treturn table The new geometry +function placement.maximize(d, args) + args = add_context(args, "maximize") + d = d or capi.client.focus + + if not d then return end + + local sgeo = get_parent_geometry(d, args) + local ngeo = geometry_common(d, args, nil, true) + local bw = d.border_width or 0 + + if (not args.axis) or args.axis :match "vertical" then + ngeo.y = sgeo.y + ngeo.height = sgeo.height - 2*bw + end + + if (not args.axis) or args.axis :match "horizontal" then + ngeo.x = sgeo.x + ngeo.width = sgeo.width - 2*bw + end + + geometry_common(d, args, ngeo) + + attach(d, placement.maximize, args) + + return fix_new_geometry(ngeo, args, true) +end + +-- Add the alias functions +for _, v in ipairs {"vertically", "horizontally"} do + placement["maximize_"..v] = function(d2, args) + args = add_context(args, "maximize_"..v) + args.axis = v + return placement_private.maximize(d2, args) + end +end + +--- +-- Vetically maximize the drawable in the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_maximize_vertically.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @name maximize_vertically +-- @class function +-- +-- @usage +--placement.maximize_vertically(c) + +--- +-- Horizontally maximize the drawable in the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_maximize_horizontally.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @name maximize_horizontally +-- @class function +-- +-- @usage +--placement.maximize_horizontally(c) + +--- Scale the drawable by either a relative or absolute percent. +-- +-- Valid args: +-- +-- **to_percent** : A number between 0 and 1. It represent a percent related to +-- the parent geometry. +-- **by_percent** : A number between 0 and 1. It represent a percent related to +-- the current size. +-- **direction**: Nothing or "left", "right", "up", "down". +-- +-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args The arguments +-- @treturn table The new geometry +function placement.scale(d, args) + args = add_context(args, "scale_to_percent") + d = d or capi.client.focus + + local to_percent = args.to_percent + local by_percent = args.by_percent + + local percent = to_percent or by_percent + + local direction = args.direction + + local sgeo = get_parent_geometry(d, args) + local ngeo = geometry_common(d, args, nil) + + local old_area = {width = ngeo.width, height = ngeo.height} + + if (not direction) or direction == "left" or direction == "right" then + ngeo.width = (to_percent and sgeo or ngeo).width*percent + + if direction == "left" then + ngeo.x = ngeo.x - (ngeo.width - old_area.width) + end + end + + if (not direction) or direction == "up" or direction == "down" then + ngeo.height = (to_percent and sgeo or ngeo).height*percent + + if direction == "up" then + ngeo.y = ngeo.y - (ngeo.height - old_area.height) + end + end + + local bw = d.border_width or 0 + ngeo.width = ngeo.width - 2*bw + ngeo.height = ngeo.height - 2*bw + + geometry_common(d, args, ngeo) + + attach(d, placement.maximize, args) + + return fix_new_geometry(ngeo, args, true) +end + +--- Move a drawable to a relative position next to another one. +-- +-- The `args.preferred_positions` look like this: +-- +-- {"top", "right", "left", "bottom"} +-- +-- In that case, if there is room on the top of the geomtry, then it will have +-- priority, followed by all the others, in order. +-- +-- @tparam drawable d A wibox or client +-- @tparam table args +-- @tparam string args.mode The mode +-- @tparam string args.preferred_positions The preferred positions (in order) +-- @tparam string args.geometry A geometry inside the other drawable +-- @treturn table The new geometry +-- @treturn string The choosen position +-- @treturn string The choosen direction +function placement.next_to(d, args) + args = add_context(args, "next_to") + d = d or capi.client.focus + + local preferred_positions = {} + + if #(args.preferred_positions or {}) then + for k, v in ipairs(args.preferred_positions) do + preferred_positions[v] = k + end + end + + local dgeo = geometry_common(d, args) + local pref_idx, pref_name = 99, nil + local mode,wgeo = args.mode + + if args.geometry then + mode = "geometry" + wgeo = args.geometry + else + local pos = capi.mouse.current_widget_geometry + + if pos then + wgeo, mode = pos, "cursor" + elseif capi.mouse.current_client then + wgeo, mode = capi.mouse.current_client:geometry(), "cursor" + end + end + + if not wgeo then return end + + -- See get_relative_regions comments + local is_absolute = wgeo.ontop ~= nil + + local regions = get_relative_regions(wgeo, mode, is_absolute) + + -- Check each possible slot around the drawable (8 total), see what fits + -- and order them by preferred_positions + local does_fit = {} + for k,v in pairs(regions) do + local geo, dir = outer_positions[k.."1"](v, dgeo.width, dgeo.height) + geo.width, geo.height = dgeo.width, dgeo.height + local fit = fit_in_bounding(v.screen, geo, args) + + -- Try the other compatible geometry + if not fit then + geo, dir = outer_positions[k.."2"](v, dgeo.width, dgeo.height) + geo.width, geo.height = dgeo.width, dgeo.height + fit = fit_in_bounding(v.screen, geo, args) + end + + does_fit[k] = fit and {geo, dir} or nil + + if fit and preferred_positions[k] and preferred_positions[k] < pref_idx then + pref_idx = preferred_positions[k] + pref_name = k + end + + -- No need to continue + if fit and preferred_positions[k] == 1 then break end + end + + local pos_name = pref_name or next(does_fit) + local ngeo, dir = unpack(does_fit[pos_name] or {}) --FIXME why does this happen + + geometry_common(d, args, ngeo) + + attach(d, placement.next_to, args) + + return fix_new_geometry(ngeo, args, true), pos_name, dir +end + +--- Restore the geometry. +-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args The arguments +-- @treturn boolean If the geometry was restored +function placement.restore(d, args) + if not args or not args.context then return false end + d = d or capi.client.focus + + if not data[d] then return false end + + local memento = data[d][args.context] + + if not memento then return false end + + memento.screen = nil --TODO use it + + d.border_width = memento.border_width + + d:geometry(memento) + return true +end + +return placement + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/prompt.lua b/lib/awful/prompt.lua new file mode 100644 index 0000000..9a86475 --- /dev/null +++ b/lib/awful/prompt.lua @@ -0,0 +1,777 @@ +--------------------------------------------------------------------------- +--- Prompt module for awful. +-- +-- By default, `rc.lua` will create one `awful.widget.prompt` per screen called +-- `mypromptbox`. It is used for both the command execution (`mod4+r`) and +-- Lua prompt (`mod4+x`). It can be re-used for random inputs using: +-- +-- -- Create a shortcut function +-- local function echo_test() +-- awful.prompt.run { +-- prompt = "Echo: ", +-- textbox = mouse.screen.mypromptbox.widget, +-- exe_callback = function(input) +-- if not input or #input == 0 then return end +-- naughty.notify{ text = "The input was: "..input } +-- end +-- } +-- end +-- +-- -- Then **IN THE globalkeys TABLE** add a new shortcut +-- awful.key({ modkey }, "e", echo_test, +-- {description = "Echo a string", group = "custom"}), +-- +-- Note that this assumes an `rc.lua` file based on the default one. The way +-- to access the screen prompt may vary. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module awful.prompt +--------------------------------------------------------------------------- + +-- Grab environment we need +local assert = assert +local io = io +local table = table +local math = math +local ipairs = ipairs +local pcall = pcall +local capi = +{ + selection = selection +} +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local keygrabber = require("awful.keygrabber") +local util = require("awful.util") +local beautiful = require("beautiful") +local akey = require("awful.key") + +local prompt = {} + +--- Private data +local data = {} +data.history = {} + +local search_term = nil +local function itera (inc,a, i) + i = i + inc + local v = a[i] + if v then return i,v end +end + +--- Load history file in history table +-- @param id The data.history identifier which is the path to the filename. +-- @param[opt] max The maximum number of entries in file. +local function history_check_load(id, max) + if id and id ~= "" and not data.history[id] then + data.history[id] = { max = 50, table = {} } + + if max then + data.history[id].max = max + end + + local f = io.open(id, "r") + if not f then return end + + -- Read history file + for line in f:lines() do + if util.table.hasitem(data.history[id].table, line) == nil then + table.insert(data.history[id].table, line) + if #data.history[id].table >= data.history[id].max then + break + end + end + end + f:close() + end +end + +local function is_word_char(c) + if string.find("[{[(,.:;_-+=@/ ]", c) then + return false + else + return true + end +end + +local function cword_start(s, pos) + local i = pos + if i > 1 then + i = i - 1 + end + while i >= 1 and not is_word_char(s:sub(i, i)) do + i = i - 1 + end + while i >= 1 and is_word_char(s:sub(i, i)) do + i = i - 1 + end + if i <= #s then + i = i + 1 + end + return i +end + +local function cword_end(s, pos) + local i = pos + while i <= #s and not is_word_char(s:sub(i, i)) do + i = i + 1 + end + while i <= #s and is_word_char(s:sub(i, i)) do + i = i + 1 + end + return i +end + +--- Save history table in history file +-- @param id The data.history identifier +local function history_save(id) + if data.history[id] then + local f = io.open(id, "w") + if not f then + local i = 0 + for d in id:gmatch(".-/") do + i = i + #d + end + util.mkdir(id:sub(1, i - 1)) + f = assert(io.open(id, "w")) + end + for i = 1, math.min(#data.history[id].table, data.history[id].max) do + f:write(data.history[id].table[i] .. "\n") + end + f:close() + end +end + +--- Return the number of items in history table regarding the id +-- @param id The data.history identifier +-- @return the number of items in history table, -1 if history is disabled +local function history_items(id) + if data.history[id] then + return #data.history[id].table + else + return -1 + end +end + +--- Add an entry to the history file +-- @param id The data.history identifier +-- @param command The command to add +local function history_add(id, command) + if data.history[id] and command ~= "" then + local index = util.table.hasitem(data.history[id].table, command) + if index == nil then + table.insert(data.history[id].table, command) + + -- Do not exceed our max_cmd + if #data.history[id].table > data.history[id].max then + table.remove(data.history[id].table, 1) + end + + history_save(id) + else + -- Bump this command to the end of history + table.remove(data.history[id].table, index) + table.insert(data.history[id].table, command) + history_save(id) + end + end +end + + +--- Draw the prompt text with a cursor. +-- @tparam table args The table of arguments. +-- @field text The text. +-- @field font The font. +-- @field prompt The text prefix. +-- @field text_color The text color. +-- @field cursor_color The cursor color. +-- @field cursor_pos The cursor position. +-- @field cursor_ul The cursor underline style. +-- @field selectall If true cursor is rendered on the entire text. +local function prompt_text_with_cursor(args) + local char, spacer, text_start, text_end, ret + local text = args.text or "" + local _prompt = args.prompt or "" + local underline = args.cursor_ul or "none" + + if args.selectall then + if #text == 0 then char = " " else char = util.escape(text) end + spacer = " " + text_start = "" + text_end = "" + elseif #text < args.cursor_pos then + char = " " + spacer = "" + text_start = util.escape(text) + text_end = "" + else + char = util.escape(text:sub(args.cursor_pos, args.cursor_pos)) + spacer = " " + text_start = util.escape(text:sub(1, args.cursor_pos - 1)) + text_end = util.escape(text:sub(args.cursor_pos + 1)) + end + + local cursor_color = util.ensure_pango_color(args.cursor_color) + local text_color = util.ensure_pango_color(args.text_color) + + ret = _prompt .. text_start .. "" .. char .. "" .. text_end .. spacer + return ret +end + +--- Run a prompt in a box. +-- +-- The following readline keyboard shortcuts are implemented as expected: +-- CTRL+A, CTRL+B, CTRL+C, CTRL+D, +-- CTRL+E, CTRL+J, CTRL+M, CTRL+F, +-- CTRL+H, CTRL+K, CTRL+U, CTRL+W, +-- CTRL+BACKSPACE, SHIFT+INSERT, HOME, +-- END and arrow keys. +-- +-- The following shortcuts implement additional history manipulation commands +-- where the search term is defined as the substring of the command from first +-- character to cursor position. +-- +-- * CTRL+R: reverse history search, matches any history entry +-- containing search term. +-- * CTRL+S: forward history search, matches any history entry +-- containing search term. +-- * CTRL+UP: ZSH up line or search, matches any history entry +-- starting with search term. +-- * CTRL+DOWN: ZSH down line or search, matches any history +-- entry starting with search term. +-- * CTRL+DELETE: delete the currently visible history entry from +-- history file. This does not delete new commands or history entries under +-- user editing. +-- +-- @tparam[opt={}] table args A table with optional arguments +-- @tparam[opt] gears.color args.fg_cursor +-- @tparam[opt] gears.color args.bg_cursor +-- @tparam[opt] gears.color args.ul_cursor +-- @tparam[opt] widget args.prompt +-- @tparam[opt] string args.text +-- @tparam[opt] boolean args.selectall +-- @tparam[opt] string args.font +-- @tparam[opt] boolean args.autoexec +-- @tparam widget args.textbox The textbox to use for the prompt. +-- @tparam function args.exe_callback The callback function to call with command as argument +-- when finished. +-- @tparam function args.completion_callback The callback function to call to get completion. +-- @tparam[opt] string args.history_path File path where the history should be +-- saved, set nil to disable history +-- @tparam[opt] function args.history_max Set the maximum entries in history +-- file, 50 by default +-- @tparam[opt] function args.done_callback The callback function to always call +-- without arguments, regardless of whether the prompt was cancelled. +-- @tparam[opt] function args.changed_callback The callback function to call +-- with command as argument when a command was changed. +-- @tparam[opt] function args.keypressed_callback The callback function to call +-- with mod table, key and command as arguments when a key was pressed. +-- @tparam[opt] function args.keyreleased_callback The callback function to call +-- with mod table, key and command as arguments when a key was pressed. +-- @tparam[opt] table args.hooks The "hooks" argument uses a syntax similar to +-- `awful.key`. It will call a function for the matching modifiers + key. +-- It receives the command (widget text/input) as an argument. +-- If the callback returns a command, this will be passed to the +-- `exe_callback`, otherwise nothing gets executed by default, and the hook +-- needs to handle it. +-- hooks = { +-- -- Apply startup notification properties with Shift-Return. +-- {{"Shift" }, "Return", function(command) +-- awful.screen.focused().mypromptbox:spawn_and_handle_error( +-- command, {floating=true}) +-- end}, +-- -- Override default behavior of "Return": launch commands prefixed +-- -- with ":" in a terminal. +-- {{}, "Return", function(command) +-- if command:sub(1,1) == ":" then +-- return terminal .. ' -e ' .. command:sub(2) +-- end +-- return command +-- end} +-- } +-- @param textbox The textbox to use for the prompt. [**DEPRECATED**] +-- @param exe_callback The callback function to call with command as argument +-- when finished. [**DEPRECATED**] +-- @param completion_callback The callback function to call to get completion. +-- [**DEPRECATED**] +-- @param[opt] history_path File path where the history should be +-- saved, set nil to disable history [**DEPRECATED**] +-- @param[opt] history_max Set the maximum entries in history +-- file, 50 by default [**DEPRECATED**] +-- @param[opt] done_callback The callback function to always call +-- without arguments, regardless of whether the prompt was cancelled. +-- [**DEPRECATED**] +-- @param[opt] changed_callback The callback function to call +-- with command as argument when a command was changed. [**DEPRECATED**] +-- @param[opt] keypressed_callback The callback function to call +-- with mod table, key and command as arguments when a key was pressed. +-- [**DEPRECATED**] +-- @see gears.color +function prompt.run(args, textbox, exe_callback, completion_callback, + history_path, history_max, done_callback, + changed_callback, keypressed_callback) + local grabber + local theme = beautiful.get() + if not args then args = {} end + local command = args.text or "" + local command_before_comp + local cur_pos_before_comp + local prettyprompt = args.prompt or "" + local inv_col = args.fg_cursor or theme.fg_focus or "black" + local cur_col = args.bg_cursor or theme.bg_focus or "white" + local cur_ul = args.ul_cursor + local text = args.text or "" + local font = args.font or theme.font + local selectall = args.selectall + local hooks = {} + + -- A function with 9 parameters deserve to die + if textbox then + util.deprecate("Use args.textbox instead of the textbox parameter") + end + if exe_callback then + util.deprecate( + "Use args.exe_callback instead of the exe_callback parameter" + ) + end + if completion_callback then + util.deprecate( + "Use args.completion_callback instead of the completion_callback parameter" + ) + end + if history_path then + util.deprecate( + "Use args.history_path instead of the history_path parameter" + ) + end + if history_max then + util.deprecate( + "Use args.history_max instead of the history_max parameter" + ) + end + if done_callback then + util.deprecate( + "Use args.done_callback instead of the done_callback parameter" + ) + end + if changed_callback then + util.deprecate( + "Use args.changed_callback instead of the changed_callback parameter" + ) + end + if keypressed_callback then + util.deprecate( + "Use args.keypressed_callback instead of the keypressed_callback parameter" + ) + end + + -- This function has already an absurd number of parameters, allow them + -- to be set using the args to avoid a "nil, nil, nil, nil, foo" scenario + keypressed_callback = keypressed_callback or args.keypressed_callback + changed_callback = changed_callback or args.changed_callback + done_callback = done_callback or args.done_callback + history_max = history_max or args.history_max + history_path = history_path or args.history_path + completion_callback = completion_callback or args.completion_callback + exe_callback = exe_callback or args.exe_callback + textbox = textbox or args.textbox + + search_term=nil + + history_check_load(history_path, history_max) + local history_index = history_items(history_path) + 1 + -- The cursor position + local cur_pos = (selectall and 1) or text:wlen() + 1 + -- The completion element to use on completion request. + local ncomp = 1 + if not textbox then + return + end + + -- Build the hook map + for _,v in ipairs(args.hooks or {}) do + if #v == 3 then + local _,key,callback = unpack(v) + if type(callback) == "function" then + hooks[key] = hooks[key] or {} + hooks[key][#hooks[key]+1] = v + else + assert("The hook's 3rd parameter has to be a function.") + end + else + assert("The hook has to have 3 parameters.") + end + end + + textbox:set_font(font) + textbox:set_markup(prompt_text_with_cursor{ + text = text, text_color = inv_col, cursor_color = cur_col, + cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, + prompt = prettyprompt }) + + local function exec(cb, command_to_history) + textbox:set_markup("") + history_add(history_path, command_to_history) + keygrabber.stop(grabber) + if cb then cb(command) end + if done_callback then done_callback() end + end + + -- Update textbox + local function update() + textbox:set_font(font) + textbox:set_markup(prompt_text_with_cursor{ + text = command, text_color = inv_col, cursor_color = cur_col, + cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, + prompt = prettyprompt }) + end + + grabber = keygrabber.run( + function (modifiers, key, event) + -- Convert index array to hash table + local mod = {} + for _, v in ipairs(modifiers) do mod[v] = true end + + if event ~= "press" then + if args.keyreleased_callback then + args.keyreleased_callback(mod, key, command) + end + + return + end + + -- Call the user specified callback. If it returns true as + -- the first result then return from the function. Treat the + -- second and third results as a new command and new prompt + -- to be set (if provided) + if keypressed_callback then + local user_catched, new_command, new_prompt = + keypressed_callback(mod, key, command) + if new_command or new_prompt then + if new_command then + command = new_command + end + if new_prompt then + prettyprompt = new_prompt + end + update() + end + if user_catched then + if changed_callback then + changed_callback(command) + end + return + end + end + + local filtered_modifiers = {} + + -- User defined cases + if hooks[key] then + -- Remove caps and num lock + for _, m in ipairs(modifiers) do + if not util.table.hasitem(akey.ignore_modifiers, m) then + table.insert(filtered_modifiers, m) + end + end + + for _,v in ipairs(hooks[key]) do + if #filtered_modifiers == #v[1] then + local match = true + for _,v2 in ipairs(v[1]) do + match = match and mod[v2] + end + if match then + local cb + local ret = v[3](command) + local original_command = command + if ret then + command = ret + cb = exe_callback + else + -- No callback. + cb = function() end + end + exec(cb, original_command) + return + end + end + end + end + + -- Get out cases + if (mod.Control and (key == "c" or key == "g")) + or (not mod.Control and key == "Escape") then + keygrabber.stop(grabber) + textbox:set_markup("") + history_save(history_path) + if done_callback then done_callback() end + return false + elseif (mod.Control and (key == "j" or key == "m")) + or (not mod.Control and key == "Return") + or (not mod.Control and key == "KP_Enter") then + exec(exe_callback, command) + -- We already unregistered ourselves so we don't want to return + -- true, otherwise we may unregister someone else. + return + end + + -- Control cases + if mod.Control then + selectall = nil + if key == "a" then + cur_pos = 1 + elseif key == "b" then + if cur_pos > 1 then + cur_pos = cur_pos - 1 + end + elseif key == "d" then + if cur_pos <= #command then + command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1) + end + elseif key == "p" then + if history_index > 1 then + history_index = history_index - 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + end + elseif key == "n" then + if history_index < history_items(history_path) then + history_index = history_index + 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + elseif history_index == history_items(history_path) then + history_index = history_index + 1 + + command = "" + cur_pos = 1 + end + elseif key == "e" then + cur_pos = #command + 1 + elseif key == "r" then + search_term = search_term or command:sub(1, cur_pos - 1) + for i,v in (function(a,i) return itera(-1,a,i) end), data.history[history_path].table, history_index do + if v:find(search_term,1,true) ~= nil then + command=v + history_index=i + cur_pos=#command+1 + break + end + end + elseif key == "s" then + search_term = search_term or command:sub(1, cur_pos - 1) + for i,v in (function(a,i) return itera(1,a,i) end), data.history[history_path].table, history_index do + if v:find(search_term,1,true) ~= nil then + command=v + history_index=i + cur_pos=#command+1 + break + end + end + elseif key == "f" then + if cur_pos <= #command then + cur_pos = cur_pos + 1 + end + elseif key == "h" then + if cur_pos > 1 then + command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) + cur_pos = cur_pos - 1 + end + elseif key == "k" then + command = command:sub(1, cur_pos - 1) + elseif key == "u" then + command = command:sub(cur_pos, #command) + cur_pos = 1 + elseif key == "Up" then + search_term = command:sub(1, cur_pos - 1) or "" + for i,v in (function(a,i) return itera(-1,a,i) end), data.history[history_path].table, history_index do + if v:find(search_term,1,true) == 1 then + command=v + history_index=i + break + end + end + elseif key == "Down" then + search_term = command:sub(1, cur_pos - 1) or "" + for i,v in (function(a,i) return itera(1,a,i) end), data.history[history_path].table, history_index do + if v:find(search_term,1,true) == 1 then + command=v + history_index=i + break + end + end + elseif key == "w" or key == "BackSpace" then + local wstart = 1 + local wend = 1 + local cword_start_pos = 1 + local cword_end_pos = 1 + while wend < cur_pos do + wend = command:find("[{[(,.:;_-+=@/ ]", wstart) + if not wend then wend = #command + 1 end + if cur_pos >= wstart and cur_pos <= wend + 1 then + cword_start_pos = wstart + cword_end_pos = cur_pos - 1 + break + end + wstart = wend + 1 + end + command = command:sub(1, cword_start_pos - 1) .. command:sub(cword_end_pos + 1) + cur_pos = cword_start_pos + elseif key == "Delete" then + -- delete from history only if: + -- we are not dealing with a new command + -- the user has not edited an existing entry + if command == data.history[history_path].table[history_index] then + table.remove(data.history[history_path].table, history_index) + if history_index <= history_items(history_path) then + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + elseif history_index > 1 then + history_index = history_index - 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + else + command = "" + cur_pos = 1 + end + end + end + elseif mod.Mod1 or mod.Mod3 then + if key == "b" then + cur_pos = cword_start(command, cur_pos) + elseif key == "f" then + cur_pos = cword_end(command, cur_pos) + elseif key == "d" then + command = command:sub(1, cur_pos - 1) .. command:sub(cword_end(command, cur_pos)) + elseif key == "BackSpace" then + local wstart = cword_start(command, cur_pos) + command = command:sub(1, wstart - 1) .. command:sub(cur_pos) + cur_pos = wstart + end + else + if completion_callback then + if key == "Tab" or key == "ISO_Left_Tab" then + if key == "ISO_Left_Tab" then + if ncomp == 1 then return end + if ncomp == 2 then + command = command_before_comp + textbox:set_font(font) + textbox:set_markup(prompt_text_with_cursor{ + text = command_before_comp, text_color = inv_col, cursor_color = cur_col, + cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, + prompt = prettyprompt }) + return + end + + ncomp = ncomp - 2 + elseif ncomp == 1 then + command_before_comp = command + cur_pos_before_comp = cur_pos + end + local matches + command, cur_pos, matches = completion_callback(command_before_comp, cur_pos_before_comp, ncomp) + ncomp = ncomp + 1 + key = "" + -- execute if only one match found and autoexec flag set + if matches and #matches == 1 and args.autoexec then + exec(exe_callback) + return + end + else + ncomp = 1 + end + end + + -- Typin cases + if mod.Shift and key == "Insert" then + local selection = capi.selection() + if selection then + -- Remove \n + local n = selection:find("\n") + if n then + selection = selection:sub(1, n - 1) + end + command = command:sub(1, cur_pos - 1) .. selection .. command:sub(cur_pos) + cur_pos = cur_pos + #selection + end + elseif key == "Home" then + cur_pos = 1 + elseif key == "End" then + cur_pos = #command + 1 + elseif key == "BackSpace" then + if cur_pos > 1 then + command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) + cur_pos = cur_pos - 1 + end + elseif key == "Delete" then + command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1) + elseif key == "Left" then + cur_pos = cur_pos - 1 + elseif key == "Right" then + cur_pos = cur_pos + 1 + elseif key == "Up" then + if history_index > 1 then + history_index = history_index - 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + end + elseif key == "Down" then + if history_index < history_items(history_path) then + history_index = history_index + 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + elseif history_index == history_items(history_path) then + history_index = history_index + 1 + + command = "" + cur_pos = 1 + end + else + -- wlen() is UTF-8 aware but #key is not, + -- so check that we have one UTF-8 char but advance the cursor of # position + if key:wlen() == 1 then + if selectall then command = "" end + command = command:sub(1, cur_pos - 1) .. key .. command:sub(cur_pos) + cur_pos = cur_pos + #key + end + end + if cur_pos < 1 then + cur_pos = 1 + elseif cur_pos > #command + 1 then + cur_pos = #command + 1 + end + selectall = nil + end + + local success = pcall(update) + while not success do + -- TODO UGLY HACK TODO + -- Setting the text failed. Most likely reason is that the user + -- entered a multibyte character and pressed backspace which only + -- removed the last byte. Let's remove another byte. + if cur_pos <= 1 then + -- No text left?! + break + end + + command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) + cur_pos = cur_pos - 1 + success = pcall(update) + end + + if changed_callback then + changed_callback(command) + end + end) +end + +return prompt + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/remote.lua b/lib/awful/remote.lua new file mode 100644 index 0000000..7967f2f --- /dev/null +++ b/lib/awful/remote.lua @@ -0,0 +1,47 @@ +--------------------------------------------------------------------------- +--- Remote control module allowing usage of awesome-client. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @module awful.remote +--------------------------------------------------------------------------- + +-- Grab environment we need +require("awful.dbus") +local load = loadstring or load -- luacheck: globals loadstring (compatibility with Lua 5.1) +local tostring = tostring +local ipairs = ipairs +local table = table +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local dbus = dbus +local type = type + +if dbus then + dbus.connect_signal("org.awesomewm.awful.Remote", function(data, code) + if data.member == "Eval" then + local f, e = load(code) + if f then + local results = { f() } + local retvals = {} + for _, v in ipairs(results) do + local t = type(v) + if t == "boolean" then + table.insert(retvals, "b") + table.insert(retvals, v) + elseif t == "number" then + table.insert(retvals, "d") + table.insert(retvals, v) + else + table.insert(retvals, "s") + table.insert(retvals, tostring(v)) + end + end + return unpack(retvals) + elseif e then + return "s", e + end + end + end) +end + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/rules.lua b/lib/awful/rules.lua new file mode 100644 index 0000000..dc44e00 --- /dev/null +++ b/lib/awful/rules.lua @@ -0,0 +1,545 @@ +--------------------------------------------------------------------------- +--- Apply rules to clients at startup. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @module awful.rules +--------------------------------------------------------------------------- + +-- Grab environment we need +local client = client +local awesome = awesome +local screen = screen +local table = table +local type = type +local ipairs = ipairs +local pairs = pairs +local atag = require("awful.tag") +local util = require("awful.util") +local a_place = require("awful.placement") +local protected_call = require("gears.protected_call") + +local rules = {} + +--[[-- +This is the global rules table. + +You should fill this table with your rule and properties to apply. +For example, if you want to set xterm maximized at startup, you can add: + + { rule = { class = "xterm" }, + properties = { maximized_vertical = true, maximized_horizontal = true } } + +If you want to set mplayer floating at startup, you can add: + + { rule = { name = "MPlayer" }, + properties = { floating = true } } + +If you want to put Firefox on a specific tag at startup, you can add: + + { rule = { instance = "firefox" }, + properties = { tag = mytagobject } } + +Alternatively, you can specify the tag by name: + + { rule = { instance = "firefox" }, + properties = { tag = "3" } } + +If you want to put Thunderbird on a specific screen at startup, use: + + { rule = { instance = "Thunderbird" }, + properties = { screen = 1 } } + +Assuming that your X11 server supports the RandR extension, you can also specify +the screen by name: + + { rule = { instance = "Thunderbird" }, + properties = { screen = "VGA1" } } + +If you want to put Emacs on a specific tag at startup, and immediately switch +to that tag you can add: + + { rule = { class = "Emacs" }, + properties = { tag = mytagobject, switchtotag = true } } + +If you want to apply a custom callback to execute when a rule matched, +for example to pause playing music from mpd when you start dosbox, you +can add: + + { rule = { class = "dosbox" }, + callback = function(c) + awful.spawn('mpc pause') + end } + +Note that all "rule" entries need to match. If any of the entry does not +match, the rule won't be applied. + +If a client matches multiple rules, they are applied in the order they are +put in this global rules table. If the value of a rule is a string, then the +match function is used to determine if the client matches the rule. + +If the value of a property is a function, that function gets called and +function's return value is used for the property. + +To match multiple clients to a rule one need to use slightly different +syntax: + + { rule_any = { class = { "MPlayer", "Nitrogen" }, instance = { "xterm" } }, + properties = { floating = true } } + +To match multiple clients with an exception one can couple `rules.except` or +`rules.except_any` with the rules: + + { rule = { class = "Firefox" }, + except = { instance = "Navigator" }, + properties = {floating = true}, + }, + + { rule_any = { class = { "Pidgin", "Xchat" } }, + except_any = { role = { "conversation" } }, + properties = { tag = "1" } + } + + { rule = {}, + except_any = { class = { "Firefox", "Vim" } }, + properties = { floating = true } + } +]]-- +rules.rules = {} + +--- Check if a client matches a rule. +-- @client c The client. +-- @tab rule The rule to check. +-- @treturn bool True if it matches, false otherwise. +function rules.match(c, rule) + if not rule then return false end + for field, value in pairs(rule) do + if c[field] then + if type(c[field]) == "string" then + if not c[field]:match(value) and c[field] ~= value then + return false + end + elseif c[field] ~= value then + return false + end + else + return false + end + end + return true +end + +--- Check if a client matches any part of a rule. +-- @client c The client. +-- @tab rule The rule to check. +-- @treturn bool True if at least one rule is matched, false otherwise. +function rules.match_any(c, rule) + if not rule then return false end + for field, values in pairs(rule) do + if c[field] then + for _, value in ipairs(values) do + if c[field] == value then + return true + elseif type(c[field]) == "string" and c[field]:match(value) then + return true + end + end + end + end + return false +end + +--- Does a given rule entry match a client? +-- @client c The client. +-- @tab entry Rule entry (with keys `rule`, `rule_any`, `except` and/or +-- `except_any`). +-- @treturn bool +function rules.matches(c, entry) + return (rules.match(c, entry.rule) or rules.match_any(c, entry.rule_any)) and + (not rules.match(c, entry.except) and not rules.match_any(c, entry.except_any)) +end + +--- Get list of matching rules for a client. +-- @client c The client. +-- @tab _rules The rules to check. List with "rule", "rule_any", "except" and +-- "except_any" keys. +-- @treturn table The list of matched rules. +function rules.matching_rules(c, _rules) + local result = {} + for _, entry in ipairs(_rules) do + if (rules.matches(c, entry)) then + table.insert(result, entry) + end + end + return result +end + +--- Check if a client matches a given set of rules. +-- @client c The client. +-- @tab _rules The rules to check. List of tables with `rule`, `rule_any`, +-- `except` and `except_any` keys. +-- @treturn bool True if at least one rule is matched, false otherwise. +function rules.matches_list(c, _rules) + for _, entry in ipairs(_rules) do + if (rules.matches(c, entry)) then + return true + end + end + return false +end + +--- Apply awful.rules.rules to a client. +-- @client c The client. +function rules.apply(c) + + local props = {} + local callbacks = {} + + for _, entry in ipairs(rules.matching_rules(c, rules.rules)) do + if entry.properties then + for property, value in pairs(entry.properties) do + props[property] = value + end + end + if entry.callback then + table.insert(callbacks, entry.callback) + end + end + + rules.execute(c, props, callbacks) +end + +local function add_to_tag(c, t) + if not t then return end + + local tags = c:tags() + table.insert(tags, t) + c:tags(tags) +end + +--- Extra rules properties. +-- +-- These properties are used in the rules only and are not sent to the client +-- afterward. +-- +-- To add a new properties, just do: +-- +-- function awful.rules.extra_properties.my_new_property(c, value, props) +-- -- do something +-- end +-- +-- By default, the table has the following functions: +-- +-- * geometry +-- * switchtotag +-- +-- @tfield table awful.rules.extra_properties +rules.extra_properties = {} + +--- Extra high priority properties. +-- +-- Some properties, such as anything related to tags, geometry or focus, will +-- cause a race condition if set in the main property section. This is why +-- they have a section for them. +-- +-- To add a new properties, just do: +-- +-- function awful.rules.high_priority_properties.my_new_property(c, value, props) +-- -- do something +-- end +-- +-- By default, the table has the following functions: +-- +-- * tag +-- * new_tag +-- +-- @tfield table awful.rules.high_priority_properties +rules.high_priority_properties = {} + +--- Delayed properties. +-- Properties applied after all other categories. +-- @tfield table awful.rules.delayed_properties +rules.delayed_properties = {} + +local force_ignore = { + titlebars_enabled=true, focus=true, screen=true, x=true, + y=true, width=true, height=true, geometry=true,placement=true, + border_width=true,floating=true,size_hints_honor=true +} + +function rules.high_priority_properties.tag(c, value, props) + if value then + if type(value) == "string" then + value = atag.find_by_name(c.screen, value) + end + + -- In case the tag has been forced to another screen, move the client + if c.screen ~= value.screen then + c.screen = value.screen + props.screen = value.screen -- In case another rule query it + end + + c:tags{ value } + end +end + +function rules.delayed_properties.switchtotag(c, value) + if not value then return end + + local selected_tags = {} + + for _,v in ipairs(c.screen.selected_tags) do + selected_tags[v] = true + end + + local tags = c:tags() + + for _, t in ipairs(tags) do + t.selected = true + selected_tags[t] = nil + end + + for t in pairs(selected_tags) do + t.selected = false + end +end + +function rules.extra_properties.geometry(c, _, props) + local cur_geo = c:geometry() + + local new_geo = type(props.geometry) == "function" + and props.geometry(c, props) or props.geometry or {} + + for _, v in ipairs {"x", "y", "width", "height"} do + new_geo[v] = type(props[v]) == "function" and props[v](c, props) + or props[v] or new_geo[v] or cur_geo[v] + end + + c:geometry(new_geo) --TODO use request::geometry +end + +--- Create a new tag based on a rule. +-- @tparam client c The client +-- @tparam boolean|function|string value The value. +-- @tparam table props The properties. +-- @treturn tag The new tag +function rules.high_priority_properties.new_tag(c, value, props) + local ty = type(value) + local t = nil + + if ty == "boolean" then + -- Create a new tag named after the client class + t = atag.add(c.class or "N/A", {screen=c.screen, volatile=true}) + elseif ty == "string" then + -- Create a tag named after "value" + t = atag.add(value, {screen=c.screen, volatile=true}) + elseif ty == "table" then + -- Assume a table of tags properties. Set the right screen, but + -- avoid editing the original table + local values = value.screen and value or util.table.clone(value) + values.screen = values.screen or c.screen + + t = atag.add(value.name or c.class or "N/A", values) + + -- In case the tag has been forced to another screen, move the client + c.screen = t.screen + props.screen = t.screen -- In case another rule query it + else + assert(false) + end + + add_to_tag(c, t) + + return t +end + +function rules.extra_properties.placement(c, value) + -- Avoid problems + if awesome.startup and + (c.size_hints.user_position or c.size_hints.program_position) then + return + end + + local ty = type(value) + + local args = { + honor_workarea = true, + honor_padding = true + } + + if ty == "function" or (ty == "table" and + getmetatable(value) and getmetatable(value).__call + ) then + value(c, args) + elseif ty == "string" and a_place[value] then + a_place[value](c, args) + end +end + +function rules.extra_properties.tags(c, value, props) + local current = c:tags() + + local tags, s = {}, nil + + for _, t in ipairs(value) do + if type(t) == "string" then + t = atag.find_by_name(c.screen, t) + end + + if t and ((not s) or t.screen == s) then + table.insert(tags, t) + s = s or t.screen + end + end + + if s and s ~= c.screen then + c.screen = s + props.screen = s -- In case another rule query it + end + + if #current == 0 or (value[1] and value[1].screen ~= current[1].screen) then + c:tags(tags) + else + c:tags(util.table.merge(current, tags)) + end +end + +--- Apply properties and callbacks to a client. +-- @client c The client. +-- @tab props Properties to apply. +-- @tab[opt] callbacks Callbacks to apply. +function rules.execute(c, props, callbacks) + -- This has to be done first, as it will impact geometry related props. + if props.titlebars_enabled then + c:emit_signal("request::titlebars", "rules", {properties=props}) + end + + -- Border width will also cause geometry related properties to fail + if props.border_width then + c.border_width = type(props.border_width) == "function" and + props.border_width(c, props) or props.border_width + end + + -- Size hints will be re-applied when setting width/height unless it is + -- disabled first + if props.size_hints_honor ~= nil then + c.size_hints_honor = type(props.size_hints_honor) == "function" and props.size_hints_honor(c,props) + or props.size_hints_honor + end + + -- Geometry will only work if floating is true, otherwise the "saved" + -- geometry will be restored. + if props.floating ~= nil then + c.floating = type(props.floating) == "function" and props.floating(c,props) + or props.floating + end + + -- Before requesting a tag, make sure the screen is right + if props.screen then + c.screen = type(props.screen) == "function" and screen[props.screen(c,props)] + or screen[props.screen] + end + + -- Some properties need to be handled first. For example, many properties + -- depend that the client is tagged, this isn't yet the case. + for prop, handler in pairs(rules.high_priority_properties) do + local value = props[prop] + + if value ~= nil then + if type(value) == "function" then + value = value(c, props) + end + + handler(c, value, props) + end + + end + + -- By default, rc.lua use no_overlap+no_offscreen placement. This has to + -- be executed before x/y/width/height/geometry as it would otherwise + -- always override the user specified position with the default rule. + if props.placement then + -- It may be a function, so this one doesn't execute it like others + rules.extra_properties.placement(c, props.placement, props) + end + + -- Make sure the tag is selected before the main rules are called. + -- Otherwise properties like "urgent" or "focus" may fail because they + -- will be overiden by various callbacks. + -- Previously, this was done in a second client.manage callback, but caused + -- a race condition where the order the require() would change the output. + c:emit_signal("request::tag", nil, {reason="rules"}) + + -- By default, rc.lua use no_overlap+no_offscreen placement. This has to + -- be executed before x/y/width/height/geometry as it would otherwise + -- always override the user specified position with the default rule. + if props.placement then + -- It may be a function, so this one doesn't execute it like others + rules.extra_properties.placement(c, props.placement, props) + end + + -- Now that the tags and screen are set, handle the geometry + if props.height or props.width or props.x or props.y or props.geometry then + rules.extra_properties.geometry(c, nil, props) + end + + -- As most race conditions should now have been avoided, apply the remaining + -- properties. + for property, value in pairs(props) do + if property ~= "focus" and type(value) == "function" then + value = value(c, props) + end + + local ignore = rules.high_priority_properties[property] or + rules.delayed_properties[property] or force_ignore[property] + + if not ignore then + if rules.extra_properties[property] then + rules.extra_properties[property](c, value, props) + elseif type(c[property]) == "function" then + c[property](c, value) + else + c[property] = value + end + end + end + + -- Apply all callbacks. + if callbacks then + for _, callback in pairs(callbacks) do + protected_call(callback, c) + end + end + + -- Apply the delayed properties + for prop, handler in pairs(rules.delayed_properties) do + if not force_ignore[prop] then + local value = props[prop] + + if value ~= nil then + if type(value) == "function" then + value = value(c, props) + end + + handler(c, value, props) + end + end + end + + -- Do this at last so we do not erase things done by the focus signal. + if props.focus and (type(props.focus) ~= "function" or props.focus(c)) then + c:emit_signal('request::activate', "rules", {raise=true}) + end +end + +function rules.completed_with_payload_callback(c, props, callbacks) + rules.execute(c, props, callbacks) +end + +client.connect_signal("spawn::completed_with_payload", rules.completed_with_payload_callback) + +client.connect_signal("manage", rules.apply) + +return rules + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/screen.lua b/lib/awful/screen.lua new file mode 100644 index 0000000..e36e622 --- /dev/null +++ b/lib/awful/screen.lua @@ -0,0 +1,477 @@ +--------------------------------------------------------------------------- +--- Screen module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module screen +--------------------------------------------------------------------------- + +-- Grab environment we need +local capi = +{ + mouse = mouse, + screen = screen, + client = client, + awesome = awesome, +} +local util = require("awful.util") +local object = require("gears.object") +local grect = require("gears.geometry").rectangle + +local function get_screen(s) + return s and capi.screen[s] +end + +-- we use require("awful.client") inside functions to prevent circular dependencies. +local client + +local screen = {object={}} + +local data = {} +data.padding = {} + +--- Take an input geometry and substract/add a delta. +-- @tparam table geo A geometry (width, height, x, y) table. +-- @tparam table delta A delta table (top, bottom, x, y). +-- @treturn table A geometry (width, height, x, y) table. +local function apply_geometry_ajustments(geo, delta) + return { + x = geo.x + (delta.left or 0), + y = geo.y + (delta.top or 0), + width = geo.width - (delta.left or 0) - (delta.right or 0), + height = geo.height - (delta.top or 0) - (delta.bottom or 0), + } +end + +--- Get the square distance between a `screen` and a point. +-- @deprecated awful.screen.getdistance_sq +-- @param s Screen +-- @param x X coordinate of point +-- @param y Y coordinate of point +-- @return The squared distance of the screen to the provided point. +-- @see screen.get_square_distance +function screen.getdistance_sq(s, x, y) + util.deprecate("Use s:get_square_distance(x, y) instead of awful.screen.getdistance_sq") + return screen.object.get_square_distance(s, x, y) +end + +--- Get the square distance between a `screen` and a point. +-- @function screen.get_square_distance +-- @tparam number x X coordinate of point +-- @tparam number y Y coordinate of point +-- @treturn number The squared distance of the screen to the provided point. +function screen.object.get_square_distance(self, x, y) + return grect.get_square_distance(get_screen(self).geometry, x, y) +end + +--- Return the screen index corresponding to the given (pixel) coordinates. +-- +-- The number returned can be used as an index into the global +-- `screen` table/object. +-- @function awful.screen.getbycoord +-- @tparam number x The x coordinate +-- @tparam number y The y coordinate +-- @treturn ?number The screen index +function screen.getbycoord(x, y) + local s, sgeos = capi.screen.primary, {} + for scr in capi.screen do + sgeos[scr] = scr.geometry + end + s = grect.get_closest_by_coord(sgeos, x, y) or s + return s and s.index +end + +--- Move the focus to a screen. +-- +-- This moves the mouse pointer to the last known position on the new screen, +-- or keeps its position relative to the current focused screen. +-- @function awful.screen.focus +-- @screen _screen Screen number (defaults / falls back to mouse.screen). +function screen.focus(_screen) + client = client or require("awful.client") + if type(_screen) == "number" and _screen > capi.screen.count() then _screen = screen.focused() end + _screen = get_screen(_screen) + + -- screen and pos for current screen + local s = get_screen(capi.mouse.screen) + local pos + + if not _screen.mouse_per_screen then + -- This is the first time we enter this screen, + -- keep relative mouse position on the new screen. + pos = capi.mouse.coords() + local relx = (pos.x - s.geometry.x) / s.geometry.width + local rely = (pos.y - s.geometry.y) / s.geometry.height + + pos.x = _screen.geometry.x + relx * _screen.geometry.width + pos.y = _screen.geometry.y + rely * _screen.geometry.height + else + -- restore mouse position + pos = _screen.mouse_per_screen + end + + -- save pointer position of current screen + s.mouse_per_screen = capi.mouse.coords() + + -- move cursor without triggering signals mouse::enter and mouse::leave + capi.mouse.coords(pos, true) + + local c = client.focus.history.get(_screen, 0) + if c then + c:emit_signal("request::activate", "screen.focus", {raise=false}) + end +end + +--- Move the focus to a screen in a specific direction. +-- +-- This moves the mouse pointer to the last known position on the new screen, +-- or keeps its position relative to the current focused screen. +-- @function awful.screen.focus_bydirection +-- @param dir The direction, can be either "up", "down", "left" or "right". +-- @param _screen Screen. +function screen.focus_bydirection(dir, _screen) + local sel = get_screen(_screen or screen.focused()) + if sel then + local geomtbl = {} + for s in capi.screen do + geomtbl[s] = s.geometry + end + local target = grect.get_in_direction(dir, geomtbl, sel.geometry) + if target then + return screen.focus(target) + end + end +end + +--- Move the focus to a screen relative to the current one, +-- +-- This moves the mouse pointer to the last known position on the new screen, +-- or keeps its position relative to the current focused screen. +-- +-- @function awful.screen.focus_relative +-- @tparam int offset Value to add to the current focused screen index. 1 to +-- focus the next one, -1 to focus the previous one. +function screen.focus_relative(offset) + return screen.focus(util.cycle(capi.screen.count(), + screen.focused().index + offset)) +end + +--- Get or set the screen padding. +-- +-- @deprecated awful.screen.padding +-- @param _screen The screen object to change the padding on +-- @param[opt=nil] padding The padding, a table with 'top', 'left', 'right' and/or +-- 'bottom' or a number value to apply set the same padding on all sides. Can be +-- nil if you only want to retrieve padding +-- @treturn table A table with left, right, top and bottom number values. +-- @see padding +function screen.padding(_screen, padding) + util.deprecate("Use _screen.padding = value instead of awful.screen.padding") + if padding then + screen.object.set_padding(_screen, padding) + end + return screen.object.get_padding(_screen) +end + +--- The screen padding. +-- +-- This adds a "buffer" section on each side of the screen. +-- +-- **Signal:** +-- +-- * *property::padding* +-- +-- @property padding +-- @param table +-- @tfield integer table.left The padding on the left. +-- @tfield integer table.right The padding on the right. +-- @tfield integer table.top The padding on the top. +-- @tfield integer table.bottom The padding on the bottom. + +function screen.object.get_padding(self) + local p = data.padding[self] or {} + -- Create a copy to avoid accidental mutation and nil values. + return { + left = p.left or 0, + right = p.right or 0, + top = p.top or 0, + bottom = p.bottom or 0, + } +end + +function screen.object.set_padding(self, padding) + if type(padding) == "number" then + padding = { + left = padding, + right = padding, + top = padding, + bottom = padding, + } + end + + self = get_screen(self) + if padding then + data.padding[self] = padding + self:emit_signal("padding") + end +end + +--- Get the preferred screen in the context of a client. +-- +-- This is exactly the same as `awful.screen.focused` except that it avoids +-- clients being moved when Awesome is restarted. +-- This is used in the default `rc.lua` to ensure clients get assigned to the +-- focused screen by default. +-- @tparam client c A client. +-- @treturn screen The preferred screen. +function screen.preferred(c) + return capi.awesome.startup and c.screen or screen.focused() +end + +--- The defaults arguments for `awful.screen.focused`. +-- @tfield[opt=nil] table awful.screen.default_focused_args + +--- Get the focused screen. +-- +-- It is possible to set `awful.screen.default_focused_args` to override the +-- default settings. +-- +-- @function awful.screen.focused +-- @tparam[opt] table args +-- @tparam[opt=false] boolean args.client Use the client screen instead of the +-- mouse screen. +-- @tparam[opt=true] boolean args.mouse Use the mouse screen +-- @treturn ?screen The focused screen object, or `nil` in case no screen is +-- present currently. +function screen.focused(args) + args = args or screen.default_focused_args or {} + return get_screen( + args.client and capi.client.focus and capi.client.focus.screen or capi.mouse.screen + ) +end + +--- Get a placement bounding geometry. +-- +-- This method computes the different variants of the "usable" screen geometry. +-- +-- @function screen.get_bounding_geometry +-- @tparam[opt={}] table args The arguments +-- @tparam[opt=false] boolean args.honor_padding Whether to honor the screen's padding. +-- @tparam[opt=false] boolean args.honor_workarea Whether to honor the screen's workarea. +-- @tparam[opt] int|table args.margins Apply some margins on the output. +-- This can either be a number or a table with *left*, *right*, *top* +-- and *bottom* keys. +-- @tag[opt] args.tag Use this tag's screen. +-- @tparam[opt] drawable args.parent A parent drawable to use as base geometry. +-- @tab[opt] args.bounding_rect A bounding rectangle. This parameter is +-- incompatible with `honor_workarea`. +-- @treturn table A table with *x*, *y*, *width* and *height*. +-- @usage local geo = screen:get_bounding_geometry { +-- honor_padding = true, +-- honor_workarea = true, +-- margins = { +-- left = 20, +-- }, +-- } +function screen.object.get_bounding_geometry(self, args) + args = args or {} + + -- If the tag has a geometry, assume it is right + if args.tag then + self = args.tag.screen + end + + self = get_screen(self or capi.mouse.screen) + + local geo = args.bounding_rect or (args.parent and args.parent:geometry()) or + self[args.honor_workarea and "workarea" or "geometry"] + + if (not args.parent) and (not args.bounding_rect) and args.honor_padding then + local padding = self.padding + geo = apply_geometry_ajustments(geo, padding) + end + + if args.margins then + geo = apply_geometry_ajustments(geo, + type(args.margins) == "table" and args.margins or { + left = args.margins, right = args.margins, + top = args.margins, bottom = args.margins, + } + ) + end + return geo +end + +--- Get the list of visible clients for the screen. +-- +-- Minimized and unmanaged clients are not included in this list as they are +-- technically not on the screen. +-- +-- The clients on tags that are currently not visible are not part of this list. +-- +-- @property clients +-- @param table The clients list, ordered from top to bottom. +-- @see all_clients +-- @see hidden_clients +-- @see client.get + +function screen.object.get_clients(s) + local cls = capi.client.get(s, true) + local vcls = {} + for _, c in pairs(cls) do + if c:isvisible() then + table.insert(vcls, c) + end + end + return vcls +end + +--- Get the list of clients assigned to the screen but not currently visible. +-- +-- This includes minimized clients and clients on hidden tags. +-- +-- @property hidden_clients +-- @param table The clients list, ordered from top to bottom. +-- @see clients +-- @see all_clients +-- @see client.get + +function screen.object.get_hidden_clients(s) + local cls = capi.client.get(s, true) + local vcls = {} + for _, c in pairs(cls) do + if not c:isvisible() then + table.insert(vcls, c) + end + end + return vcls +end + +--- Get all clients assigned to the screen. +-- +-- @property all_clients +-- @param table The clients list, ordered from top to bottom. +-- @see clients +-- @see hidden_clients +-- @see client.get + +function screen.object.get_all_clients(s) + return capi.client.get(s, true) +end + +--- Get the list of tiled clients for the screen. +-- +-- Same as `clients`, but excluding: +-- +-- * fullscreen clients +-- * maximized clients +-- * floating clients +-- +-- @property tiled_clients +-- @param table The clients list, ordered from top to bottom. + +function screen.object.get_tiled_clients(s) + local clients = s.clients + local tclients = {} + -- Remove floating clients + for _, c in pairs(clients) do + if not c.floating + and not c.fullscreen + and not c.maximized_vertical + and not c.maximized_horizontal then + table.insert(tclients, c) + end + end + return tclients +end + +--- Call a function for each existing and created-in-the-future screen. +-- +-- @function awful.screen.connect_for_each_screen +-- @tparam function func The function to call. +-- @screen func.screen The screen. +function screen.connect_for_each_screen(func) + for s in capi.screen do + func(s) + end + capi.screen.connect_signal("added", func) +end + +--- Undo the effect of connect_for_each_screen. +-- @function awful.screen.disconnect_for_each_screen +-- @tparam function func The function that should no longer be called. +function screen.disconnect_for_each_screen(func) + capi.screen.disconnect_signal("added", func) +end + +--- A list of all tags on the screen. +-- +-- This property is read only, use `tag.screen`, `awful.tag.add`, +-- `awful.tag.new` or `t:delete()` to alter this list. +-- +-- @property tags +-- @param table +-- @treturn table A table with all available tags. + +function screen.object.get_tags(s, unordered) + local tags = {} + + for _, t in ipairs(root.tags()) do + if get_screen(t.screen) == s then + table.insert(tags, t) + end + end + + -- Avoid infinite loop and save some time. + if not unordered then + table.sort(tags, function(a, b) + return (a.index or math.huge) < (b.index or math.huge) + end) + end + return tags +end + +--- A list of all selected tags on the screen. +-- @property selected_tags +-- @param table +-- @treturn table A table with all selected tags. +-- @see tag.selected +-- @see client.to_selected_tags + +function screen.object.get_selected_tags(s) + local tags = screen.object.get_tags(s, true) + + local vtags = {} + for _, t in pairs(tags) do + if t.selected then + vtags[#vtags + 1] = t + end + end + return vtags +end + +--- The first selected tag. +-- @property selected_tag +-- @param table +-- @treturn ?tag The first selected tag or nil. +-- @see tag.selected +-- @see selected_tags + +function screen.object.get_selected_tag(s) + return screen.object.get_selected_tags(s)[1] +end + + +--- When the tag history changed. +-- @signal tag::history::update + +-- Extend the luaobject +object.properties(capi.screen, { + getter_class = screen.object, + setter_class = screen.object, + auto_emit = true, +}) + +return screen + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/spawn.lua b/lib/awful/spawn.lua new file mode 100644 index 0000000..bcfb68d --- /dev/null +++ b/lib/awful/spawn.lua @@ -0,0 +1,421 @@ +--------------------------------------------------------------------------- +--- Spawning of programs. +-- +-- This module provides methods to start programs and supports startup +-- notifications, which allows for callbacks and applying properties to the +-- program after it has been launched. This requires currently that the +-- applicaton supports them. +-- +-- **Rules of thumb when a shell is needed**: +-- +-- * A shell is required when the commands contain `&&`, `;`, `||`, `&` or +-- any other unix shell language syntax +-- * When shell variables are defined as part of the command +-- * When the command is a shell alias +-- +-- Note that a shell is **not** a terminal emulator. A terminal emulator is +-- something like XTerm, Gnome-terminal or Konsole. A shell is something like +-- `bash`, `zsh`, `busybox sh` or `Debian ash`. +-- +-- If you wish to open a process in a terminal window, check that your terminal +-- emulator supports the common `-e` option. If it does, then something like +-- this should work: +-- +-- awful.spawn(terminal.." -e my_command") +-- +-- Note that some terminals, such as rxvt-unicode (urxvt) support full commands +-- using quotes, while other terminal emulators require to use quoting. +-- +-- **Understanding clients versus PID versus commands versus class**: +-- +-- A *process* has a *PID* (process identifier). It can have 0, 1 or many +-- *window*s. +-- +-- A *command* if what is used to start *process*(es). It has no direct relation +-- with *process*, *client* or *window*. When a command is executed, it will +-- usually start a *process* which keeps running until it exits. This however is +-- not always the case as some applications use scripts as command and others +-- use various single-instance mechanisms (usually client/server) and merge +-- with an existing process. +-- +-- A *client* corresponds to a *window*. It is owned by a process. It can have +-- both a parent and one or many children. A *client* has a *class*, an +-- *instance*, a *role*, and a *type*. See `client.class`, `client.instance`, +-- `client.role` and `client.type` for more information about these properties. +-- +-- **The startup notification protocol**: +-- +-- The startup notification protocol is an optional specification implemented +-- by X11 applications to bridge the chain of knowledge between the moment a +-- program is launched to the moment its window (client) is shown. It can be +-- found [on the FreeDesktop.org website](https://www.freedesktop.org/wiki/Specifications/startup-notification-spec/). +-- +-- Awesome has support for the various events that are part of the protocol, but +-- the most useful is the identifier, usually identified by its `SNID` acronym in +-- the documentation. It isn't usually necessary to even know it exists, as it +-- is all done automatically. However, if more control is required, the +-- identifier can be specified by an environment variable called +-- `DESKTOP_STARTUP_ID`. For example, let us consider execution of the following +-- command: +-- +-- DESKTOP_STARTUP_ID="something_TIME$(date '+%s')" my_command +-- +-- This should (if the program correctly implements the protocol) result in +-- `c.startup_id` to at least match `something`. +-- This identifier can then be used in `awful.rules` to configure the client. +-- +-- Awesome can automatically set the `DESKTOP_STARTUP_ID` variable. This is used +-- by `awful.spawn` to specify additional rules for the startup. For example: +-- +-- awful.spawn("urxvt -e maxima -name CALCULATOR", { +-- floating = true, +-- tag = mouse.screen.selected_tag, +-- placement = awful.placement.bottom_right, +-- }) +-- +-- This can also be used from the command line: +-- +-- awesome-client 'awful=require("awful"); +-- awful.spawn("urxvt -e maxima -name CALCULATOR", { +-- floating = true, +-- tag = mouse.screen.selected_tag, +-- placement = awful.placement.bottom_right, +-- })' +-- +-- **Getting a command's output**: +-- +-- First, do **not** use `io.popen` **ever**. It is synchronous. Synchronous +-- functions **block everything** until they are done. All visual applications +-- lock (as Awesome no longer responds), you will probably lose some keyboard +-- and mouse events and will have higher latency when playing games. This is +-- also true when reading files synchronously, but this is another topic. +-- +-- Awesome provides a few ways to get output from commands. One is to use the +-- `Gio` libraries directly. This is usually very complicated, but gives a lot +-- of control on the command execution. +-- +-- This modules provides `with_line_callback` and `easy_async` for convenience. +-- First, lets add this bash command to `rc.lua`: +-- +-- local noisy = [[bash -c ' +-- for I in $(seq 1 5); do +-- date +-- echo err >&2 +-- sleep 2 +-- done +-- ']] +-- +-- It prints a bunch of junk on the standard output (*stdout*) and error +-- (*stderr*) streams. This command would block Awesome for 10 seconds if it +-- were executed synchronously, but will not block it at all using the +-- asynchronous functions. +-- +-- `with_line_callback` will execute the callbacks every time a new line is +-- printed by the command: +-- +-- awful.spawn.with_line_callback(noisy, { +-- stdout = function(line) +-- naughty.notify { text = "LINE:"..line } +-- end, +-- stderr = function(line) +-- naughty.notify { text = "ERR:"..line} +-- end, +-- }) +-- +-- If only the full output is needed, then `easy_async` is the right choice: +-- +-- awful.spawn.easy_async(noisy, function(stdout, stderr, reason, exit_code) +-- naughty.notify { text = stdout } +-- end) +-- +-- **Default applications**: +-- +-- If the intent is to open a file/document, then it is recommended to use the +-- following standard command. The default application will be selected +-- according to the [Shared MIME-info Database](https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html) +-- specification. The `xdg-utils` package provided by most distributions +-- includes the `xdg-open` command: +-- +-- awful.spawn({"xdg-open", "/path/to/file"}) +-- +-- Awesome **does not** manage, modify or otherwise influence the database +-- for default applications. For information about how to do this, consult the +-- [ARCH Linux Wiki](https://wiki.archlinux.org/index.php/default_applications). +-- +-- If you wish to change how the default applications behave, then consult the +-- [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html) +-- specification. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2008 Julien Danjou +-- @copyright 2014 Emmanuel Lepage Vallee +-- @module awful.spawn +--------------------------------------------------------------------------- + +local capi = +{ + awesome = awesome, + mouse = mouse, + client = client, +} +local lgi = require("lgi") +local Gio = lgi.Gio +local GLib = lgi.GLib +local util = require("awful.util") +local protected_call = require("gears.protected_call") + +local spawn = {} + + +local end_of_file +do + -- API changes, bug fixes and lots of fun. Figure out how a EOF is signalled. + local input + if not pcall(function() + -- No idea when this API changed, but some versions expect a string, + -- others a table with some special(?) entries + input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data("")) + end) then + input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data({})) + end + local line, length = input:read_line() + if not line then + -- Fixed in 2016: NULL on the C side is transformed to nil in Lua + end_of_file = function(arg) + return not arg + end + elseif tostring(line) == "" and #line ~= length then + -- "Historic" behaviour for end-of-file: + -- - NULL is turned into an empty string + -- - The length variable is not initialized + -- It's highly unlikely that the uninitialized variable has value zero. + -- Use this hack to detect EOF. + end_of_file = function(arg1, arg2) + return #arg1 ~= arg2 + end + else + assert(tostring(line) == "", "Cannot determine how to detect EOF") + -- The above uninitialized variable was fixed and thus length is + -- always 0 when line is NULL in C. We cannot tell apart an empty line and + -- EOF in this case. + require("gears.debug").print_warning("Cannot reliably detect EOF on an " + .. "GIOInputStream with this LGI version") + end_of_file = function(arg) + return tostring(arg) == "" + end + end +end + +spawn.snid_buffer = {} + +function spawn.on_snid_callback(c) + local entry = spawn.snid_buffer[c.startup_id] + if entry then + local props = entry[1] + local callback = entry[2] + c:emit_signal("spawn::completed_with_payload", props, callback) + spawn.snid_buffer[c.startup_id] = nil + end +end + +function spawn.on_snid_cancel(id) + if spawn.snid_buffer[id] then + spawn.snid_buffer[id] = nil + end +end + +--- Spawn a program, and optionally apply properties and/or run a callback. +-- +-- Applying properties or running a callback requires the program/client to +-- support startup notifications. +-- +-- See `awful.rules.execute` for more details about the format of `sn_rules`. +-- +-- @tparam string|table cmd The command. +-- @tparam[opt=true] table|boolean sn_rules A table of properties to be applied +-- after startup; `false` to disable startup notifications. +-- @tparam[opt] function callback A callback function to be run after startup. +-- @treturn[1] integer The forked PID. +-- @treturn[1] ?string The startup notification ID, if `sn` is not false, or +-- a `callback` is provided. +-- @treturn[2] string Error message. +function spawn.spawn(cmd, sn_rules, callback) + if cmd and cmd ~= "" then + local enable_sn = (sn_rules ~= false or callback) + enable_sn = not not enable_sn -- Force into a boolean. + local pid, snid = capi.awesome.spawn(cmd, enable_sn) + -- The snid will be nil in case of failure + if snid then + sn_rules = type(sn_rules) ~= "boolean" and sn_rules or {} + spawn.snid_buffer[snid] = { sn_rules, { callback } } + end + return pid, snid + end + -- For consistency + return "Error: No command to execute" +end + +--- Spawn a program using the shell. +-- This calls `cmd` with `$SHELL -c` (via `awful.util.shell`). +-- @tparam string cmd The command. +function spawn.with_shell(cmd) + if cmd and cmd ~= "" then + cmd = { util.shell, "-c", cmd } + return capi.awesome.spawn(cmd, false) + end +end + +--- Spawn a program and asynchronously capture its output line by line. +-- @tparam string|table cmd The command. +-- @tab callbacks Table containing callbacks that should be invoked on +-- various conditions. +-- @tparam[opt] function callbacks.stdout Function that is called with each +-- line of output on stdout, e.g. `stdout(line)`. +-- @tparam[opt] function callbacks.stderr Function that is called with each +-- line of output on stderr, e.g. `stderr(line)`. +-- @tparam[opt] function callbacks.output_done Function to call when no more +-- output is produced. +-- @tparam[opt] function callbacks.exit Function to call when the spawned +-- process exits. This function gets the exit reason and code as its +-- arguments. +-- The reason can be "exit" or "signal". +-- For "exit", the second argument is the exit code. +-- For "signal", the second argument is the signal causing process +-- termination. +-- @treturn[1] Integer the PID of the forked process. +-- @treturn[2] string Error message. +function spawn.with_line_callback(cmd, callbacks) + local stdout_callback, stderr_callback, done_callback, exit_callback = + callbacks.stdout, callbacks.stderr, callbacks.output_done, callbacks.exit + local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil + local pid, _, stdin, stdout, stderr = capi.awesome.spawn(cmd, + false, false, have_stdout, have_stderr, exit_callback) + if type(pid) == "string" then + -- Error + return pid + end + + local done_before = false + local function step_done() + if have_stdout and have_stderr and not done_before then + done_before = true + return + end + if done_callback then + done_callback() + end + end + if have_stdout then + spawn.read_lines(Gio.UnixInputStream.new(stdout, true), + stdout_callback, step_done, true) + end + if have_stderr then + spawn.read_lines(Gio.UnixInputStream.new(stderr, true), + stderr_callback, step_done, true) + end + assert(stdin == nil) + return pid +end + +--- Asynchronously spawn a program and capture its output. +-- (wraps `spawn.with_line_callback`). +-- @tparam string|table cmd The command. +-- @tab callback Function with the following arguments +-- @tparam string callback.stdout Output on stdout. +-- @tparam string callback.stderr Output on stderr. +-- @tparam string callback.exitreason Exit Reason. +-- The reason can be "exit" or "signal". +-- @tparam integer callback.exitcode Exit code. +-- For "exit" reason it's the exit code. +-- For "signal" reason — the signal causing process termination. +-- @treturn[1] Integer the PID of the forked process. +-- @treturn[2] string Error message. +-- @see spawn.with_line_callback +function spawn.easy_async(cmd, callback) + local stdout = '' + local stderr = '' + local exitcode, exitreason + local function parse_stdout(str) + stdout = stdout .. str .. "\n" + end + local function parse_stderr(str) + stderr = stderr .. str .. "\n" + end + local function done_callback() + return callback(stdout, stderr, exitreason, exitcode) + end + local exit_callback_fired = false + local output_done_callback_fired = false + local function exit_callback(reason, code) + exitcode = code + exitreason = reason + exit_callback_fired = true + if output_done_callback_fired then + return done_callback() + end + end + local function output_done_callback() + output_done_callback_fired = true + if exit_callback_fired then + return done_callback() + end + end + return spawn.with_line_callback( + cmd, { + stdout=parse_stdout, + stderr=parse_stderr, + exit=exit_callback, + output_done=output_done_callback + }) +end + +--- Read lines from a Gio input stream +-- @tparam Gio.InputStream input_stream The input stream to read from. +-- @tparam function line_callback Function that is called with each line +-- read, e.g. `line_callback(line_from_stream)`. +-- @tparam[opt] function done_callback Function that is called when the +-- operation finishes (e.g. due to end of file). +-- @tparam[opt=false] boolean close Should the stream be closed after end-of-file? +function spawn.read_lines(input_stream, line_callback, done_callback, close) + local stream = Gio.DataInputStream.new(input_stream) + local function done() + if close then + stream:close() + end + if done_callback then + protected_call(done_callback) + end + end + local start_read, finish_read + start_read = function() + stream:read_line_async(GLib.PRIORITY_DEFAULT, nil, finish_read) + end + finish_read = function(obj, res) + local line, length = obj:read_line_finish(res) + if type(length) ~= "number" then + -- Error + print("Error in awful.spawn.read_lines:", tostring(length)) + done() + elseif end_of_file(line, length) then + -- End of file + done() + else + -- Read a line + -- This needs tostring() for older lgi versions which returned + -- "GLib.Bytes" instead of Lua strings (I guess) + protected_call(line_callback, tostring(line)) + + -- Read the next line + start_read() + end + end + start_read() +end + +capi.awesome.connect_signal("spawn::canceled" , spawn.on_snid_cancel ) +capi.awesome.connect_signal("spawn::timeout" , spawn.on_snid_cancel ) +capi.client.connect_signal ("manage" , spawn.on_snid_callback ) + +return setmetatable(spawn, { __call = function(_, ...) return spawn.spawn(...) end }) +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/startup_notification.lua b/lib/awful/startup_notification.lua new file mode 100644 index 0000000..5f1c123 --- /dev/null +++ b/lib/awful/startup_notification.lua @@ -0,0 +1,53 @@ +--------------------------------------------------------------------------- +--- Startup notification module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @module awful.startup_notification +--------------------------------------------------------------------------- + +-- Grab environment we need +local ipairs = ipairs +local table = table +local capi = +{ + awesome = awesome, + root = root +} + +local app_starting = {} + +local cursor_waiting = "watch" + +local function update_cursor() + if #app_starting > 0 then + capi.root.cursor(cursor_waiting) + else + capi.root.cursor("left_ptr") + end +end + +local function unregister_event(event_id) + for k, v in ipairs(app_starting) do + if v == event_id then + table.remove(app_starting, k) + update_cursor() + break + end + end +end + +local function register_event(event_id) + table.insert(app_starting, event_id) + update_cursor() +end + +local function unregister_hook(event) unregister_event(event.id) end +local function register_hook(event) register_event(event.id) end + +capi.awesome.connect_signal("spawn::initiated", register_hook) +capi.awesome.connect_signal("spawn::canceled", unregister_hook) +capi.awesome.connect_signal("spawn::completed", unregister_hook) +capi.awesome.connect_signal("spawn::timeout", unregister_hook) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/tag.lua b/lib/awful/tag.lua new file mode 100644 index 0000000..dbf3a60 --- /dev/null +++ b/lib/awful/tag.lua @@ -0,0 +1,1505 @@ +--------------------------------------------------------------------------- +--- Useful functions for tag manipulation. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module tag +--------------------------------------------------------------------------- + +-- Grab environment we need +local util = require("awful.util") +local ascreen = require("awful.screen") +local beautiful = require("beautiful") +local object = require("gears.object") +local timer = require("gears.timer") +local pairs = pairs +local ipairs = ipairs +local table = table +local setmetatable = setmetatable +local capi = +{ + tag = tag, + screen = screen, + mouse = mouse, + client = client, + root = root +} + +local function get_screen(s) + return s and capi.screen[s] +end + +local tag = {object = {}, mt = {} } + +-- Private data +local data = {} +data.history = {} + +-- History functions +tag.history = {} +tag.history.limit = 20 + +-- Default values +local defaults = {} + +-- The gap between clients (in points). +defaults.gap = 0 + +-- The default gap_count. +defaults.gap_single_client = true + +-- The default master fill policy. +defaults.master_fill_policy = "expand" + +-- The default master width factor. +defaults.master_width_factor = 0.5 + +-- The default master count. +defaults.master_count = 1 + +-- The default column count. +defaults.column_count = 1 + +-- screen.tags depend on index, it cannot be used by awful.tag +local function raw_tags(scr) + local tmp_tags = {} + for _, t in ipairs(root.tags()) do + if get_screen(t.screen) == scr then + table.insert(tmp_tags, t) + end + end + + return tmp_tags +end + +--- The number of elements kept in the history. +-- @tfield integer awful.tag.history.limit +-- @tparam[opt=20] integer limit + +--- The tag index. +-- +-- The index is the position as shown in the `awful.widget.taglist`. +-- +-- **Signal:** +-- +-- * *property::index* +-- +-- @property index +-- @param integer +-- @treturn number The tag index. + +function tag.object.set_index(self, idx) + local scr = get_screen(tag.getproperty(self, "screen")) + + -- screen.tags cannot be used as it depend on index + local tmp_tags = raw_tags(scr) + + -- sort the tags by index + table.sort(tmp_tags, function(a, b) + local ia, ib = tag.getproperty(a, "index"), tag.getproperty(b, "index") + return (ia or math.huge) < (ib or math.huge) + end) + + if (not idx) or (idx < 1) or (idx > #tmp_tags) then + return + end + + local rm_index = nil + + for i, t in ipairs(tmp_tags) do + if t == self then + table.remove(tmp_tags, i) + rm_index = i + break + end + end + + table.insert(tmp_tags, idx, self) + for i = idx < rm_index and idx or rm_index, #tmp_tags do + local tmp_tag = tmp_tags[i] + tag.object.set_screen(tmp_tag, scr) + tag.setproperty(tmp_tag, "index", i) + end +end + +function tag.object.get_index(query_tag) + + local idx = tag.getproperty(query_tag, "index") + + if idx then return idx end + + -- Get an unordered list of tags + local tags = raw_tags(query_tag.screen) + + -- Too bad, lets compute it + for i, t in ipairs(tags) do + if t == query_tag then + tag.setproperty(t, "index", i) + return i + end + end +end + +--- Move a tag to an absolute position in the screen[]:tags() table. +-- @deprecated awful.tag.move +-- @param new_index Integer absolute position in the table to insert. +-- @param target_tag The tag that should be moved. If null, the currently +-- selected tag is used. +-- @see index +function tag.move(new_index, target_tag) + util.deprecate("Use t.index = new_index instead of awful.tag.move") + + target_tag = target_tag or ascreen.focused().selected_tag + tag.object.set_index(target_tag, new_index) +end + +--- Swap 2 tags +-- @function tag.swap +-- @param tag2 The second tag +-- @see client.swap +function tag.object.swap(self, tag2) + local idx1, idx2 = tag.object.get_index(self), tag.object.get_index(tag2) + local scr2, scr1 = tag.getproperty(tag2, "screen"), tag.getproperty(self, "screen") + + -- If they are on the same screen, avoid recomputing the whole table + -- for nothing. + if scr1 == scr2 then + tag.setproperty(self, "index", idx2) + tag.setproperty(tag2, "index", idx1) + else + tag.object.set_screen(tag2, scr1) + tag.object.set_index (tag2, idx1) + tag.object.set_screen(self, scr2) + tag.object.set_index (self, idx2) + end +end + +--- Swap 2 tags +-- @deprecated awful.tag.swap +-- @see tag.swap +-- @param tag1 The first tag +-- @param tag2 The second tag +function tag.swap(tag1, tag2) + util.deprecate("Use t:swap(tag2) instead of awful.tag.swap") + + tag.object.swap(tag1, tag2) +end + +--- Add a tag. +-- +-- This function allow to create tags from a set of properties: +-- +-- local t = awful.tag.add("my new tag", { +-- screen = screen.primary, +-- layout = awful.layout.suit.max, +-- }) +-- +-- @function awful.tag.add +-- @param name The tag name, a string +-- @param props The tags inital properties, a table +-- @return The created tag +-- @see tag.delete +function tag.add(name, props) + local properties = props or {} + + -- Be sure to set the screen before the tag is activated to avoid function + -- connected to property::activated to be called without a valid tag. + -- set properties cannot be used as this has to be set before the first + -- signal is sent + properties.screen = get_screen(properties.screen or ascreen.focused()) + -- Index is also required + properties.index = properties.index or #raw_tags(properties.screen)+1 + + local newtag = capi.tag{ name = name } + + -- Start with a fresh property table to avoid collisions with unsupported data + newtag.data.awful_tag_properties = {screen=properties.screen, index=properties.index} + + newtag.activated = true + + for k, v in pairs(properties) do + -- `rawget` doesn't work on userdata, `:clients()` is the only relevant + -- entry. + if k == "clients" or tag.object[k] then + newtag[k](newtag, v) + else + newtag[k] = v + end + end + + return newtag +end + +--- Create a set of tags and attach it to a screen. +-- @function awful.tag.new +-- @param names The tag name, in a table +-- @param screen The tag screen, or 1 if not set. +-- @param layout The layout or layout table to set for this tags by default. +-- @return A table with all created tags. +function tag.new(names, screen, layout) + screen = get_screen(screen or 1) + local tags = {} + for id, name in ipairs(names) do + table.insert(tags, id, tag.add(name, {screen = screen, + layout = (layout and layout[id]) or + layout})) + -- Select the first tag. + if id == 1 then + tags[id].selected = true + end + end + + return tags +end + +--- Find a suitable fallback tag. +-- @function awful.tag.find_fallback +-- @param screen The screen to look for a tag on. [awful.screen.focused()] +-- @param invalids A table of tags we consider unacceptable. [selectedlist(scr)] +function tag.find_fallback(screen, invalids) + local scr = screen or ascreen.focused() + local t = invalids or scr.selected_tags + + for _, v in pairs(scr.tags) do + if not util.table.hasitem(t, v) then return v end + end +end + +--- Delete a tag. +-- +-- To delete the current tag: +-- +-- mouse.screen.selected_tag:delete() +-- +-- @function tag.delete +-- @see awful.tag.add +-- @see awful.tag.find_fallback +-- @tparam[opt=awful.tag.find_fallback()] tag fallback_tag Tag to assign +-- stickied tags to. +-- @tparam[opt=false] boolean force Move even non-sticky clients to the fallback +-- tag. +-- @return Returns true if the tag is successfully deleted. +-- If there are no clients exclusively on this tag then delete it. Any +-- stickied clients are assigned to the optional 'fallback_tag'. +-- If after deleting the tag there is no selected tag, try and restore from +-- history or select the first tag on the screen. +function tag.object.delete(self, fallback_tag, force) + -- abort if the taf isn't currently activated + if not self.activated then return false end + + local target_scr = get_screen(tag.getproperty(self, "screen")) + local tags = target_scr.tags + local idx = tag.object.get_index(self) + local ntags = #tags + + -- We can't use the target tag as a fallback. + if fallback_tag == self then return false end + + -- No fallback_tag provided, try and get one. + if fallback_tag == nil then + fallback_tag = tag.find_fallback(target_scr, {self}) + end + + -- Abort if we would have un-tagged clients. + local clients = self:clients() + if #clients > 0 and fallback_tag == nil then return false end + + -- Move the clients we can off of this tag. + for _, c in pairs(clients) do + local nb_tags = #c:tags() + + -- If a client has only this tag, or stickied clients with + -- nowhere to go, abort. + if (not c.sticky and nb_tags == 1 and not force) then + return + -- If a client has multiple tags, then do not move it to fallback + elseif nb_tags < 2 then + c:tags({fallback_tag}) + end + end + + -- delete the tag + self.data.awful_tag_properties.screen = nil + self.activated = false + + -- Update all indexes + for i=idx+1, #tags do + tag.setproperty(tags[i], "index", i-1) + end + + -- If no tags are visible (and we did not delete the lasttag), try and + -- view one. The > 1 is because ntags is no longer synchronized with the + -- current count. + if target_scr.selected_tag == nil and ntags > 1 then + tag.history.restore(target_scr, 1) + if target_scr.selected_tag == nil then + local other_tag = tags[tags[1] == self and 2 or 1] + if other_tag then + other_tag.selected = true + end + end + end + + return true +end + +--- Delete a tag. +-- @deprecated awful.tag.delete +-- @see tag.delete +-- @param target_tag Optional tag object to delete. [selected()] +-- @param fallback_tag Tag to assign stickied tags to. [~selected()] +-- @return Returns true if the tag is successfully deleted, nil otherwise. +-- If there are no clients exclusively on this tag then delete it. Any +-- stickied clients are assigned to the optional 'fallback_tag'. +-- If after deleting the tag there is no selected tag, try and restore from +-- history or select the first tag on the screen. +function tag.delete(target_tag, fallback_tag) + util.deprecate("Use t:delete(fallback_tag) instead of awful.tag.delete") + + return tag.object.delete(target_tag, fallback_tag) +end + +--- Update the tag history. +-- @function awful.tag.history.update +-- @param obj Screen object. +function tag.history.update(obj) + local s = get_screen(obj) + local curtags = s.selected_tags + -- create history table + if not data.history[s] then + data.history[s] = {} + else + if data.history[s].current then + -- Check that the list is not identical + local identical = #data.history[s].current == #curtags + if identical then + for idx, _tag in ipairs(data.history[s].current) do + if curtags[idx] ~= _tag then + identical = false + break + end + end + end + + -- Do not update history the table are identical + if identical then return end + end + + -- Limit history + if #data.history[s] >= tag.history.limit then + for i = tag.history.limit, #data.history[s] do + data.history[s][i] = nil + end + end + end + + -- store previously selected tags in the history table + table.insert(data.history[s], 1, data.history[s].current) + data.history[s].previous = data.history[s][1] + -- store currently selected tags + data.history[s].current = setmetatable(curtags, { __mode = 'v' }) +end + +--- Revert tag history. +-- @function awful.tag.history.restore +-- @param screen The screen. +-- @param idx Index in history. Defaults to "previous" which is a special index +-- toggling between last two selected sets of tags. Number (eg 1) will go back +-- to the given index in history. +function tag.history.restore(screen, idx) + local s = get_screen(screen or ascreen.focused()) + local i = idx or "previous" + local sel = s.selected_tags + -- do nothing if history empty + if not data.history[s] or not data.history[s][i] then return end + -- if all tags been deleted, try next entry + if #data.history[s][i] == 0 then + if i == "previous" then i = 0 end + tag.history.restore(s, i + 1) + return + end + -- deselect all tags + tag.viewnone(s) + -- select tags from the history entry + for _, t in ipairs(data.history[s][i]) do + if t.activated and t.screen then + t.selected = true + end + end + -- update currently selected tags table + data.history[s].current = data.history[s][i] + -- store previously selected tags + data.history[s].previous = setmetatable(sel, { __mode = 'v' }) + -- remove the reverted history entry + if i ~= "previous" then table.remove(data.history[s], i) end + + s:emit_signal("tag::history::update") +end + +--- Get a list of all tags on a screen +-- @deprecated awful.tag.gettags +-- @tparam screen s Screen +-- @return A table with all available tags +-- @see screen.tags +function tag.gettags(s) + util.deprecate("Use s.tags instead of awful.tag.gettags") + + s = get_screen(s) + + return s and s.tags or {} +end + +--- Find a tag by name +-- @tparam[opt] screen s The screen of the tag +-- @tparam string name The name of the tag +-- @return The tag found, or `nil` +function tag.find_by_name(s, name) + local tags = s and s.tags or root.tags() + for _, t in ipairs(tags) do + if name == t.name then + return t + end + end +end + +--- The tag screen. +-- +-- **Signal:** +-- +-- * *property::screen* +-- +-- @property screen +-- @param screen +-- @see screen + +function tag.object.set_screen(t, s) + + s = get_screen(s or ascreen.focused()) + local sel = tag.selected + local old_screen = get_screen(tag.getproperty(t, "screen")) + + if s == old_screen then return end + + -- Keeping the old index make very little sense when changing screen + tag.setproperty(t, "index", nil) + + -- Change the screen + tag.setproperty(t, "screen", s) + tag.setproperty(t, "index", #s:get_tags(true)) + + -- Make sure the client's screen matches its tags + for _,c in ipairs(t:clients()) do + c.screen = s --Move all clients + c:tags({t}) + end + + -- Update all indexes + for i,t2 in ipairs(old_screen.tags) do + tag.setproperty(t2, "index", i) + end + + -- Restore the old screen history if the tag was selected + if sel then + tag.history.restore(old_screen, 1) + end +end + +--- Set a tag's screen +-- @deprecated awful.tag.setscreen +-- @see screen +-- @param s Screen +-- @param t tag object +function tag.setscreen(s, t) + -- For API consistency, the arguments have been swapped for Awesome 3.6 + -- this method is already deprecated, so be silent and swap the args + if type(t) == "number" then + s, t = t, s + end + + util.deprecate("Use t.screen = s instead of awful.tag.setscreen(t, s)") + + tag.object.set_screen(t, s) +end + +--- Get a tag's screen +-- @deprecated awful.tag.getscreen +-- @see screen +-- @param[opt] t tag object +-- @return Screen number +function tag.getscreen(t) + util.deprecate("Use t.screen instead of awful.tag.getscreen(t)") + + -- A new getter is not required + + t = t or ascreen.focused().selected_tag + local prop = tag.getproperty(t, "screen") + return prop and prop.index +end + +--- Return a table with all visible tags +-- @deprecated awful.tag.selectedlist +-- @param s Screen. +-- @return A table with all selected tags. +-- @see screen.selected_tags +function tag.selectedlist(s) + util.deprecate("Use s.selected_tags instead of awful.tag.selectedlist") + + s = get_screen(s or ascreen.focused()) + + return s.selected_tags +end + +--- Return only the first visible tag. +-- @deprecated awful.tag.selected +-- @param s Screen. +-- @see screen.selected_tag +function tag.selected(s) + util.deprecate("Use s.selected_tag instead of awful.tag.selected") + + s = get_screen(s or ascreen.focused()) + + return s.selected_tag +end + +--- The default master width factor +-- +-- @beautiful beautiful.master_width_factor +-- @param number (default: 0.5) +-- @see master_width_factor +-- @see gap + +--- The tag master width factor. +-- +-- The master width factor is one of the 5 main properties used to configure +-- the `layout`. Each layout interpret (or ignore) this property differenly. +-- +-- See the layout suit documentation for information about how the master width +-- factor is used. +-- +-- **Signal:** +-- +-- * *property::mwfact* (deprecated) +-- * *property::master_width_factor* +-- +-- @property master_width_factor +-- @param number Between 0 and 1 +-- @see master_count +-- @see column_count +-- @see master_fill_policy +-- @see gap + +function tag.object.set_master_width_factor(t, mwfact) + if mwfact >= 0 and mwfact <= 1 then + tag.setproperty(t, "mwfact", mwfact) + tag.setproperty(t, "master_width_factor", mwfact) + end +end + +function tag.object.get_master_width_factor(t) + return tag.getproperty(t, "master_width_factor") + or beautiful.master_width_factor + or defaults.master_width_factor +end + +--- Set master width factor. +-- @deprecated awful.tag.setmwfact +-- @see master_fill_policy +-- @see master_width_factor +-- @param mwfact Master width factor. +-- @param t The tag to modify, if null tag.selected() is used. +function tag.setmwfact(mwfact, t) + util.deprecate("Use t.master_width_factor = mwfact instead of awful.tag.setmwfact") + + tag.object.get_master_width_factor(t or ascreen.focused().selected_tag, mwfact) +end + +--- Increase master width factor. +-- @function awful.tag.incmwfact +-- @see master_width_factor +-- @param add Value to add to master width factor. +-- @param t The tag to modify, if null tag.selected() is used. +function tag.incmwfact(add, t) + t = t or t or ascreen.focused().selected_tag + tag.object.set_master_width_factor(t, tag.object.get_master_width_factor(t) + add) +end + +--- Get master width factor. +-- @deprecated awful.tag.getmwfact +-- @see master_width_factor +-- @see master_fill_policy +-- @param[opt] t The tag. +function tag.getmwfact(t) + util.deprecate("Use t.master_width_factor instead of awful.tag.getmwfact") + + return tag.object.get_master_width_factor(t or ascreen.focused().selected_tag) +end + +--- An ordered list of layouts. +-- `awful.tag.layout` Is usually defined in `rc.lua`. It store the list of +-- layouts used when selecting the previous and next layouts. This is the +-- default: +-- +-- -- Table of layouts to cover with awful.layout.inc, order matters. +-- awful.layout.layouts = { +-- awful.layout.suit.floating, +-- awful.layout.suit.tile, +-- awful.layout.suit.tile.left, +-- awful.layout.suit.tile.bottom, +-- awful.layout.suit.tile.top, +-- awful.layout.suit.fair, +-- awful.layout.suit.fair.horizontal, +-- awful.layout.suit.spiral, +-- awful.layout.suit.spiral.dwindle, +-- awful.layout.suit.max, +-- awful.layout.suit.max.fullscreen, +-- awful.layout.suit.magnifier, +-- awful.layout.suit.corner.nw, +-- -- awful.layout.suit.corner.ne, +-- -- awful.layout.suit.corner.sw, +-- -- awful.layout.suit.corner.se, +-- } +-- +-- @field awful.tag.layouts + +--- The tag client layout. +-- +-- This property hold the layout. A layout can be either stateless or stateful. +-- Stateless layouts are used by default by Awesome. They tile clients without +-- any other overhead. They take an ordered list of clients and place them on +-- the screen. Stateful layouts create an object instance for each tags and +-- can store variables and metadata. Because of this, they are able to change +-- over time and be serialized (saved). +-- +-- Both types of layouts have valid usage scenarios. +-- +-- **Stateless layouts:** +-- +-- These layouts are stored in `awful.layout.suit`. They expose a table with 2 +-- fields: +-- +-- * **name** (*string*): The layout name. This should be unique. +-- * **arrange** (*function*): The function called when the clients need to be +-- placed. The only parameter is a table or arguments returned by +-- `awful.layout.parameters` +-- +-- **Stateful layouts:** +-- +-- The stateful layouts API is the same as stateless, but they are a function +-- returining a layout instead of a layout itself. They also should have an +-- `is_dynamic = true` property. If they don't, `awful.tag` will create a new +-- instance everytime the layout is set. If they do, the instance will be +-- cached and re-used. +-- +-- **Signal:** +-- +-- * *property::layout* +-- +-- @property layout +-- @see awful.tag.layouts +-- @tparam layout|function layout A layout table or a constructor function +-- @return The layout + +function tag.object.set_layout(t, layout) + -- Check if the signature match a stateful layout + if type(layout) == "function" or ( + type(layout) == "table" + and getmetatable(layout) + and getmetatable(layout).__call + ) then + if not t.dynamic_layout_cache then + t.dynamic_layout_cache = {} + end + + local instance = t.dynamic_layout_cache[layout] or layout(t) + + -- Always make sure the layout is notified it is enabled + if tag.getproperty(t, "screen").selected_tag == t and instance.wake_up then + instance:wake_up() + end + + -- Avoid creating the same layout twice, use layout:reset() to reset + if instance.is_dynamic then + t.dynamic_layout_cache[layout] = instance + end + + layout = instance + end + + tag.setproperty(t, "layout", layout) + + return layout +end + +--- Set layout. +-- @deprecated awful.tag.setlayout +-- @see layout +-- @param layout a layout table or a constructor function +-- @param t The tag to modify +-- @return The layout +function tag.setlayout(layout, t) + util.deprecate("Use t.layout = layout instead of awful.tag.setlayout") + + return tag.object.set_layout(t, layout) +end + +--- Define if the tag must be deleted when the last client is untagged. +-- +-- This is useful to create "throw-away" tags for operation like 50/50 +-- side-by-side views. +-- +-- local t = awful.tag.add("Temporary", { +-- screen = client.focus.screen, +-- volatile = true, +-- clients = { +-- client.focus, +-- awful.client.focus.history.get(client.focus.screen, 1) +-- } +-- } +-- +-- **Signal:** +-- +-- * *property::volatile* +-- +-- @property volatile +-- @param boolean + +-- Volatile accessors are implicit + +--- Set if the tag must be deleted when the last client is untagged +-- @deprecated awful.tag.setvolatile +-- @see volatile +-- @tparam boolean volatile If the tag must be deleted when the last client is untagged +-- @param t The tag to modify, if null tag.selected() is used. +function tag.setvolatile(volatile, t) + util.deprecate("Use t.volatile = volatile instead of awful.tag.setvolatile") + + tag.setproperty(t, "volatile", volatile) +end + +--- Get if the tag must be deleted when the last client closes +-- @deprecated awful.tag.getvolatile +-- @see volatile +-- @param t The tag to modify, if null tag.selected() is used. +-- @treturn boolean If the tag will be deleted when the last client is untagged +function tag.getvolatile(t) + util.deprecate("Use t.volatile instead of awful.tag.getvolatile") + + return tag.getproperty(t, "volatile") or false +end + +--- The default gap. +-- +-- @beautiful beautiful.useless_gap +-- @param number (default: 0) +-- @see gap +-- @see gap_single_client + +--- The gap (spacing, also called `useless_gap`) between clients. +-- +-- This property allow to waste space on the screen in the name of style, +-- unicorns and readability. +-- +-- **Signal:** +-- +-- * *property::useless_gap* +-- +-- @property gap +-- @param number The value has to be greater than zero. +-- @see gap_single_client + +function tag.object.set_gap(t, useless_gap) + if useless_gap >= 0 then + tag.setproperty(t, "useless_gap", useless_gap) + end +end + +function tag.object.get_gap(t) + return tag.getproperty(t, "useless_gap") + or beautiful.useless_gap + or defaults.gap +end + +--- Set the spacing between clients +-- @deprecated awful.tag.setgap +-- @see gap +-- @param useless_gap The spacing between clients +-- @param t The tag to modify, if null tag.selected() is used. +function tag.setgap(useless_gap, t) + util.deprecate("Use t.gap = useless_gap instead of awful.tag.setgap") + + tag.object.set_gap(t or ascreen.focused().selected_tag, useless_gap) +end + +--- Increase the spacing between clients +-- @function awful.tag.incgap +-- @see gap +-- @param add Value to add to the spacing between clients +-- @param t The tag to modify, if null tag.selected() is used. +function tag.incgap(add, t) + t = t or t or ascreen.focused().selected_tag + tag.object.set_gap(t, tag.object.get_gap(t) + add) +end + +--- Enable gaps for a single client. +-- +-- @beautiful beautiful.gap_single_client +-- @param boolean (default: true) +-- @see gap +-- @see gap_single_client + +--- Enable gaps for a single client. +-- +-- **Signal:** +-- +-- * *property::gap\_single\_client* +-- +-- @property gap_single_client +-- @param boolean Enable gaps for a single client + +function tag.object.set_gap_single_client(t, gap_single_client) + tag.setproperty(t, "gap_single_client", gap_single_client == true) +end + +function tag.object.get_gap_single_client(t) + local val = tag.getproperty(t, "gap_single_client") + if val ~= nil then + return val + end + val = beautiful.gap_single_client + if val ~= nil then + return val + end + return defaults.gap_single_client +end + +--- Get the spacing between clients. +-- @deprecated awful.tag.getgap +-- @see gap +-- @tparam[opt=tag.selected()] tag t The tag. +-- @tparam[opt] int numclients Number of (tiled) clients. Passing this will +-- return 0 for a single client. You can override this function to change +-- this behavior. +function tag.getgap(t, numclients) + util.deprecate("Use t.gap instead of awful.tag.getgap") + + if numclients == 1 then + return 0 + end + + return tag.object.get_gap(t or ascreen.focused().selected_tag) +end + +--- The default fill policy. +-- +-- ** Possible values**: +-- +-- * *expand*: Take all the space +-- * *master_width_factor*: Only take the ratio defined by the +-- `master_width_factor` +-- +-- @beautiful beautiful.master_fill_policy +-- @param string (default: "expand") +-- @see master_fill_policy + +--- Set size fill policy for the master client(s). +-- +-- ** Possible values**: +-- +-- * *expand*: Take all the space +-- * *master_width_factor*: Only take the ratio defined by the +-- `master_width_factor` +-- +-- **Signal:** +-- +-- * *property::master_fill_policy* +-- +-- @property master_fill_policy +-- @param string "expand" or "master_width_factor" + +function tag.object.get_master_fill_policy(t) + return tag.getproperty(t, "master_fill_policy") + or beautiful.master_fill_policy + or defaults.master_fill_policy +end + +--- Set size fill policy for the master client(s) +-- @deprecated awful.tag.setmfpol +-- @see master_fill_policy +-- @tparam string policy Can be set to +-- "expand" (fill all the available workarea) or +-- "master_width_factor" (fill only an area inside the master width factor) +-- @tparam[opt=tag.selected()] tag t The tag to modify +function tag.setmfpol(policy, t) + util.deprecate("Use t.master_fill_policy = policy instead of awful.tag.setmfpol") + + t = t or ascreen.focused().selected_tag + tag.setproperty(t, "master_fill_policy", policy) +end + +--- Toggle size fill policy for the master client(s) +-- between "expand" and "master_width_factor". +-- @function awful.tag.togglemfpol +-- @see master_fill_policy +-- @tparam tag t The tag to modify, if null tag.selected() is used. +function tag.togglemfpol(t) + t = t or ascreen.focused().selected_tag + + if tag.getmfpol(t) == "expand" then + tag.setproperty(t, "master_fill_policy", "master_width_factor") + else + tag.setproperty(t, "master_fill_policy", "expand") + end +end + +--- Get size fill policy for the master client(s) +-- @deprecated awful.tag.getmfpol +-- @see master_fill_policy +-- @tparam[opt=tag.selected()] tag t The tag +-- @treturn string Possible values are +-- "expand" (fill all the available workarea, default one) or +-- "master_width_factor" (fill only an area inside the master width factor) +function tag.getmfpol(t) + util.deprecate("Use t.master_fill_policy instead of awful.tag.getmfpol") + + t = t or ascreen.focused().selected_tag + return tag.getproperty(t, "master_fill_policy") + or beautiful.master_fill_policy + or defaults.master_fill_policy +end + +--- The default number of master windows. +-- +-- @beautiful beautiful.master_count +-- @param integer (default: 1) +-- @see master_count + +--- Set the number of master windows. +-- +-- **Signal:** +-- +-- * *property::nmaster* (deprecated) +-- * *property::master_count* (deprecated) +-- +-- @property master_count +-- @param integer nmaster Only positive values are accepted + +function tag.object.set_master_count(t, nmaster) + if nmaster >= 0 then + tag.setproperty(t, "nmaster", nmaster) + tag.setproperty(t, "master_count", nmaster) + end +end + +function tag.object.get_master_count(t) + return tag.getproperty(t, "master_count") + or beautiful.master_count + or defaults.master_count +end + +--- +-- @deprecated awful.tag.setnmaster +-- @see master_count +-- @param nmaster The number of master windows. +-- @param[opt] t The tag. +function tag.setnmaster(nmaster, t) + util.deprecate("Use t.master_count = nmaster instead of awful.tag.setnmaster") + + tag.object.set_master_count(t or ascreen.focused().selected_tag, nmaster) +end + +--- Get the number of master windows. +-- @deprecated awful.tag.getnmaster +-- @see master_count +-- @param[opt] t The tag. +function tag.getnmaster(t) + util.deprecate("Use t.master_count instead of awful.tag.setnmaster") + + t = t or ascreen.focused().selected_tag + return tag.getproperty(t, "master_count") or 1 +end + +--- Increase the number of master windows. +-- @function awful.tag.incnmaster +-- @see master_count +-- @param add Value to add to number of master windows. +-- @param[opt] t The tag to modify, if null tag.selected() is used. +-- @tparam[opt=false] boolean sensible Limit nmaster based on the number of +-- visible tiled windows? +function tag.incnmaster(add, t, sensible) + t = t or ascreen.focused().selected_tag + + if sensible then + local screen = get_screen(tag.getproperty(t, "screen")) + local ntiled = #screen.tiled_clients + + local nmaster = tag.object.get_master_count(t) + if nmaster > ntiled then + nmaster = ntiled + end + + local newnmaster = nmaster + add + if newnmaster > ntiled then + newnmaster = ntiled + end + tag.object.set_master_count(t, newnmaster) + else + tag.object.set_master_count(t, tag.object.get_master_count(t) + add) + end +end + +--- Set the tag icon. +-- +-- **Signal:** +-- +-- * *property::icon* +-- +-- @property icon +-- @tparam path|surface icon The icon + +-- accessors are implicit. + +--- Set the tag icon +-- @deprecated awful.tag.seticon +-- @see icon +-- @param icon the icon to set, either path or image object +-- @param _tag the tag +function tag.seticon(icon, _tag) + util.deprecate("Use t.icon = icon instead of awful.tag.seticon") + + _tag = _tag or ascreen.focused().selected_tag + tag.setproperty(_tag, "icon", icon) +end + +--- Get the tag icon +-- @deprecated awful.tag.geticon +-- @see icon +-- @param _tag the tag +function tag.geticon(_tag) + util.deprecate("Use t.icon instead of awful.tag.geticon") + + _tag = _tag or ascreen.focused().selected_tag + return tag.getproperty(_tag, "icon") +end + +--- The default number of columns. +-- +-- @beautiful beautiful.column_count +-- @param integer (default: 1) +-- @see column_count + +--- Set the number of columns. +-- +-- **Signal:** +-- +-- * *property::ncol* (deprecated) +-- * *property::column_count* +-- +-- @property column_count +-- @tparam integer ncol Has to be greater than 1 + +function tag.object.set_column_count(t, ncol) + if ncol >= 1 then + tag.setproperty(t, "ncol", ncol) + tag.setproperty(t, "column_count", ncol) + end +end + +function tag.object.get_column_count(t) + return tag.getproperty(t, "column_count") + or beautiful.column_count + or defaults.column_count +end + +--- Set number of column windows. +-- @deprecated awful.tag.setncol +-- @see column_count +-- @param ncol The number of column. +-- @param t The tag to modify, if null tag.selected() is used. +function tag.setncol(ncol, t) + util.deprecate("Use t.column_count = new_index instead of awful.tag.setncol") + + t = t or ascreen.focused().selected_tag + if ncol >= 1 then + tag.setproperty(t, "ncol", ncol) + tag.setproperty(t, "column_count", ncol) + end +end + +--- Get number of column windows. +-- @deprecated awful.tag.getncol +-- @see column_count +-- @param[opt] t The tag. +function tag.getncol(t) + util.deprecate("Use t.column_count instead of awful.tag.getncol") + + t = t or ascreen.focused().selected_tag + return tag.getproperty(t, "column_count") or 1 +end + +--- Increase number of column windows. +-- @function awful.tag.incncol +-- @param add Value to add to number of column windows. +-- @param[opt] t The tag to modify, if null tag.selected() is used. +-- @tparam[opt=false] boolean sensible Limit column_count based on the number +-- of visible tiled windows? +function tag.incncol(add, t, sensible) + t = t or ascreen.focused().selected_tag + + if sensible then + local screen = get_screen(tag.getproperty(t, "screen")) + local ntiled = #screen.tiled_clients + local nmaster = tag.object.get_master_count(t) + local nsecondary = ntiled - nmaster + + local ncol = tag.object.get_column_count(t) + if ncol > nsecondary then + ncol = nsecondary + end + + local newncol = ncol + add + if newncol > nsecondary then + newncol = nsecondary + end + + tag.object.set_column_count(t, newncol) + else + tag.object.set_column_count(t, tag.object.get_column_count(t) + add) + end +end + +--- View no tag. +-- @function awful.tag.viewnone +-- @tparam[opt] int|screen screen The screen. +function tag.viewnone(screen) + screen = screen or ascreen.focused() + local tags = screen.tags + for _, t in pairs(tags) do + t.selected = false + end +end + +--- View a tag by its taglist index. +-- +-- This is equivalent to `screen.tags[i]:view_only()` +-- @function awful.tag.viewidx +-- @see screen.tags +-- @param i The **relative** index to see. +-- @param[opt] screen The screen. +function tag.viewidx(i, screen) + screen = get_screen(screen or ascreen.focused()) + local tags = screen.tags + local showntags = {} + for _, t in ipairs(tags) do + if not tag.getproperty(t, "hide") then + table.insert(showntags, t) + end + end + local sel = screen.selected_tag + tag.viewnone(screen) + for k, t in ipairs(showntags) do + if t == sel then + showntags[util.cycle(#showntags, k + i)].selected = true + end + end + screen:emit_signal("tag::history::update") +end + +--- Get a tag's index in the gettags() table. +-- @deprecated awful.tag.getidx +-- @see index +-- @param query_tag The tag object to find. [selected()] +-- @return The index of the tag, nil if the tag is not found. +function tag.getidx(query_tag) + util.deprecate("Use t.index instead of awful.tag.getidx") + + return tag.object.get_index(query_tag or ascreen.focused().selected_tag) +end + +--- View next tag. This is the same as tag.viewidx(1). +-- @function awful.tag.viewnext +-- @param screen The screen. +function tag.viewnext(screen) + return tag.viewidx(1, screen) +end + +--- View previous tag. This is the same a tag.viewidx(-1). +-- @function awful.tag.viewprev +-- @param screen The screen. +function tag.viewprev(screen) + return tag.viewidx(-1, screen) +end + +--- View only a tag. +-- @function tag.view_only +-- @see selected +function tag.object.view_only(self) + local tags = self.screen.tags + -- First, untag everyone except the viewed tag. + for _, _tag in pairs(tags) do + if _tag ~= self then + _tag.selected = false + end + end + -- Then, set this one to selected. + -- We need to do that in 2 operations so we avoid flickering and several tag + -- selected at the same time. + self.selected = true + capi.screen[self.screen]:emit_signal("tag::history::update") +end + +--- View only a tag. +-- @deprecated awful.tag.viewonly +-- @see tag.view_only +-- @param t The tag object. +function tag.viewonly(t) + util.deprecate("Use t:view_only() instead of awful.tag.viewonly") + + tag.object.view_only(t) +end + +--- View only a set of tags. +-- @function awful.tag.viewmore +-- @param tags A table with tags to view only. +-- @param[opt] screen The screen of the tags. +function tag.viewmore(tags, screen) + screen = get_screen(screen or ascreen.focused()) + local screen_tags = screen.tags + for _, _tag in ipairs(screen_tags) do + if not util.table.hasitem(tags, _tag) then + _tag.selected = false + end + end + for _, _tag in ipairs(tags) do + _tag.selected = true + end + screen:emit_signal("tag::history::update") +end + +--- Toggle selection of a tag +-- @function awful.tag.viewtoggle +-- @see selected +-- @tparam tag t Tag to be toggled +function tag.viewtoggle(t) + t.selected = not t.selected + capi.screen[tag.getproperty(t, "screen")]:emit_signal("tag::history::update") +end + +--- Get tag data table. +-- +-- Do not use. +-- +-- @deprecated awful.tag.getdata +-- @tparam tag _tag The tag. +-- @return The data table. +function tag.getdata(_tag) + return _tag.data.awful_tag_properties +end + +--- Get a tag property. +-- +-- Use `_tag.prop` directly. +-- +-- @deprecated awful.tag.getproperty +-- @tparam tag _tag The tag. +-- @tparam string prop The property name. +-- @return The property. +function tag.getproperty(_tag, prop) + if not _tag then return end -- FIXME: Turn this into an error? + if _tag.data.awful_tag_properties then + return _tag.data.awful_tag_properties[prop] + end +end + +--- Set a tag property. +-- This properties are internal to awful. Some are used to draw taglist, or to +-- handle layout, etc. +-- +-- Use `_tag.prop = value` +-- +-- @deprecated awful.tag.setproperty +-- @param _tag The tag. +-- @param prop The property name. +-- @param value The value. +function tag.setproperty(_tag, prop, value) + if not _tag.data.awful_tag_properties then + _tag.data.awful_tag_properties = {} + end + + if _tag.data.awful_tag_properties[prop] ~= value then + _tag.data.awful_tag_properties[prop] = value + _tag:emit_signal("property::" .. prop) + end +end + +--- Tag a client with the set of current tags. +-- @deprecated awful.tag.withcurrent +-- @param c The client to tag. +function tag.withcurrent(c) + util.deprecate("Use c:to_selected_tags() instead of awful.tag.selectedlist") + + -- It can't use c:to_selected_tags() because awful.tag is loaded before + -- awful.client + + local tags = {} + for _, t in ipairs(c:tags()) do + if get_screen(tag.getproperty(t, "screen")) == get_screen(c.screen) then + table.insert(tags, t) + end + end + if #tags == 0 then + tags = c.screen.selected_tags + end + if #tags == 0 then + tags = c.screen.tags + end + if #tags ~= 0 then + c:tags(tags) + end +end + +local function attached_connect_signal_screen(screen, sig, func) + screen = get_screen(screen) + capi.tag.connect_signal(sig, function(_tag) + if get_screen(tag.getproperty(_tag, "screen")) == screen then + func(_tag) + end + end) +end + +--- Add a signal to all attached tags and all tags that will be attached in the +-- future. When a tag is detached from the screen, its signal is removed. +-- +-- @function awful.tag.attached_connect_signal +-- @screen The screen concerned, or all if nil. +-- @tparam[opt] string Signal +-- @tparam[opt] function Callback +function tag.attached_connect_signal(screen, ...) + if screen then + attached_connect_signal_screen(screen, ...) + else + capi.tag.connect_signal(...) + end +end + +-- Register standard signals. +capi.client.connect_signal("property::screen", function(c) + -- First, the delayed timer is necessary to avoid a race condition with + -- awful.rules. It is also messing up the tags before the user have a chance + -- to set them manually. + timer.delayed_call(function() + local tags, new_tags = c:tags(), {} + + for _, t in ipairs(tags) do + if t.screen == c.screen then + table.insert(new_tags, t) + end + end + + if #new_tags == 0 then + c:emit_signal("request::tag", nil, {reason="screen"}) + elseif #new_tags < #tags then + c:tags(new_tags) + end + end) +end) + +-- Keep track of the number of urgent clients. +local function update_urgent(t, modif) + local count = tag.getproperty(t, "urgent_count") or 0 + count = (count + modif) >= 0 and (count + modif) or 0 + tag.setproperty(t, "urgent" , count > 0) + tag.setproperty(t, "urgent_count", count ) +end + +-- Update the urgent counter when a client is tagged. +local function client_tagged(c, t) + if c.urgent then + update_urgent(t, 1) + end +end + +-- Update the urgent counter when a client is untagged. +local function client_untagged(c, t) + if c.urgent then + update_urgent(t, -1) + end + + if #t:clients() == 0 and tag.getproperty(t, "volatile") then + tag.object.delete(t) + end +end + +-- Count the urgent clients. +local function urgent_callback(c) + for _,t in ipairs(c:tags()) do + update_urgent(t, c.urgent and 1 or -1) + end +end + +capi.client.connect_signal("property::urgent", urgent_callback) +capi.client.connect_signal("untagged", client_untagged) +capi.client.connect_signal("tagged", client_tagged) +capi.tag.connect_signal("request::select", tag.object.view_only) + +--- True when a tagged client is urgent +-- @signal property::urgent +-- @see client.urgent + +--- The number of urgent tagged clients +-- @signal property::urgent_count +-- @see client.urgent + +capi.screen.connect_signal("tag::history::update", tag.history.update) + +capi.screen.connect_signal("removed", function(s) + -- First give other code a chance to move the tag to another screen + for _, t in pairs(s.tags) do + t:emit_signal("request::screen") + end + -- Everything that's left: Tell everyone that these tags go away (other code + -- could e.g. save clients) + for _, t in pairs(s.tags) do + t:emit_signal("removal-pending") + end + -- Give other code yet another change to save clients + for _, c in pairs(capi.client.get(s)) do + c:emit_signal("request::tag", nil, { reason = "screen-removed" }) + end + -- Then force all clients left to go somewhere random + local fallback = nil + for other_screen in capi.screen do + if #other_screen.tags > 0 then + fallback = other_screen.tags[1] + break + end + end + for _, t in pairs(s.tags) do + t:delete(fallback, true) + end + -- If any tag survived until now, forcefully get rid of it + for _, t in pairs(s.tags) do + t.activated = false + + if t.data.awful_tag_properties then + t.data.awful_tag_properties.screen = nil + end + end +end) + +function tag.mt:__call(...) + return tag.new(...) +end + +-- Extend the luaobject +-- `awful.tag.setproperty` currently handle calling the setter method itself +-- while `awful.tag.getproperty`. +object.properties(capi.tag, { + getter_class = tag.object, + setter_class = tag.object, + getter_fallback = tag.getproperty, + setter_fallback = tag.setproperty, +}) + +return setmetatable(tag, tag.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/titlebar.lua b/lib/awful/titlebar.lua new file mode 100644 index 0000000..c055349 --- /dev/null +++ b/lib/awful/titlebar.lua @@ -0,0 +1,509 @@ +--------------------------------------------------------------------------- +--- Titlebars for awful. +-- +-- @author Uli Schlachter +-- @copyright 2012 Uli Schlachter +-- @module awful.titlebar +--------------------------------------------------------------------------- + +local error = error +local type = type +local util = require("awful.util") +local abutton = require("awful.button") +local aclient = require("awful.client") +local atooltip = require("awful.tooltip") +local beautiful = require("beautiful") +local drawable = require("wibox.drawable") +local imagebox = require("wibox.widget.imagebox") +local textbox = require("wibox.widget.textbox") +local base = require("wibox.widget.base") +local capi = { + client = client +} +local titlebar = { + widget = {} +} + +--- The titlebar foreground (text) color. +-- @beautiful beautiful.titlebar_fg_normal +-- @param color +-- @see gears.color + +--- The titlebar background color. +-- @beautiful beautiful.titlebar_bg_normal +-- @param color +-- @see gears.color + +--- The titlebar background image image. +-- @beautiful beautiful.titlebar_bgimage_normal +-- @param surface +-- @see gears.surface + +--- The titlebar foreground (text) color. +-- @beautiful beautiful.titlebar_fg +-- @param color +-- @see gears.color + +--- The titlebar background color. +-- @beautiful beautiful.titlebar_bg +-- @param color +-- @see gears.color + +--- The titlebar background image image. +-- @beautiful beautiful.titlebar_bgimage +-- @param surface +-- @see gears.surface + +--- The focused titlebar foreground (text) color. +-- @beautiful beautiful.titlebar_fg_focus +-- @param color +-- @see gears.color + +--- The focused titlebar background color. +-- @beautiful beautiful.titlebar_bg_focus +-- @param color +-- @see gears.color + +--- The focused titlebar background image image. +-- @beautiful beautiful.titlebar_bgimage_focus +-- @param surface +-- @see gears.surface + +--- floating_button_normal. +-- @beautiful beautiful.titlebar_floating_button_normal +-- @param surface +-- @see gears.surface + +--- maximized_button_normal. +-- @beautiful beautiful.titlebar_maximized_button_normal +-- @param surface +-- @see gears.surface + +--- minimize_button_normal +-- @beautiful beautiful.titlebar_minimize_button_normal +-- @param surface +-- @see gears.surface + +--- close_button_normal. +-- @beautiful beautiful.titlebar_close_button_normal +-- @param surface +-- @see gears.surface + +--- ontop_button_normal. +-- @beautiful beautiful.titlebar_ontop_button_normal +-- @param surface +-- @see gears.surface + +--- sticky_button_normal. +-- @beautiful beautiful.titlebar_sticky_button_normal +-- @param surface +-- @see gears.surface + +--- floating_button_focus. +-- @beautiful beautiful.titlebar_floating_button_focus +-- @param surface +-- @see gears.surface + +--- maximized_button_focus. +-- @beautiful beautiful.titlebar_maximized_button_focus +-- @param surface +-- @see gears.surface + +--- minimize_button_focus. +-- @beautiful beautiful.titlebar_minimize_button_focus +-- @param surface +-- @see gears.surface + +--- close_button_focus. +-- @beautiful beautiful.titlebar_close_button_focus +-- @param surface +-- @see gears.surface + +--- ontop_button_focus. +-- @beautiful beautiful.titlebar_ontop_button_focus +-- @param surface +-- @see gears.surface + +--- sticky_button_focus. +-- @beautiful beautiful.titlebar_sticky_button_focus +-- @param surface +-- @see gears.surface + +--- floating_button_normal_active. +-- @beautiful beautiful.titlebar_floating_button_normal_active +-- @param surface +-- @see gears.surface + +--- maximized_button_normal_active. +-- @beautiful beautiful.titlebar_maximized_button_normal_active +-- @param surface +-- @see gears.surface + +--- ontop_button_normal_active. +-- @beautiful beautiful.titlebar_ontop_button_normal_active +-- @param surface +-- @see gears.surface + +--- sticky_button_normal_active. +-- @beautiful beautiful.titlebar_sticky_button_normal_active +-- @param surface +-- @see gears.surface + +--- floating_button_focus_active. +-- @beautiful beautiful.titlebar_floating_button_focus_active +-- @param surface +-- @see gears.surface + +--- maximized_button_focus_active. +-- @beautiful beautiful.titlebar_maximized_button_focus_active +-- @param surface +-- @see gears.surface + +--- ontop_button_focus_active. +-- @beautiful beautiful.titlebar_ontop_button_focus_active +-- @param surface +-- @see gears.surface + +--- sticky_button_focus_active. +-- @beautiful beautiful.titlebar_sticky_button_focus_active +-- @param surface +-- @see gears.surface + +--- floating_button_normal_inactive. +-- @beautiful beautiful.titlebar_floating_button_normal_inactive +-- @param surface +-- @see gears.surface + +--- maximized_button_normal_inactive. +-- @beautiful beautiful.titlebar_maximized_button_normal_inactive +-- @param surface +-- @see gears.surface + +--- ontop_button_normal_inactive. +-- @beautiful beautiful.titlebar_ontop_button_normal_inactive +-- @param surface +-- @see gears.surface + +--- sticky_button_normal_inactive. +-- @beautiful beautiful.titlebar_sticky_button_normal_inactive +-- @param surface +-- @see gears.surface + +--- floating_button_focus_inactive. +-- @beautiful beautiful.titlebar_floating_button_focus_inactive +-- @param surface +-- @see gears.surface + +--- maximized_button_focus_inactive. +-- @beautiful beautiful.titlebar_maximized_button_focus_inactive +-- @param surface +-- @see gears.surface + +--- ontop_button_focus_inactive. +-- @beautiful beautiful.titlebar_ontop_button_focus_inactive +-- @param surface +-- @see gears.surface + +--- sticky_button_focus_inactive. +-- @beautiful beautiful.titlebar_sticky_button_focus_inactive +-- @param surface +-- @see gears.surface + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @name setup +-- @class function + +--- Show tooltips when hover on titlebar buttons (defaults to 'true') +titlebar.enable_tooltip = true + +local all_titlebars = setmetatable({}, { __mode = 'k' }) + +-- Get a color for a titlebar, this tests many values from the array and the theme +local function get_color(name, c, args) + local suffix = "_normal" + if capi.client.focus == c then + suffix = "_focus" + end + local function get(array) + return array["titlebar_"..name..suffix] or array["titlebar_"..name] or array[name..suffix] or array[name] + end + return get(args) or get(beautiful) +end + +local function get_titlebar_function(c, position) + if position == "left" then + return c.titlebar_left + elseif position == "right" then + return c.titlebar_right + elseif position == "top" then + return c.titlebar_top + elseif position == "bottom" then + return c.titlebar_bottom + else + error("Invalid titlebar position '" .. position .. "'") + end +end + +--- Get a client's titlebar +-- @class function +-- @tparam client c The client for which a titlebar is wanted. +-- @tparam[opt={}] table args A table with extra arguments for the titlebar. +-- @tparam[opt=font.height*1.5] number args.size The height of the titlebar. +-- @tparam[opt=top] string args.position" values are `top`, +-- `left`, `right` and `bottom`. +-- @tparam[opt=top] string args.bg_normal +-- @tparam[opt=top] string args.bg_focus +-- @tparam[opt=top] string args.bgimage_normal +-- @tparam[opt=top] string args.bgimage_focus +-- @tparam[opt=top] string args.fg_normal +-- @tparam[opt=top] string args.fg_focus +-- @tparam[opt=top] string args.font +-- @name titlebar +local function new(c, args) + args = args or {} + local position = args.position or "top" + local size = args.size or util.round(beautiful.get_font_height(args.font) * 1.5) + local d = get_titlebar_function(c, position)(c, size) + + -- Make sure that there is never more than one titlebar for any given client + local bars = all_titlebars[c] + if not bars then + bars = {} + all_titlebars[c] = bars + end + + local ret + if not bars[position] then + local context = { + client = c, + position = position + } + ret = drawable(d, context, "awful.titlebar") + ret:_inform_visible(true) + local function update_colors() + local args_ = bars[position].args + ret:set_bg(get_color("bg", c, args_)) + ret:set_fg(get_color("fg", c, args_)) + ret:set_bgimage(get_color("bgimage", c, args_)) + end + + bars[position] = { + args = args, + drawable = ret, + update_colors = update_colors + } + + -- Update the colors when focus changes + c:connect_signal("focus", update_colors) + c:connect_signal("unfocus", update_colors) + + -- Inform the drawable when it becomes invisible + c:connect_signal("unmanage", function() ret:_inform_visible(false) end) + else + bars[position].args = args + ret = bars[position].drawable + end + + -- Make sure the titlebar has the right colors applied + bars[position].update_colors() + + -- Handle declarative/recursive widget container + ret.setup = base.widget.setup + + return ret +end + +--- Show a client's titlebar. +-- @param c The client whose titlebar is modified +-- @param[opt] position The position of the titlebar. Must be one of "left", +-- "right", "top", "bottom". Default is "top". +function titlebar.show(c, position) + position = position or "top" + local bars = all_titlebars[c] + local data = bars and bars[position] + local args = data and data.args + new(c, args) +end + +--- Hide a client's titlebar. +-- @param c The client whose titlebar is modified +-- @param[opt] position The position of the titlebar. Must be one of "left", +-- "right", "top", "bottom". Default is "top". +function titlebar.hide(c, position) + position = position or "top" + get_titlebar_function(c, position)(c, 0) +end + +--- Toggle a client's titlebar, hiding it if it is visible, otherwise showing it. +-- @param c The client whose titlebar is modified +-- @param[opt] position The position of the titlebar. Must be one of "left", +-- "right", "top", "bottom". Default is "top". +function titlebar.toggle(c, position) + position = position or "top" + local _, size = get_titlebar_function(c, position)(c) + if size == 0 then + titlebar.show(c, position) + else + titlebar.hide(c, position) + end +end + +--- Create a new titlewidget. A title widget displays the name of a client. +-- Please note that this returns a textbox and all of textbox' API is available. +-- This way, you can e.g. modify the font that is used. +-- @param c The client for which a titlewidget should be created. +-- @return The title widget. +function titlebar.widget.titlewidget(c) + local ret = textbox() + local function update() + ret:set_text(c.name or "") + end + c:connect_signal("property::name", update) + update() + + return ret +end + +--- Create a new icon widget. An icon widget displays the icon of a client. +-- Please note that this returns an imagebox and all of the imagebox' API is +-- available. This way, you can e.g. disallow resizes. +-- @param c The client for which an icon widget should be created. +-- @return The icon widget. +function titlebar.widget.iconwidget(c) + local ret = imagebox() + local function update() + ret:set_image(c.icon) + end + c:connect_signal("property::icon", update) + update() + + return ret +end + +--- Create a new button widget. A button widget displays an image and reacts to +-- mouse clicks. Please note that the caller has to make sure that this widget +-- gets redrawn when needed by calling the returned widget's update() function. +-- The selector function should return a value describing a state. If the value +-- is a boolean, either "active" or "inactive" are used. The actual image is +-- then found in the theme as "titlebar_[name]_button_[normal/focus]_[state]". +-- If that value does not exist, the focused state is ignored for the next try. +-- @param c The client for which a button is created. +-- @tparam string name Name of the button, used for accessing the theme and +-- in the tooltip. +-- @param selector A function that selects the image that should be displayed. +-- @param action Function that is called when the button is clicked. +-- @return The widget +function titlebar.widget.button(c, name, selector, action) + local ret = imagebox() + + if titlebar.enable_tooltip then + ret._private.tooltip = atooltip({ objects = {ret}, delay_show = 1 }) + ret._private.tooltip:set_text(name) + end + + local function update() + local img = selector(c) + if type(img) ~= "nil" then + -- Convert booleans automatically + if type(img) == "boolean" then + if img then + img = "active" + else + img = "inactive" + end + end + local prefix = "normal" + if capi.client.focus == c then + prefix = "focus" + end + if img ~= "" then + prefix = prefix .. "_" + end + -- First try with a prefix based on the client's focus state, + -- then try again without that prefix if nothing was found, + -- and finally, try a fallback for compatibility with Awesome 3.5 themes + local theme = beautiful["titlebar_" .. name .. "_button_" .. prefix .. img] + or beautiful["titlebar_" .. name .. "_button_" .. img] + or beautiful["titlebar_" .. name .. "_button_" .. prefix .. "_inactive"] + if theme then + img = theme + end + end + ret:set_image(img) + end + if action then + ret:buttons(abutton({ }, 1, nil, function() action(c, selector(c)) end)) + end + + ret.update = update + update() + + -- We do magic based on whether a client is focused above, so we need to + -- connect to the corresponding signal here. + c:connect_signal("focus", update) + c:connect_signal("unfocus", update) + + return ret +end + +--- Create a new float button for a client. +-- @param c The client for which the button is wanted. +function titlebar.widget.floatingbutton(c) + local widget = titlebar.widget.button(c, "floating", aclient.object.get_floating, aclient.floating.toggle) + c:connect_signal("property::floating", widget.update) + return widget +end + +--- Create a new maximize button for a client. +-- @param c The client for which the button is wanted. +function titlebar.widget.maximizedbutton(c) + local widget = titlebar.widget.button(c, "maximized", function(cl) + return cl.maximized_horizontal or cl.maximized_vertical + end, function(cl, state) + cl.maximized_horizontal = not state + cl.maximized_vertical = not state + end) + c:connect_signal("property::maximized_vertical", widget.update) + c:connect_signal("property::maximized_horizontal", widget.update) + return widget +end + +--- Create a new minimize button for a client. +-- @param c The client for which the button is wanted. +function titlebar.widget.minimizebutton(c) + local widget = titlebar.widget.button(c, "minimize", function() return "" end, function(cl) cl.minimized = not cl.minimized end) + c:connect_signal("property::minimized", widget.update) + return widget +end + +--- Create a new closing button for a client. +-- @param c The client for which the button is wanted. +function titlebar.widget.closebutton(c) + return titlebar.widget.button(c, "close", function() return "" end, function(cl) cl:kill() end) +end + +--- Create a new ontop button for a client. +-- @param c The client for which the button is wanted. +function titlebar.widget.ontopbutton(c) + local widget = titlebar.widget.button(c, "ontop", function(cl) return cl.ontop end, function(cl, state) cl.ontop = not state end) + c:connect_signal("property::ontop", widget.update) + return widget +end + +--- Create a new sticky button for a client. +-- @param c The client for which the button is wanted. +function titlebar.widget.stickybutton(c) + local widget = titlebar.widget.button(c, "sticky", function(cl) return cl.sticky end, function(cl, state) cl.sticky = not state end) + c:connect_signal("property::sticky", widget.update) + return widget +end + +client.connect_signal("unmanage", function(c) + all_titlebars[c] = nil +end) + +return setmetatable(titlebar, { __call = function(_, ...) return new(...) end}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/tooltip.lua b/lib/awful/tooltip.lua new file mode 100644 index 0000000..cd151c4 --- /dev/null +++ b/lib/awful/tooltip.lua @@ -0,0 +1,616 @@ +------------------------------------------------------------------------- +--- Tooltip module for awesome objects. +-- +-- A tooltip is a small hint displayed when the mouse cursor +-- hovers a specific item. +-- In awesome, a tooltip can be linked with almost any +-- object having a `:connect_signal()` method and receiving +-- `mouse::enter` and `mouse::leave` signals. +-- +-- How to create a tooltip? +-- --- +-- +-- myclock = wibox.widget.textclock({}, "%T", 1) +-- myclock_t = awful.tooltip({ +-- objects = { myclock }, +-- timer_function = function() +-- return os.date("Today is %A %B %d %Y\nThe time is %T") +-- end, +-- }) +-- +-- How to add the same tooltip to multiple objects? +-- --- +-- +-- myclock_t:add_to_object(obj1) +-- myclock_t:add_to_object(obj2) +-- +-- Now the same tooltip is attached to `myclock`, `obj1`, `obj2`. +-- +-- How to remove a tooltip from several objects? +-- --- +-- +-- myclock_t:remove_from_object(obj1) +-- myclock_t:remove_from_object(obj2) +-- +-- Now the same tooltip is only attached to `myclock`. +-- +-- @author Sébastien Gross <seb•ɱɩɲʋʃ•awesome•ɑƬ•chezwam•ɖɵʈ•org> +-- @copyright 2009 Sébastien Gross +-- @classmod awful.tooltip +------------------------------------------------------------------------- + +local mouse = mouse +local timer = require("gears.timer") +local util = require("awful.util") +local object = require("gears.object") +local color = require("gears.color") +local wibox = require("wibox") +local a_placement = require("awful.placement") +local abutton = require("awful.button") +local shape = require("gears.shape") +local beautiful = require("beautiful") +local textbox = require("wibox.widget.textbox") +local dpi = require("beautiful").xresources.apply_dpi +local cairo = require("lgi").cairo +local setmetatable = setmetatable +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local ipairs = ipairs +local capi = {mouse=mouse, awesome=awesome} + +local tooltip = { mt = {} } + +-- The mouse point is 1x1, so anything aligned based on it as parent +-- geometry will go out of bound. To get the desired placement, it is +-- necessary to swap left with right and top with bottom +local align_convert = { + top_left = "bottom_right", + left = "right", + bottom_left = "top_right", + right = "left", + top_right = "bottom_left", + bottom_right = "top_left", + top = "bottom", + bottom = "top", +} + +-- If the wibox is under the cursor, it will trigger a mouse::leave +local offset = { + top_left = {x = 0, y = 0 }, + left = {x = 0, y = 0 }, + bottom_left = {x = 0, y = 0 }, + right = {x = 1, y = 0 }, + top_right = {x = 0, y = 0 }, + bottom_right = {x = 1, y = 1 }, + top = {x = 0, y = 0 }, + bottom = {x = 0, y = 1 }, +} + +--- The tooltip border color. +-- @beautiful beautiful.tooltip_border_color + +--- The tooltip background color. +-- @beautiful beautiful.tooltip_bg + +--- The tooltip foregound (text) color. +-- @beautiful beautiful.tooltip_fg + +--- The tooltip font. +-- @beautiful beautiful.tooltip_font + +--- The tooltip border width. +-- @beautiful beautiful.tooltip_border_width + +--- The tooltip opacity. +-- @beautiful beautiful.tooltip_opacity + +--- The default tooltip shape. +-- By default, all tooltips are rectangles, however, by setting this variables, +-- they can default to rounded rectangle or stretched octogons. +-- @beautiful beautiful.tooltip_shape +-- @tparam[opt=gears.shape.rectangle] function shape A `gears.shape` compatible function +-- @see shape +-- @see gears.shape + +local function apply_shape(self) + local s = self._private.shape + + local wb = self.wibox + + if not s then + -- Clear the shape + if wb.shape_bounding then + wb.shape_bounding = nil + wb:set_bgimage(nil) + end + + return + end + + local w, h = wb.width, wb.height + + -- First, create a A1 mask for the shape bounding itself + local img = cairo.ImageSurface(cairo.Format.A1, w, h) + local cr = cairo.Context(img) + + cr:set_source_rgba(1,1,1,1) + + s(cr, w, h, unpack(self._private.shape_args or {})) + cr:fill() + wb.shape_bounding = img._native + + -- The wibox background uses ARGB32 border so tooltip anti-aliasing works + -- when an external compositor is used. This will look better than + -- the capi.drawin's own border support. + img = cairo.ImageSurface(cairo.Format.ARGB32, w, h) + cr = cairo.Context(img) + + -- Draw the border (multiply by 2, then mask the inner part to save a path) + local bw = (self._private.border_width + or beautiful.tooltip_border_width + or beautiful.border_width or 0) * 2 + + -- Fix anti-aliasing + if bw > 2 and awesome.composite_manager_running then + bw = bw - 1 + end + + local bc = self._private.border_color + or beautiful.tooltip_border_color + or beautiful.border_normal + or "#ffcb60" + + cr:translate(bw, bw) + s(cr, w-2*bw, h-2*bw, unpack(self._private.shape_args or {})) + cr:set_line_width(bw) + cr:set_source(color(bc)) + cr:stroke_preserve() + cr:clip() + + local bg = self._private.bg + or beautiful.tooltip_bg + or beautiful.bg_focus or "#ffcb60" + + cr:set_source(color(bg)) + cr:paint() + + wb:set_bgimage(img) +end + +local function apply_mouse_mode(self) + local w = self:get_wibox() + local align = self._private.align + local real_placement = align_convert[align] + + a_placement[real_placement](w, { + parent = capi.mouse, + offset = offset[align] + }) +end + +local function apply_outside_mode(self) + local w = self:get_wibox() + + local _, position = a_placement.next_to(w, { + geometry = self._private.widget_geometry, + preferred_positions = self.preferred_positions, + honor_workarea = true, + }) + + if position ~= self.current_position then + -- Re-apply the shape. + apply_shape(self) + end + + self.current_position = position +end + +-- Place the tooltip under the mouse. +-- +-- @tparam tooltip self A tooltip object. +local function set_geometry(self) + -- calculate width / height + local n_w, n_h = self.textbox:get_preferred_size(mouse.screen) + n_w = n_w + self.marginbox.left + self.marginbox.right + n_h = n_h + self.marginbox.top + self.marginbox.bottom + + local w = self:get_wibox() + w:geometry({ width = n_w, height = n_h }) + + if self._private.shape then + apply_shape(self) + end + + local mode = self.mode + + if mode == "outside" and self._private.widget_geometry then + apply_outside_mode(self) + else + apply_mouse_mode(self) + end + + a_placement.no_offscreen(w) +end + +-- Show a tooltip. +-- +-- @tparam tooltip self The tooltip to show. +local function show(self) + -- do nothing if the tooltip is already shown + if self._private.visible then return end + if self.timer then + if not self.timer.started then + self:timer_function() + self.timer:start() + end + end + set_geometry(self) + self.wibox.visible = true + self._private.visible = true + self:emit_signal("property::visible") +end + +-- Hide a tooltip. +-- +-- @tparam tooltip self The tooltip to hide. +local function hide(self) + -- do nothing if the tooltip is already hidden + if not self._private.visible then return end + if self.timer then + if self.timer.started then + self.timer:stop() + end + end + self.wibox.visible = false + self._private.visible = false + self:emit_signal("property::visible") +end + +--- The wibox. +-- @property wibox +-- @param `wibox` + +function tooltip:get_wibox() + if self._private.wibox then + return self._private.wibox + end + + local wb = wibox(self.wibox_properties) + wb:set_widget(self.marginbox) + + -- Close the tooltip when clicking it. This gets done on release, to not + -- emit the release event on an underlying object, e.g. the titlebar icon. + wb:buttons(abutton({}, 1, nil, self.hide)) + + self._private.wibox = wb + + return wb +end + +--- Is the tooltip visible? +-- @property visible +-- @param boolean + +function tooltip:get_visible() + return self._private.visible +end + +function tooltip:set_visible(value) + if self._private.visible == value then return end + + if value then + show(self) + else + hide(self) + end +end + +--- The horizontal alignment. +-- +-- The following values are valid: +-- +-- * top_left +-- * left +-- * bottom_left +-- * right +-- * top_right +-- * bottom_right +-- * bottom +-- * top +-- +-- @property align +-- @see beautiful.tooltip_align + +--- The default tooltip alignment. +-- @beautiful beautiful.tooltip_align +-- @param string +-- @see align + +function tooltip:get_align() + return self._private.align +end + +function tooltip:set_align(value) + if not align_convert[value] then + return + end + + self._private.align = value + + set_geometry(self) + self:emit_signal("property::align") +end + +--- The shape of the tooltip window. +-- If the shape require some parameters, use `set_shape`. +-- @property shape +-- @see gears.shape +-- @see set_shape +-- @see beautiful.tooltip_shape + +--- Set the tooltip shape. +-- All other arguments will be passed to the shape function. +-- @tparam gears.shape s The shape +-- @see shape +-- @see gears.shape +function tooltip:set_shape(s, ...) + self._private.shape = s + self._private.shape_args = {...} + apply_shape(self) +end + +--- Set the tooltip positioning mode. +-- This affects how the tooltip is placed. By default, the tooltip is `align`ed +-- close to the mouse cursor. It is also possible to place the tooltip relative +-- to the widget geometry. +-- +-- Valid modes are: +-- +-- * "mouse": Next to the mouse cursor +-- * "outside": Outside of the widget +-- +-- @property mode +-- @param string + +function tooltip:set_mode(mode) + self._private.mode = mode + + set_geometry(self) + self:emit_signal("property::mode") +end + +function tooltip:get_mode() + return self._private.mode or "mouse" +end + +--- The preferred positions when in `outside` mode. +-- +-- If the tooltip fits on multiple sides of the drawable, then this defines the +-- priority +-- +-- The default is: +-- +-- {"top", "right", "left", "bottom"} +-- +-- @property preferred_positions +-- @tparam table preferred_positions The position, ordered by priorities + +function tooltip:get_preferred_positions() + return self._private.preferred_positions or + {"top", "right", "left", "bottom"} +end + +function tooltip:set_preferred_positions(value) + self._private.preferred_positions = value + + set_geometry(self) +end + +--- Change displayed text. +-- +-- @property text +-- @tparam tooltip self The tooltip object. +-- @tparam string text New tooltip text, passed to +-- `wibox.widget.textbox.set_text`. + +function tooltip:set_text(text) + self.textbox:set_text(text) + if self._private.visible then + set_geometry(self) + end +end + +--- Change displayed markup. +-- +-- @property markup +-- @tparam tooltip self The tooltip object. +-- @tparam string text New tooltip markup, passed to +-- `wibox.widget.textbox.set_markup`. + +function tooltip:set_markup(text) + self.textbox:set_markup(text) + if self._private.visible then + set_geometry(self) + end +end + +--- Change the tooltip's update interval. +-- +-- @property timeout +-- @tparam tooltip self A tooltip object. +-- @tparam number timeout The timeout value. + +function tooltip:set_timeout(timeout) + if self.timer then + self.timer.timeout = timeout + end +end + +--- Add tooltip to an object. +-- +-- @tparam tooltip self The tooltip. +-- @tparam gears.object obj An object with `mouse::enter` and +-- `mouse::leave` signals. +-- @function add_to_object +function tooltip:add_to_object(obj) + if not obj then return end + + obj:connect_signal("mouse::enter", self.show) + obj:connect_signal("mouse::leave", self.hide) +end + +--- Remove tooltip from an object. +-- +-- @tparam tooltip self The tooltip. +-- @tparam gears.object obj An object with `mouse::enter` and +-- `mouse::leave` signals. +-- @function remove_from_object +function tooltip:remove_from_object(obj) + obj:disconnect_signal("mouse::enter", self.show) + obj:disconnect_signal("mouse::leave", self.hide) +end + +-- Tooltip can be applied to both widgets, wibox and client, their geometry +-- works differently. +local function get_parent_geometry(arg1, arg2) + if type(arg2) == "table" and arg2.width then + return arg2 + elseif type(arg1) == "table" and arg1.width then + return arg1 + end +end + +--- Create a new tooltip and link it to a widget. +-- Tooltips emit `property::visible` when their visibility changes. +-- @tparam table args Arguments for tooltip creation. +-- @tparam function args.timer_function A function to dynamically set the +-- tooltip text. Its return value will be passed to +-- `wibox.widget.textbox.set_markup`. +-- @tparam[opt=1] number args.timeout The timeout value for +-- `timer_function`. +-- @tparam[opt] table args.objects A list of objects linked to the tooltip. +-- @tparam[opt] number args.delay_show Delay showing the tooltip by this many +-- seconds. +-- @tparam[opt=apply_dpi(5)] integer args.margin_leftright The left/right margin for the text. +-- @tparam[opt=apply_dpi(3)] integer args.margin_topbottom The top/bottom margin for the text. +-- @tparam[opt=nil] gears.shape args.shape The shape +-- @treturn awful.tooltip The created tooltip. +-- @see add_to_object +-- @see timeout +-- @see text +-- @see markup +-- @function awful.tooltip +function tooltip.new(args) + local self = object { + enable_properties = true, + } + + rawset(self,"_private", {}) + + self._private.visible = false + self._private.align = args.align or beautiful.tooltip_align or "right" + self._private.shape = args.shape or beautiful.tooltip_shape + or shape.rectangle + + -- private data + if args.delay_show then + local delay_timeout + + delay_timeout = timer { timeout = args.delay_show } + delay_timeout:connect_signal("timeout", function () + show(self) + delay_timeout:stop() + end) + + function self.show(other, geo) + -- Auto detect clients and wiboxes + if other.drawable or other.pid then + geo = other:geometry() + end + + -- Cache the geometry in case it is needed later + self._private.widget_geometry = get_parent_geometry(other, geo) + + if not delay_timeout.started then + delay_timeout:start() + end + end + function self.hide() + if delay_timeout.started then + delay_timeout:stop() + end + hide(self) + end + else + function self.show(other, geo) + -- Auto detect clients and wiboxes + if other.drawable or other.pid then + geo = other:geometry() + end + + -- Cache the geometry in case it is needed later + self._private.widget_geometry = get_parent_geometry(other, geo) + + show(self) + end + function self.hide() + hide(self) + end + end + + -- export functions + util.table.crush(self, tooltip, true) + + -- setup the timer action only if needed + if args.timer_function then + self.timer = timer { timeout = args.timeout and args.timeout or 1 } + self.timer_function = function() + self:set_markup(args.timer_function()) + end + self.timer:connect_signal("timeout", self.timer_function) + end + + local fg = beautiful.tooltip_fg or beautiful.fg_focus or "#000000" + local font = beautiful.tooltip_font or beautiful.font + + -- Set default properties + self.wibox_properties = { + visible = false, + ontop = true, + border_width = 0, + fg = fg, + bg = color.transparent, + opacity = beautiful.tooltip_opacity or 1, + } + + self.textbox = textbox() + self.textbox:set_font(font) + + -- Add margin. + local m_lr = args.margin_leftright or dpi(5) + local m_tb = args.margin_topbottom or dpi(3) + self.marginbox = wibox.container.margin(self.textbox, m_lr, m_lr, m_tb, m_tb) + + -- Add tooltip to objects + if args.objects then + for _, obj in ipairs(args.objects) do + self:add_to_object(obj) + end + end + + -- Apply the properties + for k, v in pairs(args) do + if tooltip["set_"..k] then + self[k] = v + end + end + + return self +end + +function tooltip.mt:__call(...) + return tooltip.new(...) +end + +return setmetatable(tooltip, tooltip.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/util.lua b/lib/awful/util.lua new file mode 100644 index 0000000..8dcb955 --- /dev/null +++ b/lib/awful/util.lua @@ -0,0 +1,588 @@ +--------------------------------------------------------------------------- +--- Utility module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module awful.util +--------------------------------------------------------------------------- + +-- Grab environment we need +local os = os +local assert = assert +local load = loadstring or load -- luacheck: globals loadstring (compatibility with Lua 5.1) +local loadfile = loadfile +local debug = debug +local pairs = pairs +local ipairs = ipairs +local type = type +local rtable = table +local string = string +local lgi = require("lgi") +local grect = require("gears.geometry").rectangle +local Gio = require("lgi").Gio +local Pango = lgi.Pango +local capi = +{ + awesome = awesome, + mouse = mouse +} +local gears_debug = require("gears.debug") +local floor = math.floor + +local util = {} +util.table = {} + +--- The default shell used when spawing processes. +util.shell = os.getenv("SHELL") or "/bin/sh" + +local displayed_deprecations = {} +--- Display a deprecation notice, but only once per traceback. +-- @param[opt] see The message to a new method / function to use. +-- @tparam table args Extra arguments +-- @tparam boolean args.raw Print the message as-is without the automatic context +function util.deprecate(see, args) + args = args or {} + local tb = debug.traceback() + if displayed_deprecations[tb] then + return + end + displayed_deprecations[tb] = true + + -- Get function name/desc from caller. + local info = debug.getinfo(2, "n") + local funcname = info.name or "?" + local msg = "awful: function " .. funcname .. " is deprecated" + if see then + if args.raw then + msg = see + elseif string.sub(see, 1, 3) == 'Use' then + msg = msg .. ". " .. see + else + msg = msg .. ", see " .. see + end + end + gears_debug.print_warning(msg .. ".\n" .. tb) +end + +--- Create a class proxy with deprecation messages. +-- This is useful when a class has moved somewhere else. +-- @tparam table fallback The new class +-- @tparam string old_name The old class name +-- @tparam string new_name The new class name +-- @treturn table A proxy class. +function util.deprecate_class(fallback, old_name, new_name) + local message = old_name.." has been renamed to "..new_name + + local function call(_,...) + util.deprecate(message) + + return fallback(...) + end + + local function index(_, k) + util.deprecate(message) + + return fallback[k] + end + + local function newindex(_, k, v) + util.deprecate(message, {raw = true}) + + fallback[k] = v + end + + return setmetatable({}, {__call = call, __index = index, __newindex = newindex}) +end + +--- Get a valid color for Pango markup +-- @param color The color. +-- @tparam string fallback The color to return if the first is invalid. (default: black) +-- @treturn string color if it is valid, else fallback. +function util.ensure_pango_color(color, fallback) + color = tostring(color) + return Pango.Color.parse(Pango.Color(), color) and color or fallback or "black" +end + +--- Make i cycle. +-- @param t A length. Must be greater than zero. +-- @param i An absolute index to fit into #t. +-- @return An integer in (1, t) or nil if t is less than or equal to zero. +function util.cycle(t, i) + if t < 1 then return end + i = i % t + if i == 0 then + i = t + end + return i +end + +--- Create a directory +-- @param dir The directory. +-- @return mkdir return code +function util.mkdir(dir) + return os.execute("mkdir -p " .. dir) +end + +--- Eval Lua code. +-- @return The return value of Lua code. +function util.eval(s) + return assert(load(s))() +end + +local xml_entity_names = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; +--- Escape a string from XML char. +-- Useful to set raw text in textbox. +-- @param text Text to escape. +-- @return Escape text. +function util.escape(text) + return text and text:gsub("['&<>\"]", xml_entity_names) or nil +end + +local xml_entity_chars = { lt = "<", gt = ">", nbsp = " ", quot = "\"", apos = "'", ndash = "-", mdash = "-", amp = "&" }; +--- Unescape a string from entities. +-- @param text Text to unescape. +-- @return Unescaped text. +function util.unescape(text) + return text and text:gsub("&(%a+);", xml_entity_chars) or nil +end + +--- Check if a file is a Lua valid file. +-- This is done by loading the content and compiling it with loadfile(). +-- @param path The file path. +-- @return A function if everything is alright, a string with the error +-- otherwise. +function util.checkfile(path) + local f, e = loadfile(path) + -- Return function if function, otherwise return error. + if f then return f end + return e +end + +--- Try to restart awesome. +-- It checks if the configuration file is valid, and then restart if it's ok. +-- If it's not ok, the error will be returned. +-- @return Never return if awesome restart, or return a string error. +function util.restart() + local c = util.checkfile(capi.awesome.conffile) + + if type(c) ~= "function" then + return c + end + + capi.awesome.restart() +end + +--- Get the config home according to the XDG basedir specification. +-- @return the config home (XDG_CONFIG_HOME) with a slash at the end. +function util.get_xdg_config_home() + return (os.getenv("XDG_CONFIG_HOME") or os.getenv("HOME") .. "/.config") .. "/" +end + +--- Get the cache home according to the XDG basedir specification. +-- @return the cache home (XDG_CACHE_HOME) with a slash at the end. +function util.get_xdg_cache_home() + return (os.getenv("XDG_CACHE_HOME") or os.getenv("HOME") .. "/.cache") .. "/" +end + +--- Get the path to the user's config dir. +-- This is the directory containing the configuration file ("rc.lua"). +-- @return A string with the requested path with a slash at the end. +function util.get_configuration_dir() + return capi.awesome.conffile:match(".*/") or "./" +end + +--- Get the path to a directory that should be used for caching data. +-- @return A string with the requested path with a slash at the end. +function util.get_cache_dir() + return util.get_xdg_cache_home() .. "awesome/" +end + +--- Get the path to the directory where themes are installed. +-- @return A string with the requested path with a slash at the end. +function util.get_themes_dir() + return "/usr/share/awesome/themes" .. "/" +end + +--- Get the path to the directory where our icons are installed. +-- @return A string with the requested path with a slash at the end. +function util.get_awesome_icon_dir() + return "/usr/share/awesome/icons" .. "/" +end + +--- Get the user's config or cache dir. +-- It first checks XDG_CONFIG_HOME / XDG_CACHE_HOME, but then goes with the +-- default paths. +-- @param d The directory to get (either "config" or "cache"). +-- @return A string containing the requested path. +function util.getdir(d) + if d == "config" then + -- No idea why this is what is returned, I recommend everyone to use + -- get_configuration_dir() instead + return util.get_xdg_config_home() .. "awesome/" + elseif d == "cache" then + return util.get_cache_dir() + end +end + +--- Search for an icon and return the full path. +-- It searches for the icon path under the given directories with respect to the +-- given extensions for the icon filename. +-- @param iconname The name of the icon to search for. +-- @param exts Table of image extensions allowed, otherwise { 'png', gif' } +-- @param dirs Table of dirs to search, otherwise { '/usr/share/pixmaps/' } +-- @tparam[opt] string size The size. If this is specified, subdirectories `x` +-- of the dirs are searched first. +function util.geticonpath(iconname, exts, dirs, size) + exts = exts or { 'png', 'gif' } + dirs = dirs or { '/usr/share/pixmaps/', '/usr/share/icons/hicolor/' } + local icontypes = { 'apps', 'actions', 'categories', 'emblems', + 'mimetypes', 'status', 'devices', 'extras', 'places', 'stock' } + for _, d in pairs(dirs) do + local icon + for _, e in pairs(exts) do + icon = d .. iconname .. '.' .. e + if util.file_readable(icon) then + return icon + end + if size then + for _, t in pairs(icontypes) do + icon = string.format("%s%ux%u/%s/%s.%s", d, size, size, t, iconname, e) + if util.file_readable(icon) then + return icon + end + end + end + end + end +end + +--- Check if a file exists, is not readable and not a directory. +-- @param filename The file path. +-- @return True if file exists and is readable. +function util.file_readable(filename) + local gfile = Gio.File.new_for_path(filename) + local gfileinfo = gfile:query_info("standard::type,access::can-read", + Gio.FileQueryInfoFlags.NONE) + return gfileinfo and gfileinfo:get_file_type() ~= "DIRECTORY" and + gfileinfo:get_attribute_boolean("access::can-read") +end + +--- Check if a path exists, is readable and is a directory. +-- @tparam string path The directory path. +-- @treturn boolean True if dir exists and is readable. +function util.dir_readable(path) + local gfile = Gio.File.new_for_path(path) + local gfileinfo = gfile:query_info("standard::type,access::can-read", + Gio.FileQueryInfoFlags.NONE) + return gfileinfo and gfileinfo:get_file_type() == "DIRECTORY" and + gfileinfo:get_attribute_boolean("access::can-read") +end + +--- Check if a path is a directory. +-- @tparam string path +-- @treturn bool True if path exists and is a directory. +function util.is_dir(path) + return Gio.File.new_for_path(path):query_file_type({}) == "DIRECTORY" +end + +local function subset_mask_apply(mask, set) + local ret = {} + for i = 1, #set do + if mask[i] then + rtable.insert(ret, set[i]) + end + end + return ret +end + +local function subset_next(mask) + local i = 1 + while i <= #mask and mask[i] do + mask[i] = false + i = i + 1 + end + + if i <= #mask then + mask[i] = 1 + return true + end + return false +end + +--- Return all subsets of a specific set. +-- This function, giving a set, will return all subset it. +-- For example, if we consider a set with value { 10, 15, 34 }, +-- it will return a table containing 2^n set: +-- { }, { 10 }, { 15 }, { 34 }, { 10, 15 }, { 10, 34 }, etc. +-- @param set A set. +-- @return A table with all subset. +function util.subsets(set) + local mask = {} + local ret = {} + for i = 1, #set do mask[i] = false end + + -- Insert the empty one + rtable.insert(ret, {}) + + while subset_next(mask) do + rtable.insert(ret, subset_mask_apply(mask, set)) + end + return ret +end + +--- Get the nearest rectangle in the given direction. Every rectangle is specified as a table +-- with 'x', 'y', 'width', 'height' keys, the same as client or screen geometries. +-- @deprecated awful.util.get_rectangle_in_direction +-- @param dir The direction, can be either "up", "down", "left" or "right". +-- @param recttbl A table of rectangle specifications. +-- @param cur The current rectangle. +-- @return The index for the rectangle in recttbl closer to cur in the given direction. nil if none found. +-- @see gears.geometry +function util.get_rectangle_in_direction(dir, recttbl, cur) + util.deprecate("gears.geometry.rectangle.get_in_direction") + + return grect.get_in_direction(dir, recttbl, cur) +end + +--- Join all tables given as parameters. +-- This will iterate all tables and insert all their keys into a new table. +-- @param args A list of tables to join +-- @return A new table containing all keys from the arguments. +function util.table.join(...) + local ret = {} + for _, t in pairs({...}) do + if t then + for k, v in pairs(t) do + if type(k) == "number" then + rtable.insert(ret, v) + else + ret[k] = v + end + end + end + end + return ret +end + +--- Override elements in the first table by the one in the second. +-- +-- Note that this method doesn't copy entries found in `__index`. +-- @tparam table t the table to be overriden +-- @tparam table set the table used to override members of `t` +-- @tparam[opt=false] boolean raw Use rawset (avoid the metatable) +-- @treturn table t (for convenience) +function util.table.crush(t, set, raw) + if raw then + for k, v in pairs(set) do + rawset(t, k, v) + end + else + for k, v in pairs(set) do + t[k] = v + end + end + + return t +end + +--- Pack all elements with an integer key into a new table +-- While both lua and luajit implement __len over sparse +-- table, the standard define it as an implementation +-- detail. +-- +-- This function remove any non numeric keys from the value set +-- +-- @tparam table t A potentially sparse table +-- @treturn table A packed table with all numeric keys +function util.table.from_sparse(t) + local keys= {} + for k in pairs(t) do + if type(k) == "number" then + keys[#keys+1] = k + end + end + + table.sort(keys) + + local ret = {} + for _,v in ipairs(keys) do + ret[#ret+1] = t[v] + end + + return ret +end + +--- Check if a table has an item and return its key. +-- @param t The table. +-- @param item The item to look for in values of the table. +-- @return The key were the item is found, or nil if not found. +function util.table.hasitem(t, item) + for k, v in pairs(t) do + if v == item then + return k + end + end +end + +--- Split a string into multiple lines +-- @param text String to wrap. +-- @param width Maximum length of each line. Default: 72. +-- @param indent Number of spaces added before each wrapped line. Default: 0. +-- @return The string with lines wrapped to width. +function util.linewrap(text, width, indent) + text = text or "" + width = width or 72 + indent = indent or 0 + + local pos = 1 + return text:gsub("(%s+)()(%S+)()", + function(_, st, word, fi) + if fi - pos > width then + pos = st + return "\n" .. string.rep(" ", indent) .. word + end + end) +end + +--- Count number of lines in a string +-- @tparam string text Input string. +-- @treturn int Number of lines. +function util.linecount(text) + return select(2, text:gsub('\n', '\n')) + 1 +end + +--- Get a sorted table with all integer keys from a table +-- @param t the table for which the keys to get +-- @return A table with keys +function util.table.keys(t) + local keys = { } + for k, _ in pairs(t) do + rtable.insert(keys, k) + end + rtable.sort(keys, function (a, b) + return type(a) == type(b) and a < b or false + end) + return keys +end + +--- Filter a tables keys for certain content types +-- @param t The table to retrieve the keys for +-- @param ... the types to look for +-- @return A filtered table with keys +function util.table.keys_filter(t, ...) + local keys = util.table.keys(t) + local keys_filtered = { } + for _, k in pairs(keys) do + for _, et in pairs({...}) do + if type(t[k]) == et then + rtable.insert(keys_filtered, k) + break + end + end + end + return keys_filtered +end + +--- Reverse a table +-- @param t the table to reverse +-- @return the reversed table +function util.table.reverse(t) + local tr = { } + -- reverse all elements with integer keys + for _, v in ipairs(t) do + rtable.insert(tr, 1, v) + end + -- add the remaining elements + for k, v in pairs(t) do + if type(k) ~= "number" then + tr[k] = v + end + end + return tr +end + +--- Clone a table +-- @param t the table to clone +-- @param deep Create a deep clone? (default: true) +-- @return a clone of t +function util.table.clone(t, deep) + deep = deep == nil and true or deep + local c = { } + for k, v in pairs(t) do + if deep and type(v) == "table" then + c[k] = util.table.clone(v) + else + c[k] = v + end + end + return c +end + +--- +-- Returns an iterator to cycle through, starting from the first element or the +-- given index, all elements of a table that match a given criteria. +-- +-- @param t the table to iterate +-- @param filter a function that returns true to indicate a positive match +-- @param start what index to start iterating from. Default is 1 (=> start of +-- the table) +function util.table.iterate(t, filter, start) + local count = 0 + local index = start or 1 + local length = #t + + return function () + while count < length do + local item = t[index] + index = util.cycle(#t, index + 1) + count = count + 1 + if filter(item) then return item end + end + end +end + + +--- Merge items from the one table to another one +-- @tparam table t the container table +-- @tparam table set the mixin table +-- @treturn table Return `t` for convenience +function util.table.merge(t, set) + for _, v in ipairs(set) do + table.insert(t, v) + end + return t +end + + +-- Escape all special pattern-matching characters so that lua interprets them +-- literally instead of as a character class. +-- Source: http://stackoverflow.com/a/20778724/15690 +function util.quote_pattern(s) + -- All special characters escaped in a string: %%, %^, %$, ... + local patternchars = '['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..']' + return string.gsub(s, patternchars, "%%%1") +end + +-- Generate a pattern matching expression that ignores case. +-- @param s Original pattern matching expression. +function util.query_to_pattern(q) + local s = util.quote_pattern(q) + -- Poor man's case-insensitive character matching. + s = string.gsub(s, "%a", + function (c) + return string.format("[%s%s]", string.lower(c), + string.upper(c)) + end) + return s +end + +--- Round a number to an integer. +-- @tparam number x +-- @treturn integer +function util.round(x) + return floor(x + 0.5) +end + +return util + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/wibar.lua b/lib/awful/wibar.lua new file mode 100644 index 0000000..bbe7289 --- /dev/null +++ b/lib/awful/wibar.lua @@ -0,0 +1,603 @@ +--------------------------------------------------------------------------- +--- Wibox module for awful. +-- This module allows you to easily create wibox and attach them to the edge of +-- a screen. +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2016 Emmanuel Lepage Vallee +-- @classmod awful.wibar +--------------------------------------------------------------------------- + +-- Grab environment we need +local capi = +{ + screen = screen, + client = client +} +local setmetatable = setmetatable +local tostring = tostring +local ipairs = ipairs +local error = error +local wibox = require("wibox") +local beautiful = require("beautiful") +local util = require("awful.util") +local placement = require("awful.placement") + +local function get_screen(s) + return s and capi.screen[s] +end + +local awfulwibar = { mt = {} } + +--- Array of table with wiboxes inside. +-- It's an array so it is ordered. +local wiboxes = setmetatable({}, {__mode = "v"}) + +-- Compute the margin on one side +local function get_margin(w, position, auto_stop) + local h_or_w = (position == "top" or position == "bottom") and "height" or "width" + local ret = 0 + + for _, v in ipairs(wiboxes) do + -- Ignore the wibars placed after this one + if auto_stop and v == w then break end + + if v.position == position and v.screen == w.screen and v.visible then + ret = ret + v[h_or_w] + end + end + + return ret +end + +-- `honor_workarea` cannot be used as it does modify the workarea itself. +-- a manual padding has to be generated. +local function get_margins(w) + local position = w.position + assert(position) + + local margins = {left=0, right=0, top=0, bottom=0} + + margins[position] = get_margin(w, position, true) + + -- Avoid overlapping wibars + if position == "left" or position == "right" then + margins.top = get_margin(w, "top" ) + margins.bottom = get_margin(w, "bottom") + end + + return margins +end + +-- Create the placement function +local function gen_placement(position, stretch) + local maximize = (position == "right" or position == "left") and + "maximize_vertically" or "maximize_horizontally" + + return placement[position] + (stretch and placement[maximize] or nil) +end + +-- Attach the placement function. +local function attach(wb, align) + gen_placement(align, wb._stretch)(wb, { + attach = true, + update_workarea = true, + margins = get_margins(wb) + }) +end + +-- Re-attach all wibars on a given wibar screen +local function reattach(wb) + local s = wb.screen + for _, w in ipairs(wiboxes) do + if w ~= wb and w.screen == s then + if w.detach_callback then + w.detach_callback() + w.detach_callback = nil + end + attach(w, w.position) + end + end +end + +--- The wibox position. +-- @property position +-- @param string Either "left", right", "top" or "bottom" + +local function get_position(wb) + return wb._position or "top" +end + +local function set_position(wb, position) + -- Detach first to avoid any uneeded callbacks + if wb.detach_callback then + wb.detach_callback() + + -- Avoid disconnecting twice, this produces a lot of warnings + wb.detach_callback = nil + end + + -- Move the wibar to the end of the list to avoid messing up the others in + -- case there is stacked wibars on one side. + if wb._position then + for k, w in ipairs(wiboxes) do + if w == wb then + table.remove(wiboxes, k) + end + end + table.insert(wiboxes, wb) + end + + -- In case the position changed, it may be necessary to reset the size + if (wb._position == "left" or wb._position == "right") + and (position == "top" or position == "bottom") then + wb.height = math.ceil(beautiful.get_font_height(wb.font) * 1.5) + elseif (wb._position == "top" or wb._position == "bottom") + and (position == "left" or position == "right") then + wb.width = math.ceil(beautiful.get_font_height(wb.font) * 1.5) + end + + -- Changing the position will also cause the other margins to be invalidated. + -- For example, adding a wibar to the top will change the margins of any left + -- or right wibars. To solve, this, they need to be re-attached. + reattach(wb) + + -- Set the new position + wb._position = position + + -- Attach to the new position + attach(wb, position) +end + +--- Stretch the wibar. +-- +-- @property stretch +-- @param[opt=true] boolean + +local function get_stretch(w) + return w._stretch +end + +local function set_stretch(w, value) + w._stretch = value + + attach(w, w.position) +end + +--- Remove a wibar. +-- @function remove +local function remove(self) + self.visible = false + + if self.detach_callback then + self.detach_callback() + self.detach_callback = nil + end + + for k, w in ipairs(wiboxes) do + if w == self then + table.remove(wiboxes, k) + end + end + + self._screen = nil +end + +--- Get a wibox position if it has been set, or return top. +-- @param wb The wibox +-- @deprecated awful.wibar.get_position +-- @return The wibox position. +function awfulwibar.get_position(wb) + util.deprecate("Use wb:get_position() instead of awful.wibar.get_position") + return get_position(wb) +end + +--- Put a wibox on a screen at this position. +-- @param wb The wibox to attach. +-- @param position The position: top, bottom left or right. +-- @param screen This argument is deprecated, use wb.screen directly. +-- @deprecated awful.wibar.set_position +function awfulwibar.set_position(wb, position, screen) --luacheck: no unused args + util.deprecate("Use wb:set_position(position) instead of awful.wibar.set_position") + + set_position(wb, position) +end + +--- Attach a wibox to a screen. +-- +-- This function has been moved to the `awful.placement` module. Calling this +-- no longer does anything. +-- +-- @param wb The wibox to attach. +-- @param position The position of the wibox: top, bottom, left or right. +-- @param screen The screen to attach to +-- @see awful.placement +-- @deprecated awful.wibar.attach +function awfulwibar.attach(wb, position, screen) --luacheck: no unused args + util.deprecate("awful.wibar.attach is deprecated, use the 'attach' property".. + " of awful.placement. This method doesn't do anything anymore" + ) +end + +--- Align a wibox. +-- +-- Supported alignment are: +-- +-- * top_left +-- * top_right +-- * bottom_left +-- * bottom_right +-- * left +-- * right +-- * top +-- * bottom +-- * centered +-- * center_vertical +-- * center_horizontal +-- +-- @param wb The wibox. +-- @param align The alignment +-- @param screen This argument is deprecated. It is not used. Use wb.screen +-- directly. +-- @deprecated awful.wibar.align +-- @see awful.placement.align +function awfulwibar.align(wb, align, screen) --luacheck: no unused args + if align == "center" then + util.deprecate("awful.wibar.align(wb, 'center' is deprecated, use 'centered'") + align = "centered" + end + + if screen then + util.deprecate("awful.wibar.align 'screen' argument is deprecated") + end + + if placement[align] then + return placement[align](wb) + end +end + +--- Stretch a wibox so it takes all screen width or height. +-- +-- **This function has been removed.** +-- +-- @deprecated awful.wibox.stretch +-- @see awful.placement +-- @see awful.wibar.stretch + +--- Create a new wibox and attach it to a screen edge. +-- You can add also position key with value top, bottom, left or right. +-- You can also use width or height in % and set align to center, right or left. +-- You can also set the screen key with a screen number to attach the wibox. +-- If not specified, the primary screen is assumed. +-- @see wibox +-- @tparam[opt=nil] table arg +-- @tparam string arg.position The position. +-- @tparam string arg.stretch If the wibar need to be stretched to fill the screen. +-- @tparam integer arg.border_width Border width. +-- @tparam string arg.border_color Border color. +-- @tparam boolean arg.ontop On top of other windows. +-- @tparam string arg.cursor The mouse cursor. +-- @tparam boolean arg.visible Visibility. +-- @tparam number arg.opacity The opacity of the wibox, between 0 and 1. +-- @tparam string arg.type The window type (desktop, normal, dock, …). +-- @tparam integer arg.x The x coordinates. +-- @tparam integer arg.y The y coordinates. +-- @tparam integer arg.width The width of the wibox. +-- @tparam integer arg.height The height of the wibox. +-- @tparam screen arg.screen The wibox screen. +-- @tparam wibox.widget arg.widget The widget that the wibox displays. +-- @param arg.shape_bounding The wibox’s bounding shape as a (native) cairo surface. +-- @param arg.shape_clip The wibox’s clip shape as a (native) cairo surface. +-- @tparam color arg.bg The background of the wibox. +-- @tparam surface arg.bgimage The background image of the drawable. +-- @tparam color arg.fg The foreground (text) of the wibox. +-- @return The new wibar +-- @function awful.wibar +function awfulwibar.new(arg) + arg = arg or {} + local position = arg.position or "top" + local has_to_stretch = true + local screen = get_screen(arg.screen or 1) + + arg.type = arg.type or "dock" + + if position ~= "top" and position ~="bottom" + and position ~= "left" and position ~= "right" then + error("Invalid position in awful.wibar(), you may only use" + .. " 'top', 'bottom', 'left' and 'right'") + end + + -- Set default size + if position == "left" or position == "right" then + arg.width = arg.width or math.ceil(beautiful.get_font_height(arg.font) * 1.5) + if arg.height then + has_to_stretch = false + if arg.screen then + local hp = tostring(arg.height):match("(%d+)%%") + if hp then + arg.height = math.ceil(screen.geometry.height * hp / 100) + end + end + end + else + arg.height = arg.height or math.ceil(beautiful.get_font_height(arg.font) * 1.5) + if arg.width then + has_to_stretch = false + if arg.screen then + local wp = tostring(arg.width):match("(%d+)%%") + if wp then + arg.width = math.ceil(screen.geometry.width * wp / 100) + end + end + end + end + + arg.screen = nil + + local w = wibox(arg) + + w.screen = screen + w._screen = screen --HACK When a screen is removed, then getbycoords wont work + w._stretch = arg.stretch == nil and has_to_stretch or arg.stretch + + w.get_position = get_position + w.set_position = set_position + + w.get_stretch = get_stretch + w.set_stretch = set_stretch + w.remove = remove + + if arg.visible == nil then w.visible = true end + + w:set_position(position) + + table.insert(wiboxes, w) + + w:connect_signal("property::visible", function() reattach(w) end) + + return w +end + +capi.screen.connect_signal("removed", function(s) + for _, wibar in ipairs(wiboxes) do + if wibar._screen == s then + wibar:remove() + end + end +end) + +function awfulwibar.mt:__call(...) + return awfulwibar.new(...) +end + +--Imported documentation + +--- Border width. +-- +-- **Signal:** +-- +-- * *property::border_width* +-- +-- @property border_width +-- @param integer + +--- Border color. +-- +-- Please note that this property only support string based 24 bit or 32 bit +-- colors: +-- +-- Red Blue +-- _| _| +-- #FF00FF +-- T‾ +-- Green +-- +-- +-- Red Blue +-- _| _| +-- #FF00FF00 +-- T‾ ‾T +-- Green Alpha +-- +-- **Signal:** +-- +-- * *property::border_color* +-- +-- @property border_color +-- @param string + +--- On top of other windows. +-- +-- **Signal:** +-- +-- * *property::ontop* +-- +-- @property ontop +-- @param boolean + +--- The mouse cursor. +-- +-- **Signal:** +-- +-- * *property::cursor* +-- +-- @property cursor +-- @param string +-- @see mouse + +--- Visibility. +-- +-- **Signal:** +-- +-- * *property::visible* +-- +-- @property visible +-- @param boolean + +--- The opacity of the wibox, between 0 and 1. +-- +-- **Signal:** +-- +-- * *property::opacity* +-- +-- @property opacity +-- @tparam number opacity (between 0 and 1) + +--- The window type (desktop, normal, dock, ...). +-- +-- **Signal:** +-- +-- * *property::type* +-- +-- @property type +-- @param string +-- @see client.type + +--- The x coordinates. +-- +-- **Signal:** +-- +-- * *property::x* +-- +-- @property x +-- @param integer + +--- The y coordinates. +-- +-- **Signal:** +-- +-- * *property::y* +-- +-- @property y +-- @param integer + +--- The width of the wibox. +-- +-- **Signal:** +-- +-- * *property::width* +-- +-- @property width +-- @param width + +--- The height of the wibox. +-- +-- **Signal:** +-- +-- * *property::height* +-- +-- @property height +-- @param height + +--- The wibox screen. +-- +-- @property screen +-- @param screen + +--- The wibox's `drawable`. +-- +-- **Signal:** +-- +-- * *property::drawable* +-- +-- @property drawable +-- @tparam drawable drawable + +--- The widget that the `wibox` displays. +-- @property widget +-- @param widget + +--- The X window id. +-- +-- **Signal:** +-- +-- * *property::window* +-- +-- @property window +-- @param string +-- @see client.window + +--- The wibox's bounding shape as a (native) cairo surface. +-- +-- **Signal:** +-- +-- * *property::shape_bounding* +-- +-- @property shape_bounding +-- @param surface._native + +--- The wibox's clip shape as a (native) cairo surface. +-- +-- **Signal:** +-- +-- * *property::shape_clip* +-- +-- @property shape_clip +-- @param surface._native + +--- Get or set mouse buttons bindings to a wibox. +-- +-- @param buttons_table A table of buttons objects, or nothing. +-- @function buttons + +--- Get or set wibox geometry. That's the same as accessing or setting the x, +-- y, width or height properties of a wibox. +-- +-- @param A table with coordinates to modify. +-- @return A table with wibox coordinates and geometry. +-- @function geometry + +--- Get or set wibox struts. +-- +-- @param strut A table with new strut, or nothing +-- @return The wibox strut in a table. +-- @function struts +-- @see client.struts + +--- The default background color. +-- @beautiful beautiful.bg_normal +-- @see bg + +--- The default foreground (text) color. +-- @beautiful beautiful.fg_normal +-- @see fg + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @name setup +-- @class function + +--- The background of the wibox. +-- @param c The background to use. This must either be a cairo pattern object, +-- nil or a string that gears.color() understands. +-- @property bg +-- @see gears.color + +--- The background image of the drawable. +-- If `image` is a function, it will be called with `(context, cr, width, height)` +-- as arguments. Any other arguments passed to this method will be appended. +-- @param image A background image or a function +-- @property bgimage +-- @see gears.surface + +--- The foreground (text) of the wibox. +-- @param c The foreground to use. This must either be a cairo pattern object, +-- nil or a string that gears.color() understands. +-- @property fg +-- @see gears.color + +--- Find a widget by a point. +-- The wibox must have drawn itself at least once for this to work. +-- @tparam number x X coordinate of the point +-- @tparam number y Y coordinate of the point +-- @treturn table A sorted table of widgets positions. The first element is the biggest +-- container while the last is the topmost widget. The table contains *x*, *y*, +-- *width*, *height* and *widget*. +-- @name find_widgets +-- @class function + + +return setmetatable(awfulwibar, awfulwibar.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/wibox.lua b/lib/awful/wibox.lua new file mode 100644 index 0000000..e8c7c7f --- /dev/null +++ b/lib/awful/wibox.lua @@ -0,0 +1,12 @@ +--------------------------------------------------------------------------- +--- This module is deprecated and has been renamed `awful.wibar` +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2016 Emmanuel Lepage Vallee +-- @module awful.wibox +--------------------------------------------------------------------------- +local util = require("awful.util") + +return util.deprecate_class(require("awful.wibar"), "awful.wibox", "awful.wibar") + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/button.lua b/lib/awful/widget/button.lua new file mode 100644 index 0000000..388f9dd --- /dev/null +++ b/lib/awful/widget/button.lua @@ -0,0 +1,263 @@ +--------------------------------------------------------------------------- +-- A simple button widget. +-- @usage local button = awful.widget.button() +-- button:buttons(awful.util.table.join( +-- button:buttons(), +-- awful.button({}, 1, nil, function () +-- print("Mouse was clicked") +-- end) +-- )) +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008-2009 Julien Danjou +-- @classmod awful.widget.button +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local abutton = require("awful.button") +local imagebox = require("wibox.widget.imagebox") +local widget = require("wibox.widget.base") +local surface = require("gears.surface") +local cairo = require("lgi").cairo + +local button = { mt = {} } + +--- Create a button widget. When clicked, the image is deplaced to make it like +-- a real button. +-- +-- @param args Widget arguments. "image" is the image to display. +-- @return A textbox widget configured as a button. +function button.new(args) + if not args or not args.image then + return widget.empty_widget() + end + + local w = imagebox() + local orig_set_image = w.set_image + local img_release + local img_press + + function w:set_image(image) + img_release = surface.load(image) + img_press = img_release:create_similar(cairo.Content.COLOR_ALPHA, img_release.width, img_release.height) + local cr = cairo.Context(img_press) + cr:set_source_surface(img_release, 2, 2) + cr:paint() + orig_set_image(self, img_release) + end + w:set_image(args.image) + w:buttons(abutton({}, 1, function () orig_set_image(w, img_press) end, function () orig_set_image(w, img_release) end)) + + w:connect_signal("mouse::leave", function(self) orig_set_image(self, img_release) end) + + return w +end + +function button.mt:__call(...) + return button.new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(button, button.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/common.lua b/lib/awful/widget/common.lua new file mode 100644 index 0000000..e9ae699 --- /dev/null +++ b/lib/awful/widget/common.lua @@ -0,0 +1,119 @@ +--------------------------------------------------------------------------- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008-2009 Julien Danjou +-- @classmod awful.widget.common +--------------------------------------------------------------------------- + +-- Grab environment we need +local type = type +local ipairs = ipairs +local capi = { button = button } +local wibox = require("wibox") +local dpi = require("beautiful").xresources.apply_dpi + +--- Common utilities for awful widgets +local common = {} + +--- Common method to create buttons. +-- @tab buttons +-- @param object +-- @treturn table +function common.create_buttons(buttons, object) + if buttons then + local btns = {} + for _, b in ipairs(buttons) do + -- Create a proxy button object: it will receive the real + -- press and release events, and will propagate them to the + -- button object the user provided, but with the object as + -- argument. + local btn = capi.button { modifiers = b.modifiers, button = b.button } + btn:connect_signal("press", function () b:emit_signal("press", object) end) + btn:connect_signal("release", function () b:emit_signal("release", object) end) + btns[#btns + 1] = btn + end + + return btns + end +end + +--- Common update method. +-- @param w The widget. +-- @tab buttons +-- @func label Function to generate label parameters from an object. +-- The function gets passed an object from `objects`, and +-- has to return `text`, `bg`, `bg_image`, `icon`. +-- @tab data Current data/cache, indexed by objects. +-- @tab objects Objects to be displayed / updated. +function common.list_update(w, buttons, label, data, objects) + -- update the widgets, creating them if needed + w:reset() + for i, o in ipairs(objects) do + local cache = data[o] + local ib, tb, bgb, tbm, ibm, l + if cache then + ib = cache.ib + tb = cache.tb + bgb = cache.bgb + tbm = cache.tbm + ibm = cache.ibm + else + ib = wibox.widget.imagebox() + tb = wibox.widget.textbox() + bgb = wibox.container.background() + tbm = wibox.container.margin(tb, dpi(4), dpi(4)) + ibm = wibox.container.margin(ib, dpi(4)) + l = wibox.layout.fixed.horizontal() + + -- All of this is added in a fixed widget + l:fill_space(true) + l:add(ibm) + l:add(tbm) + + -- And all of this gets a background + bgb:set_widget(l) + + bgb:buttons(common.create_buttons(buttons, o)) + + data[o] = { + ib = ib, + tb = tb, + bgb = bgb, + tbm = tbm, + ibm = ibm, + } + end + + local text, bg, bg_image, icon, args = label(o, tb) + args = args or {} + + -- The text might be invalid, so use pcall. + if text == nil or text == "" then + tbm:set_margins(0) + else + if not tb:set_markup_silently(text) then + tb:set_markup("<Invalid text>") + end + end + bgb:set_bg(bg) + if type(bg_image) == "function" then + -- TODO: Why does this pass nil as an argument? + bg_image = bg_image(tb,o,nil,objects,i) + end + bgb:set_bgimage(bg_image) + if icon then + ib:set_image(icon) + else + ibm:set_margins(0) + end + + bgb.shape = args.shape + bgb.shape_border_width = args.shape_border_width + bgb.shape_border_color = args.shape_border_color + + w:add(bgb) + end +end + +return common + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/graph.lua b/lib/awful/widget/graph.lua new file mode 100644 index 0000000..e231463 --- /dev/null +++ b/lib/awful/widget/graph.lua @@ -0,0 +1,16 @@ +--------------------------------------------------------------------------- +--- This module has been moved to `wibox.widget.graph` +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @classmod awful.widget.graph +--------------------------------------------------------------------------- +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.widget.graph"), + "awful.widget.graph", + "wibox.widget.graph" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/init.lua b/lib/awful/widget/init.lua new file mode 100644 index 0000000..02400a1 --- /dev/null +++ b/lib/awful/widget/init.lua @@ -0,0 +1,24 @@ +--------------------------------------------------------------------------- +--- Widget module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008-2009 Julien Danjou +-- @classmod awful.widget +--------------------------------------------------------------------------- + +return +{ + taglist = require("awful.widget.taglist"); + tasklist = require("awful.widget.tasklist"); + button = require("awful.widget.button"); + launcher = require("awful.widget.launcher"); + prompt = require("awful.widget.prompt"); + progressbar = require("awful.widget.progressbar"); + graph = require("awful.widget.graph"); + layoutbox = require("awful.widget.layoutbox"); + textclock = require("awful.widget.textclock"); + keyboardlayout = require("awful.widget.keyboardlayout"); + watch = require("awful.widget.watch"); +} + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/keyboardlayout.lua b/lib/awful/widget/keyboardlayout.lua new file mode 100644 index 0000000..4f79a81 --- /dev/null +++ b/lib/awful/widget/keyboardlayout.lua @@ -0,0 +1,308 @@ +--------------------------------------------------------------------------- +-- @author Aleksey Fedotov <lexa@cfotr.com> +-- @copyright 2015 Aleksey Fedotov +-- @classmod wibox.widget.keyboardlayout +--------------------------------------------------------------------------- + +local capi = {awesome = awesome} +local setmetatable = setmetatable +local textbox = require("wibox.widget.textbox") +local button = require("awful.button") +local util = require("awful.util") +local widget_base = require("wibox.widget.base") +local gdebug = require("gears.debug") + +--- Keyboard Layout widget. +-- awful.widget.keyboardlayout +local keyboardlayout = { mt = {} } + +-- As to the country-code-like symbols below, refer to the names of XKB's +-- data files in /.../xkb/symbols/*. +keyboardlayout.xkeyboard_country_code = { + ["ad"] = true, -- Andorra + ["af"] = true, -- Afganistan + ["al"] = true, -- Albania + ["am"] = true, -- Armenia + ["ara"] = true, -- Arabic + ["at"] = true, -- Austria + ["az"] = true, -- Azerbaijan + ["ba"] = true, -- Bosnia and Herzegovina + ["bd"] = true, -- Bangladesh + ["be"] = true, -- Belgium + ["bg"] = true, -- Bulgaria + ["br"] = true, -- Brazil + ["bt"] = true, -- Bhutan + ["bw"] = true, -- Botswana + ["by"] = true, -- Belarus + ["ca"] = true, -- Canada + ["cd"] = true, -- Congo + ["ch"] = true, -- Switzerland + ["cm"] = true, -- Cameroon + ["cn"] = true, -- China + ["cz"] = true, -- Czechia + ["de"] = true, -- Germany + ["dk"] = true, -- Denmark + ["ee"] = true, -- Estonia + ["epo"] = true, -- Esperanto + ["es"] = true, -- Spain + ["et"] = true, -- Ethiopia + ["fi"] = true, -- Finland + ["fo"] = true, -- Faroe Islands + ["fr"] = true, -- France + ["gb"] = true, -- United Kingdom + ["ge"] = true, -- Georgia + ["gh"] = true, -- Ghana + ["gn"] = true, -- Guinea + ["gr"] = true, -- Greece + ["hr"] = true, -- Croatia + ["hu"] = true, -- Hungary + ["ie"] = true, -- Ireland + ["il"] = true, -- Israel + ["in"] = true, -- India + ["iq"] = true, -- Iraq + ["ir"] = true, -- Iran + ["is"] = true, -- Iceland + ["it"] = true, -- Italy + ["jp"] = true, -- Japan + ["ke"] = true, -- Kenya + ["kg"] = true, -- Kyrgyzstan + ["kh"] = true, -- Cambodia + ["kr"] = true, -- Korea + ["kz"] = true, -- Kazakhstan + ["la"] = true, -- Laos + ["latam"] = true, -- Latin America + ["latin"] = true, -- Latin + ["lk"] = true, -- Sri Lanka + ["lt"] = true, -- Lithuania + ["lv"] = true, -- Latvia + ["ma"] = true, -- Morocco + ["mao"] = true, -- Maori + ["me"] = true, -- Montenegro + ["mk"] = true, -- Macedonia + ["ml"] = true, -- Mali + ["mm"] = true, -- Myanmar + ["mn"] = true, -- Mongolia + ["mt"] = true, -- Malta + ["mv"] = true, -- Maldives + ["ng"] = true, -- Nigeria + ["nl"] = true, -- Netherlands + ["no"] = true, -- Norway + ["np"] = true, -- Nepal + ["ph"] = true, -- Philippines + ["pk"] = true, -- Pakistan + ["pl"] = true, -- Poland + ["pt"] = true, -- Portugal + ["ro"] = true, -- Romania + ["rs"] = true, -- Serbia + ["ru"] = true, -- Russia + ["se"] = true, -- Sweden + ["si"] = true, -- Slovenia + ["sk"] = true, -- Slovakia + ["sn"] = true, -- Senegal + ["sy"] = true, -- Syria + ["th"] = true, -- Thailand + ["tj"] = true, -- Tajikistan + ["tm"] = true, -- Turkmenistan + ["tr"] = true, -- Turkey + ["tw"] = true, -- Taiwan + ["tz"] = true, -- Tanzania + ["ua"] = true, -- Ukraine + ["us"] = true, -- USA + ["uz"] = true, -- Uzbekistan + ["vn"] = true, -- Vietnam + ["za"] = true, -- South Africa +} + +-- Callback for updating current layout. +local function update_status (self) + self._current = awesome.xkb_get_layout_group(); + local text = "" + if (#self._layout > 0) then + text = (" " .. self._layout[self._current] .. " ") + end + self.widget:set_text(text) +end + +--- Auxiliary function for the local function update_layout(). +-- Create an array whose element is a table consisting of the four fields: +-- vendor, file, section and group_idx, which all correspond to the +-- xkb_symbols pattern "vendor/file(section):group_idx". +-- @tparam string group_names The string awesome.xkb_get_group_names() returns. +-- @treturn table An array of tables whose keys are vendor, file, section, and group_idx. +function keyboardlayout.get_groups_from_group_names(group_names) + if group_names == nil then + return nil + end + + -- Pattern elements to be captured. + local word_pat = "([%w_]+)" + local sec_pat = "(%b())" + local idx_pat = ":(%d)" + -- Pairs of a pattern and its callback. In callbacks, set 'group_idx' to 1 + -- and return it if there's no specification on 'group_idx' in the given + -- pattern. + local pattern_and_callback_pairs = { + -- vendor/file(section):group_idx + ["^" .. word_pat .. "/" .. word_pat .. sec_pat .. idx_pat .. "$"] + = function(token, pattern) + local vendor, file, section, group_idx = string.match(token, pattern) + return vendor, file, section, group_idx + end, + -- vendor/file(section) + ["^" .. word_pat .. "/" .. word_pat .. sec_pat .. "$"] + = function(token, pattern) + local vendor, file, section = string.match(token, pattern) + return vendor, file, section, 1 + end, + -- vendor/file:group_idx + ["^" .. word_pat .. "/" .. word_pat .. idx_pat .. "$"] + = function(token, pattern) + local vendor, file, group_idx = string.match(token, pattern) + return vendor, file, nil, group_idx + end, + -- vendor/file + ["^" .. word_pat .. "/" .. word_pat .. "$"] + = function(token, pattern) + local vendor, file = string.match(token, pattern) + return vendor, file, nil, 1 + end, + -- file(section):group_idx + ["^" .. word_pat .. sec_pat .. idx_pat .. "$"] + = function(token, pattern) + local file, section, group_idx = string.match(token, pattern) + return nil, file, section, group_idx + end, + -- file(section) + ["^" .. word_pat .. sec_pat .. "$"] + = function(token, pattern) + local file, section = string.match(token, pattern) + return nil, file, section, 1 + end, + -- file:group_idx + ["^" .. word_pat .. idx_pat .. "$"] + = function(token, pattern) + local file, group_idx = string.match(token, pattern) + return nil, file, nil, group_idx + end, + -- file + ["^" .. word_pat .. "$"] + = function(token, pattern) + local file = string.match(token, pattern) + return nil, file, nil, 1 + end + } + + -- Split 'group_names' into 'tokens'. The separator is "+". + local tokens = {} + string.gsub(group_names, "[^+]+", function(match) + table.insert(tokens, match) + end) + + -- For each token in 'tokens', check if it matches one of the patterns in + -- the array 'pattern_and_callback_pairs', where the patterns are used as + -- key. If a match is found, extract captured strings using the + -- corresponding callback function. Check if those extracted is country + -- specific part of a layout. If so, add it to 'layout_groups'; otherwise, + -- ignore it. + local layout_groups = {} + for i = 1, #tokens do + for pattern, callback in pairs(pattern_and_callback_pairs) do + local vendor, file, section, group_idx = callback(tokens[i], pattern) + if file then + if not keyboardlayout.xkeyboard_country_code[file] then + break + end + + if section then + section = string.gsub(section, "%(([%w-_]+)%)", "%1") + end + + table.insert(layout_groups, { vendor = vendor, + file = file, + section = section, + group_idx = tonumber(group_idx) }) + break + end + end + end + + return layout_groups +end + +-- Callback for updating list of layouts +local function update_layout(self) + self._layout = {}; + local layouts = keyboardlayout.get_groups_from_group_names(awesome.xkb_get_group_names()) + if layouts == nil or layouts[1] == nil then + gdebug.print_error("Failed to get list of keyboard groups") + return + end + if #layouts == 1 then + layouts[1].group_idx = 0 + end + for _, v in ipairs(layouts) do + local layout_name = self.layout_name(v) + -- Please note that numbers of groups reported by xkb_get_group_names + -- is greater by one than the real group number. + self._layout[v.group_idx - 1] = layout_name + end + update_status(self) +end + +--- Create a keyboard layout widget. It shows current keyboard layout name in a textbox. +-- @return A keyboard layout widget. +function keyboardlayout.new() + local widget = textbox() + local self = widget_base.make_widget(widget) + + self.widget = widget + + self.layout_name = function(v) + local name = v.file + if v.section ~= nil then + name = name .. "(" .. v.section .. ")" + end + return name + end + + self.next_layout = function() + self.set_layout((self._current + 1) % (#self._layout + 1)) + end + + self.set_layout = function(group_number) + if (0 > group_number) or (group_number > #self._layout) then + error("Invalid group number: " .. group_number .. + "expected number from 0 to " .. #self._layout) + return; + end + awesome.xkb_set_layout_group(group_number); + end + + update_layout(self); + + -- callback for processing layout changes + capi.awesome.connect_signal("xkb::map_changed", + function () update_layout(self) end) + capi.awesome.connect_signal("xkb::group_changed", + function () update_status(self) end); + + -- Mouse bindings + self:buttons( + util.table.join(button({ }, 1, self.next_layout)) + ) + + return self +end + +local _instance = nil; + +function keyboardlayout.mt:__call(...) + if _instance == nil then + _instance = keyboardlayout.new(...) + end + return _instance +end + +return setmetatable(keyboardlayout, keyboardlayout.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/launcher.lua b/lib/awful/widget/launcher.lua new file mode 100644 index 0000000..a944908 --- /dev/null +++ b/lib/awful/widget/launcher.lua @@ -0,0 +1,41 @@ +--------------------------------------------------------------------------- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008-2009 Julien Danjou +-- @classmod awful.widget.launcher +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local util = require("awful.util") +local spawn = require("awful.spawn") +local wbutton = require("awful.widget.button") +local button = require("awful.button") + +local launcher = { mt = {} } + +--- Create a button widget which will launch a command. +-- @param args Standard widget table arguments, plus image for the image path +-- and command for the command to run on click, or either menu to create menu. +-- @return A launcher widget. +function launcher.new(args) + if not args.command and not args.menu then return end + local w = wbutton(args) + if not w then return end + + local b + if args.command then + b = util.table.join(w:buttons(), button({}, 1, nil, function () spawn(args.command) end)) + elseif args.menu then + b = util.table.join(w:buttons(), button({}, 1, nil, function () args.menu:toggle() end)) + end + + w:buttons(b) + return w +end + +function launcher.mt:__call(...) + return launcher.new(...) +end + +return setmetatable(launcher, launcher.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/layoutbox.lua b/lib/awful/widget/layoutbox.lua new file mode 100644 index 0000000..c5f8ed3 --- /dev/null +++ b/lib/awful/widget/layoutbox.lua @@ -0,0 +1,73 @@ +--------------------------------------------------------------------------- +--- Layoutbox widget. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @classmod awful.widget.layoutbox +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local capi = { screen = screen, tag = tag } +local layout = require("awful.layout") +local tooltip = require("awful.tooltip") +local beautiful = require("beautiful") +local imagebox = require("wibox.widget.imagebox") + +local function get_screen(s) + return s and capi.screen[s] +end + +local layoutbox = { mt = {} } + +local boxes = nil + +local function update(w, screen) + screen = get_screen(screen) + local name = layout.getname(layout.get(screen)) + w._layoutbox_tooltip:set_text(name or "[no name]") + w:set_image(name and beautiful["layout_" .. name]) +end + +local function update_from_tag(t) + local screen = get_screen(t.screen) + local w = boxes[screen] + if w then + update(w, screen) + end +end + +--- Create a layoutbox widget. It draws a picture with the current layout +-- symbol of the current tag. +-- @param screen The screen number that the layout will be represented for. +-- @return An imagebox widget configured as a layoutbox. +function layoutbox.new(screen) + screen = get_screen(screen or 1) + + -- Do we already have the update callbacks registered? + if boxes == nil then + boxes = setmetatable({}, { __mode = "kv" }) + capi.tag.connect_signal("property::selected", update_from_tag) + capi.tag.connect_signal("property::layout", update_from_tag) + layoutbox.boxes = boxes + end + + -- Do we already have a layoutbox for this screen? + local w = boxes[screen] + if not w then + w = imagebox() + w._layoutbox_tooltip = tooltip {objects = {w}, delay_show = 1} + + update(w, screen) + boxes[screen] = w + end + + return w +end + +function layoutbox.mt:__call(...) + return layoutbox.new(...) +end + +return setmetatable(layoutbox, layoutbox.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/progressbar.lua b/lib/awful/widget/progressbar.lua new file mode 100644 index 0000000..11147f4 --- /dev/null +++ b/lib/awful/widget/progressbar.lua @@ -0,0 +1,16 @@ +--------------------------------------------------------------------------- +--- This module has been moved to `wibox.widget.progressbar` +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @classmod awful.widget.progressbar +--------------------------------------------------------------------------- +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.widget.progressbar"), + "awful.widget.progressbar", + "wibox.widget.progressbar" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/prompt.lua b/lib/awful/widget/prompt.lua new file mode 100644 index 0000000..ff9d904 --- /dev/null +++ b/lib/awful/widget/prompt.lua @@ -0,0 +1,64 @@ +--------------------------------------------------------------------------- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @classmod awful.widget.prompt +--------------------------------------------------------------------------- + +local setmetatable = setmetatable + +local completion = require("awful.completion") +local util = require("awful.util") +local spawn = require("awful.spawn") +local prompt = require("awful.prompt") +local widget_base = require("wibox.widget.base") +local textbox = require("wibox.widget.textbox") +local type = type + +local widgetprompt = { mt = {} } + +--- Run method for promptbox. +-- +-- @param promptbox The promptbox to run. +local function run(promptbox) + return prompt.run { + prompt = promptbox.prompt, + textbox = promptbox.widget, + completion_callback = completion.shell, + history_path = util.get_cache_dir() .. "/history", + exe_callback = function (...) + promptbox:spawn_and_handle_error(...) + end, + } +end + +local function spawn_and_handle_error(self, ...) + local result = spawn(...) + if type(result) == "string" then + self.widget:set_text(result) + end +end + +--- Create a prompt widget which will launch a command. +-- +-- @param args Arguments table. "prompt" is the prompt to use. +-- @return A launcher widget. +function widgetprompt.new(args) + args = args or {} + local widget = textbox() + local promptbox = widget_base.make_widget(widget) + + promptbox.widget = widget + promptbox.widget:set_ellipsize("start") + promptbox.run = run + promptbox.spawn_and_handle_error = spawn_and_handle_error + promptbox.prompt = args.prompt or "Run: " + return promptbox +end + +function widgetprompt.mt:__call(...) + return widgetprompt.new(...) +end + +return setmetatable(widgetprompt, widgetprompt.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/taglist.lua b/lib/awful/widget/taglist.lua new file mode 100644 index 0000000..d8cf475 --- /dev/null +++ b/lib/awful/widget/taglist.lua @@ -0,0 +1,452 @@ +--------------------------------------------------------------------------- +--- Taglist widget module for awful +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008-2009 Julien Danjou +-- @classmod awful.widget.taglist +--------------------------------------------------------------------------- + +-- Grab environment we need +local capi = { screen = screen, + awesome = awesome, + client = client } +local setmetatable = setmetatable +local pairs = pairs +local ipairs = ipairs +local table = table +local common = require("awful.widget.common") +local util = require("awful.util") +local tag = require("awful.tag") +local beautiful = require("beautiful") +local fixed = require("wibox.layout.fixed") +local surface = require("gears.surface") +local timer = require("gears.timer") + +local function get_screen(s) + return s and capi.screen[s] +end + +local taglist = { mt = {} } +taglist.filter = {} + +--- The tag list main foreground (text) color. +-- @beautiful beautiful.taglist_fg_focus +-- @param[opt=fg_focus] color +-- @see gears.color + +--- The tag list main background color. +-- @beautiful beautiful.taglist_bg_focus +-- @param[opt=bg_focus] color +-- @see gears.color + +--- The tag list urgent elements foreground (text) color. +-- @beautiful beautiful.taglist_fg_urgent +-- @param[opt=fg_urgent] color +-- @see gears.color + +--- The tag list urgent elements background color. +-- @beautiful beautiful.taglist_bg_urgent +-- @param[opt=bg_urgent] color +-- @see gears.color + +--- The tag list occupied elements background color. +-- @beautiful beautiful.taglist_bg_occupied +-- @param color +-- @see gears.color + +--- The tag list occupied elements foreground (text) color. +-- @beautiful beautiful.taglist_fg_occupied +-- @param color +-- @see gears.color + +--- The tag list empty elements background color. +-- @beautiful beautiful.taglist_bg_empty +-- @param color +-- @see gears.color + +--- The tag list empty elements foreground (text) color. +-- @beautiful beautiful.taglist_fg_empty +-- @param color +-- @see gears.color + +--- The selected elements background image. +-- @beautiful beautiful.taglist_squares_sel +-- @param surface +-- @see gears.surface + +--- The unselected elements background image. +-- @beautiful beautiful.taglist_squares_unsel +-- @param surface +-- @see gears.surface + +--- The selected empty elements background image. +-- @beautiful beautiful.taglist_squares_sel_empty +-- @param surface +-- @see gears.surface + +--- The unselected empty elements background image. +-- @beautiful beautiful.taglist_squares_unsel_empty +-- @param surface +-- @see gears.surface + +--- If the background images can be resized. +-- @beautiful beautiful.taglist_squares_resize +-- @param boolean + +--- Do not display the tag icons, even if they are set. +-- @beautiful beautiful.taglist_disable_icon +-- @param boolean + +--- The taglist font. +-- @beautiful beautiful.taglist_font +-- @param string + +--- The main shape used for the elements. +-- This will be the fallback for state specific shapes. +-- To get a shape for the whole taglist, use `wibox.container.background`. +-- @beautiful beautiful.taglist_shape +-- @param[opt=rectangle] gears.shape +-- @see gears.shape +-- @see beautiful.taglist_shape_empty +-- @see beautiful.taglist_shape_focus +-- @see beautiful.taglist_shape_urgent + +--- The shape elements border width. +-- @beautiful beautiful.taglist_shape_border_width +-- @param[opt=0] number +-- @see wibox.container.background + +--- The elements shape border color. +-- @beautiful beautiful.taglist_shape_border_color +-- @param color +-- @see gears.color + +--- The shape used for the empty elements. +-- @beautiful beautiful.taglist_shape_empty +-- @param[opt=rectangle] gears.shape +-- @see gears.shape + +--- The shape used for the empty elements border width. +-- @beautiful beautiful.taglist_shape_border_width_empty +-- @param[opt=0] number +-- @see wibox.container.background + +--- The empty elements shape border color. +-- @beautiful beautiful.taglist_shape_border_color_empty +-- @param color +-- @see gears.color + +--- The shape used for the selected elements. +-- @beautiful beautiful.taglist_shape_focus +-- @param[opt=rectangle] gears.shape +-- @see gears.shape + +--- The shape used for the selected elements border width. +-- @beautiful beautiful.taglist_shape_border_width_focus +-- @param[opt=0] number +-- @see wibox.container.background + +--- The selected elements shape border color. +-- @beautiful beautiful.taglist_shape_border_color_focus +-- @param color +-- @see gears.color + +--- The shape used for the urgent elements. +-- @beautiful beautiful.taglist_shape_urgent +-- @param[opt=rectangle] gears.shape +-- @see gears.shape + +--- The shape used for the urgent elements border width. +-- @beautiful beautiful.taglist_shape_border_width_urgent +-- @param[opt=0] number +-- @see wibox.container.background + +--- The urgents elements shape border color. +-- @beautiful beautiful.taglist_shape_border_color_urgent +-- @param color +-- @see gears.color + +local instances = nil + +function taglist.taglist_label(t, args) + if not args then args = {} end + local theme = beautiful.get() + local fg_focus = args.fg_focus or theme.taglist_fg_focus or theme.fg_focus + local bg_focus = args.bg_focus or theme.taglist_bg_focus or theme.bg_focus + local fg_urgent = args.fg_urgent or theme.taglist_fg_urgent or theme.fg_urgent + local bg_urgent = args.bg_urgent or theme.taglist_bg_urgent or theme.bg_urgent + local bg_occupied = args.bg_occupied or theme.taglist_bg_occupied + local fg_occupied = args.fg_occupied or theme.taglist_fg_occupied + local bg_empty = args.bg_empty or theme.taglist_bg_empty + local fg_empty = args.fg_empty or theme.taglist_fg_empty + local taglist_squares_sel = args.squares_sel or theme.taglist_squares_sel + local taglist_squares_unsel = args.squares_unsel or theme.taglist_squares_unsel + local taglist_squares_sel_empty = args.squares_sel_empty or theme.taglist_squares_sel_empty + local taglist_squares_unsel_empty = args.squares_unsel_empty or theme.taglist_squares_unsel_empty + local taglist_squares_resize = theme.taglist_squares_resize or args.squares_resize or "true" + local taglist_disable_icon = args.taglist_disable_icon or theme.taglist_disable_icon or false + local font = args.font or theme.taglist_font or theme.font or "" + local text = nil + local sel = capi.client.focus + local bg_color = nil + local fg_color = nil + local bg_image + local icon + local shape = args.shape or theme.taglist_shape + local shape_border_width = args.shape_border_width or theme.taglist_shape_border_width + local shape_border_color = args.shape_border_color or theme.taglist_shape_border_color + -- TODO: Re-implement bg_resize + local bg_resize = false -- luacheck: ignore + local is_selected = false + local cls = t:clients() + + if sel and taglist_squares_sel then + -- Check that the selected client is tagged with 't'. + local seltags = sel:tags() + for _, v in ipairs(seltags) do + if v == t then + bg_image = taglist_squares_sel + bg_resize = taglist_squares_resize == "true" + is_selected = true + break + end + end + end + if #cls == 0 and t.selected and taglist_squares_sel_empty then + bg_image = taglist_squares_sel_empty + bg_resize = taglist_squares_resize == "true" + elseif not is_selected then + if #cls > 0 then + if taglist_squares_unsel then + bg_image = taglist_squares_unsel + bg_resize = taglist_squares_resize == "true" + end + if bg_occupied then bg_color = bg_occupied end + if fg_occupied then fg_color = fg_occupied end + else + if taglist_squares_unsel_empty then + bg_image = taglist_squares_unsel_empty + bg_resize = taglist_squares_resize == "true" + end + if bg_empty then bg_color = bg_empty end + if fg_empty then fg_color = fg_empty end + + if args.shape_empty or theme.taglist_shape_empty then + shape = args.shape_empty or theme.taglist_shape_empty + end + + if args.shape_border_width_empty or theme.taglist_shape_border_width_empty then + shape_border_width = args.shape_border_width_empty or theme.taglist_shape_border_width_empty + end + + if args.shape_border_color_empty or theme.taglist_shape_border_color_empty then + shape_border_color = args.shape_border_color_empty or theme.taglist_shape_border_color_empty + end + end + end + if t.selected then + bg_color = bg_focus + fg_color = fg_focus + + if args.shape_focus or theme.taglist_shape_focus then + shape = args.shape_focus or theme.taglist_shape_focus + end + + if args.shape_border_width_focus or theme.taglist_shape_border_width_focus then + shape = args.shape_border_width_focus or theme.taglist_shape_border_width_focus + end + + if args.shape_border_color_focus or theme.taglist_shape_border_color_focus then + shape = args.shape_border_color_focus or theme.taglist_shape_border_color_focus + end + + elseif tag.getproperty(t, "urgent") then + if bg_urgent then bg_color = bg_urgent end + if fg_urgent then fg_color = fg_urgent end + + if args.shape_urgent or theme.taglist_shape_urgent then + shape = args.shape_urgent or theme.taglist_shape_urgent + end + + if args.shape_border_width_urgent or theme.taglist_shape_border_width_urgent then + shape_border_width = args.shape_border_width_urgent or theme.taglist_shape_border_width_urgent + end + + if args.shape_border_color_urgent or theme.taglist_shape_border_color_urgent then + shape_border_color = args.shape_border_color_urgent or theme.taglist_shape_border_color_urgent + end + end + + if not tag.getproperty(t, "icon_only") then + text = "" + if fg_color then + text = text .. "" .. (util.escape(t.name) or "") .. "" + else + text = text .. (util.escape(t.name) or "") + end + text = text .. "" + end + if not taglist_disable_icon then + if t.icon then + icon = surface.load(t.icon) + end + end + + local other_args = { + shape = shape, + shape_border_width = shape_border_width, + shape_border_color = shape_border_color, + } + + return text, bg_color, bg_image, not taglist_disable_icon and icon or nil, other_args +end + +local function taglist_update(s, w, buttons, filter, data, style, update_function) + local tags = {} + for _, t in ipairs(s.tags) do + if not tag.getproperty(t, "hide") and filter(t) then + table.insert(tags, t) + end + end + + local function label(c) return taglist.taglist_label(c, style) end + + update_function(w, buttons, label, data, tags) +end + +--- Create a new taglist widget. The last two arguments (update_function +-- and base_widget) serve to customize the layout of the taglist (eg. to +-- make it vertical). For that, you will need to copy the +-- awful.widget.common.list_update function, make your changes to it +-- and pass it as update_function here. Also change the base_widget if the +-- default is not what you want. +-- @param screen The screen to draw taglist for. +-- @param filter Filter function to define what clients will be listed. +-- @param buttons A table with buttons binding to set. +-- @tparam[opt={}] table style The style overrides default theme. +-- @tparam[opt=nil] string|pattern style.fg_focus +-- @tparam[opt=nil] string|pattern style.bg_focus +-- @tparam[opt=nil] string|pattern style.fg_urgent +-- @tparam[opt=nil] string|pattern style.bg_urgent +-- @tparam[opt=nil] string|pattern style.bg_occupied +-- @tparam[opt=nil] string|pattern style.fg_occupied +-- @tparam[opt=nil] string|pattern style.bg_empty +-- @tparam[opt=nil] string|pattern style.fg_empty +-- @tparam[opt=nil] string style.taglist_squares_sel +-- @tparam[opt=nil] string style.taglist_squares_unsel +-- @tparam[opt=nil] string style.taglist_squares_sel_empty +-- @tparam[opt=nil] string style.taglist_squares_unsel_empty +-- @tparam[opt=nil] string style.taglist_squares_resize +-- @tparam[opt=nil] string style.taglist_disable_icon +-- @tparam[opt=nil] string style.font +-- @tparam[opt=nil] number style.spacing The spacing between tags. +-- @param[opt] update_function Function to create a tag widget on each +-- update. See `awful.widget.common`. +-- @param[opt] base_widget Optional container widget for tag widgets. Default +-- is wibox.layout.fixed.horizontal(). +-- @param base_widget.bg_focus The background color for focused client. +-- @param base_widget.fg_focus The foreground color for focused client. +-- @param base_widget.bg_urgent The background color for urgent clients. +-- @param base_widget.fg_urgent The foreground color for urgent clients. +-- @param[opt] base_widget.squares_sel A user provided image for selected squares. +-- @param[opt] base_widget.squares_unsel A user provided image for unselected squares. +-- @param[opt] base_widget.squares_sel_empty A user provided image for selected squares for empty tags. +-- @param[opt] base_widget.squares_unsel_empty A user provided image for unselected squares for empty tags. +-- @param[opt] base_widget.squares_resize True or false to resize squares. +-- @param base_widget.font The font. +-- @function awful.taglist +function taglist.new(screen, filter, buttons, style, update_function, base_widget) + screen = get_screen(screen) + local uf = update_function or common.list_update + local w = base_widget or fixed.horizontal() + + if w.set_spacing and (style and style.spacing or beautiful.taglist_spacing) then + w:set_spacing(style and style.spacing or beautiful.taglist_spacing) + end + + local data = setmetatable({}, { __mode = 'k' }) + + local queued_update = {} + function w._do_taglist_update() + -- Add a delayed callback for the first update. + if not queued_update[screen] then + timer.delayed_call(function() + if screen.valid then + taglist_update(screen, w, buttons, filter, data, style, uf) + end + queued_update[screen] = false + end) + queued_update[screen] = true + end + end + if instances == nil then + instances = setmetatable({}, { __mode = "k" }) + local function u(s) + local i = instances[get_screen(s)] + if i then + for _, tlist in pairs(i) do + tlist._do_taglist_update() + end + end + end + local uc = function (c) return u(c.screen) end + local ut = function (t) return u(t.screen) end + capi.client.connect_signal("focus", uc) + capi.client.connect_signal("unfocus", uc) + tag.attached_connect_signal(nil, "property::selected", ut) + tag.attached_connect_signal(nil, "property::icon", ut) + tag.attached_connect_signal(nil, "property::hide", ut) + tag.attached_connect_signal(nil, "property::name", ut) + tag.attached_connect_signal(nil, "property::activated", ut) + tag.attached_connect_signal(nil, "property::screen", ut) + tag.attached_connect_signal(nil, "property::index", ut) + tag.attached_connect_signal(nil, "property::urgent", ut) + capi.client.connect_signal("property::screen", function(c, old_screen) + u(c.screen) + u(old_screen) + end) + capi.client.connect_signal("tagged", uc) + capi.client.connect_signal("untagged", uc) + capi.client.connect_signal("unmanage", uc) + capi.screen.connect_signal("removed", function(s) + instances[get_screen(s)] = nil + end) + end + w._do_taglist_update() + local list = instances[screen] + if not list then + list = setmetatable({}, { __mode = "v" }) + instances[screen] = list + end + table.insert(list, w) + return w +end + +--- Filtering function to include all nonempty tags on the screen. +-- @param t The tag. +-- @return true if t is not empty, else false +function taglist.filter.noempty(t) + return #t:clients() > 0 or t.selected +end + +--- Filtering function to include selected tags on the screen. +-- @param t The tag. +-- @return true if t is not empty, else false +function taglist.filter.selected(t) + return t.selected +end + +--- Filtering function to include all tags on the screen. +-- @return true +function taglist.filter.all() + return true +end + +function taglist.mt:__call(...) + return taglist.new(...) +end + +return setmetatable(taglist, taglist.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/tasklist.lua b/lib/awful/widget/tasklist.lua new file mode 100644 index 0000000..d5580c1 --- /dev/null +++ b/lib/awful/widget/tasklist.lua @@ -0,0 +1,573 @@ +--------------------------------------------------------------------------- +--- Tasklist widget module for awful. +-- +-- +-- **Status icons:** +-- +-- By default, the tasklist prepends some symbols in front of the client name. +-- This is used to notify that the client has some specific properties that are +-- currently enabled. This can be disabled using +-- `beautiful.tasklist_plain_task_name`=true in the theme. +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
IconClient property
sticky
ontop
above
below
floating
+maximized
maximized_horizontal
maximized_vertical
+-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008-2009 Julien Danjou +-- @classmod awful.widget.tasklist +--------------------------------------------------------------------------- + +-- Grab environment we need +local capi = { screen = screen, + client = client } +local ipairs = ipairs +local setmetatable = setmetatable +local table = table +local common = require("awful.widget.common") +local beautiful = require("beautiful") +local util = require("awful.util") +local tag = require("awful.tag") +local flex = require("wibox.layout.flex") +local timer = require("gears.timer") + +local function get_screen(s) + return s and screen[s] +end + +local tasklist = { mt = {} } + +local instances + +--- The default foreground (text) color. +-- @beautiful beautiful.tasklist_fg_normal +-- @tparam[opt=nil] string|pattern fg_normal +-- @see gears.color + +--- The default background color. +-- @beautiful beautiful.tasklist_bg_normal +-- @tparam[opt=nil] string|pattern bg_normal +-- @see gears.color + +--- The focused client foreground (text) color. +-- @beautiful beautiful.tasklist_fg_focus +-- @tparam[opt=nil] string|pattern fg_focus +-- @see gears.color + +--- The focused client background color. +-- @beautiful beautiful.tasklist_bg_focus +-- @tparam[opt=nil] string|pattern bg_focus +-- @see gears.color + +--- The urgent clients foreground (text) color. +-- @beautiful beautiful.tasklist_fg_urgent +-- @tparam[opt=nil] string|pattern fg_urgent +-- @see gears.color + +--- The urgent clients background color. +-- @beautiful beautiful.tasklist_bg_urgent +-- @tparam[opt=nil] string|pattern bg_urgent +-- @see gears.color + +--- The minimized clients foreground (text) color. +-- @beautiful beautiful.tasklist_fg_minimize +-- @tparam[opt=nil] string|pattern fg_minimize +-- @see gears.color + +--- The minimized clients background color. +-- @beautiful beautiful.tasklist_bg_minimize +-- @tparam[opt=nil] string|pattern bg_minimize +-- @see gears.color + +--- The elements default background image. +-- @beautiful beautiful.tasklist_bg_image_normal +-- @tparam[opt=nil] string bg_image_normal + +--- The focused client background image. +-- @beautiful beautiful.tasklist_bg_image_focus +-- @tparam[opt=nil] string bg_image_focus + +--- The urgent clients background image. +-- @beautiful beautiful.tasklist_bg_image_urgent +-- @tparam[opt=nil] string bg_image_urgent + +--- The minimized clients background image. +-- @beautiful beautiful.tasklist_bg_image_minimize +-- @tparam[opt=nil] string bg_image_minimize + +--- Disable the tasklist client icons. +-- @beautiful beautiful.tasklist_tasklist_disable_icon +-- @tparam[opt=false] boolean tasklist_disable_icon + +--- Disable the extra tasklist client property notification icons. +-- +-- See the Status icons section for more details. +-- +-- @beautiful beautiful.tasklist_plain_task_name +-- @tparam[opt=false] boolean tasklist_plain_task_name + +--- The tasklist font. +-- @beautiful beautiful.tasklist_font +-- @tparam[opt=nil] string font + +--- The focused client alignment. +-- @beautiful beautiful.tasklist_align +-- @tparam[opt=left] string align *left*, *right* or *center* + +--- The focused client title alignment. +-- @beautiful beautiful.tasklist_font_focus +-- @tparam[opt=nil] string font_focus + +--- The minimized clients font. +-- @beautiful beautiful.tasklist_font_minimized +-- @tparam[opt=nil] string font_minimized + +--- The urgent clients font. +-- @beautiful beautiful.tasklist_font_urgent +-- @tparam[opt=nil] string font_urgent + +--- The space between the tasklist elements. +-- @beautiful beautiful.tasklist_spacing +-- @tparam[opt=0] number spacing The spacing between tags. + +--- The default tasklist elements shape. +-- @beautiful beautiful.tasklist_shape +-- @tparam[opt=nil] gears.shape shape + +--- The default tasklist elements border width. +-- @beautiful beautiful.tasklist_shape_border_width +-- @tparam[opt=0] number shape_border_width + +--- The default tasklist elements border color. +-- @beautiful beautiful.tasklist_shape_border_color +-- @tparam[opt=nil] string|color shape_border_color +-- @see gears.color + +--- The focused client shape. +-- @beautiful beautiful.tasklist_shape_focus +-- @tparam[opt=nil] gears.shape shape_focus + +--- The focused client border width. +-- @beautiful beautiful.tasklist_shape_border_width_focus +-- @tparam[opt=0] number shape_border_width_focus + +--- The focused client border color. +-- @beautiful beautiful.tasklist_shape_border_color_focus +-- @tparam[opt=nil] string|color shape_border_color_focus +-- @see gears.color + +--- The minimized clients shape. +-- @beautiful beautiful.tasklist_shape_minimized +-- @tparam[opt=nil] gears.shape shape_minimized + +--- The minimized clients border width. +-- @beautiful beautiful.tasklist_shape_border_width_minimized +-- @tparam[opt=0] number shape_border_width_minimized + +--- The minimized clients border color. +-- @beautiful beautiful.tasklist_shape_border_color_minimized +-- @tparam[opt=nil] string|color shape_border_color_minimized +-- @see gears.color + +--- The urgent clients shape. +-- @beautiful beautiful.tasklist_shape_urgent +-- @tparam[opt=nil] gears.shape shape_urgent + +--- The urgent clients border width. +-- @beautiful beautiful.tasklist_shape_border_width_urgent +-- @tparam[opt=0] number shape_border_width_urgent + +--- The urgent clients border color. +-- @beautiful beautiful.tasklist_shape_border_color_urgent +-- @tparam[opt=nil] string|color shape_border_color_urgent +-- @see gears.color + +-- Public structures +tasklist.filter = {} + +local function tasklist_label(c, args, tb) + if not args then args = {} end + local theme = beautiful.get() + local align = args.align or theme.tasklist_align or "left" + local fg_normal = util.ensure_pango_color(args.fg_normal or theme.tasklist_fg_normal or theme.fg_normal, "white") + local bg_normal = args.bg_normal or theme.tasklist_bg_normal or theme.bg_normal or "#000000" + local fg_focus = util.ensure_pango_color(args.fg_focus or theme.tasklist_fg_focus or theme.fg_focus, fg_normal) + local bg_focus = args.bg_focus or theme.tasklist_bg_focus or theme.bg_focus or bg_normal + local fg_urgent = util.ensure_pango_color(args.fg_urgent or theme.tasklist_fg_urgent or theme.fg_urgent, fg_normal) + local bg_urgent = args.bg_urgent or theme.tasklist_bg_urgent or theme.bg_urgent or bg_normal + local fg_minimize = util.ensure_pango_color(args.fg_minimize or theme.tasklist_fg_minimize or theme.fg_minimize, fg_normal) + local bg_minimize = args.bg_minimize or theme.tasklist_bg_minimize or theme.bg_minimize or bg_normal + local bg_image_normal = args.bg_image_normal or theme.bg_image_normal + local bg_image_focus = args.bg_image_focus or theme.bg_image_focus + local bg_image_urgent = args.bg_image_urgent or theme.bg_image_urgent + local bg_image_minimize = args.bg_image_minimize or theme.bg_image_minimize + local tasklist_disable_icon = args.tasklist_disable_icon or theme.tasklist_disable_icon or false + local font = args.font or theme.tasklist_font or theme.font or "" + local font_focus = args.font_focus or theme.tasklist_font_focus or theme.font_focus or font or "" + local font_minimized = args.font_minimized or theme.tasklist_font_minimized or theme.font_minimized or font or "" + local font_urgent = args.font_urgent or theme.tasklist_font_urgent or theme.font_urgent or font or "" + local text = "" + local name = "" + local bg + local bg_image + local shape = args.shape or theme.tasklist_shape + local shape_border_width = args.shape_border_width or theme.tasklist_shape_border_width + local shape_border_color = args.shape_border_color or theme.tasklist_shape_border_color + + -- symbol to use to indicate certain client properties + local sticky = args.sticky or theme.tasklist_sticky or "▪" + local ontop = args.ontop or theme.tasklist_ontop or '⌃' + local above = args.above or theme.tasklist_above or '▴' + local below = args.below or theme.tasklist_below or '▾' + local floating = args.floating or theme.tasklist_floating or '✈' + local maximized = args.maximized or theme.tasklist_maximized or '+' + local maximized_horizontal = args.maximized_horizontal or theme.tasklist_maximized_horizontal or '⬌' + local maximized_vertical = args.maximized_vertical or theme.tasklist_maximized_vertical or '⬍' + + tb:set_align(align) + + if not theme.tasklist_plain_task_name then + if c.sticky then name = name .. sticky end + + if c.ontop then name = name .. ontop + elseif c.above then name = name .. above + elseif c.below then name = name .. below end + + if c.maximized then + name = name .. maximized + else + if c.maximized_horizontal then name = name .. maximized_horizontal end + if c.maximized_vertical then name = name .. maximized_vertical end + if c.floating then name = name .. floating end + end + end + + if c.minimized then + name = name .. (util.escape(c.icon_name) or util.escape(c.name) or util.escape("")) + else + name = name .. (util.escape(c.name) or util.escape("")) + end + + local focused = capi.client.focus == c + -- Handle transient_for: the first parent that does not skip the taskbar + -- is considered to be focused, if the real client has skip_taskbar. + if not focused and capi.client.focus and capi.client.focus.skip_taskbar + and capi.client.focus:get_transient_for_matching(function(cl) + return not cl.skip_taskbar + end) == c then + focused = true + end + + if focused then + bg = bg_focus + text = text .. ""..name.."" + bg_image = bg_image_focus + font = font_focus + + if args.shape_focus or theme.tasklist_shape_focus then + shape = args.shape_focus or theme.tasklist_shape_focus + end + + if args.shape_border_width_focus or theme.tasklist_shape_border_width_focus then + shape_border_width = args.shape_border_width_focus or theme.tasklist_shape_border_width_focus + end + + if args.shape_border_color_focus or theme.tasklist_shape_border_color_focus then + shape_border_color = args.shape_border_color_focus or theme.tasklist_shape_border_color_focus + end + elseif c.urgent then + bg = bg_urgent + text = text .. ""..name.."" + bg_image = bg_image_urgent + font = font_urgent + + if args.shape_urgent or theme.tasklist_shape_urgent then + shape = args.shape_urgent or theme.tasklist_shape_urgent + end + + if args.shape_border_width_urgent or theme.tasklist_shape_border_width_urgent then + shape_border_width = args.shape_border_width_urgent or theme.tasklist_shape_border_width_urgent + end + + if args.shape_border_color_urgent or theme.tasklist_shape_border_color_urgent then + shape_border_color = args.shape_border_color_urgent or theme.tasklist_shape_border_color_urgent + end + elseif c.minimized then + bg = bg_minimize + text = text .. ""..name.."" + bg_image = bg_image_minimize + font = font_minimized + + if args.shape_minimized or theme.tasklist_shape_minimized then + shape = args.shape_minimized or theme.tasklist_shape_minimized + end + + if args.shape_border_width_minimized or theme.tasklist_shape_border_width_minimized then + shape_border_width = args.shape_border_width_minimized or theme.tasklist_shape_border_width_minimized + end + + if args.shape_border_color_minimized or theme.tasklist_shape_border_color_minimized then + shape_border_color = args.shape_border_color_minimized or theme.tasklist_shape_border_color_minimized + end + else + bg = bg_normal + text = text .. ""..name.."" + bg_image = bg_image_normal + end + tb:set_font(font) + + local other_args = { + shape = shape, + shape_border_width = shape_border_width, + shape_border_color = shape_border_color, + } + + return text, bg, bg_image, not tasklist_disable_icon and c.icon or nil, other_args +end + +local function tasklist_update(s, w, buttons, filter, data, style, update_function) + local clients = {} + for _, c in ipairs(capi.client.get()) do + if not (c.skip_taskbar or c.hidden + or c.type == "splash" or c.type == "dock" or c.type == "desktop") + and filter(c, s) then + table.insert(clients, c) + end + end + + local function label(c, tb) return tasklist_label(c, style, tb) end + + update_function(w, buttons, label, data, clients) +end + +--- Create a new tasklist widget. The last two arguments (update_function +-- and base_widget) serve to customize the layout of the tasklist (eg. to +-- make it vertical). For that, you will need to copy the +-- awful.widget.common.list_update function, make your changes to it +-- and pass it as update_function here. Also change the base_widget if the +-- default is not what you want. +-- @param screen The screen to draw tasklist for. +-- @param filter Filter function to define what clients will be listed. +-- @param buttons A table with buttons binding to set. +-- @tparam[opt={}] table style The style overrides default theme. +-- @tparam[opt=nil] string|pattern style.fg_normal +-- @tparam[opt=nil] string|pattern style.bg_normal +-- @tparam[opt=nil] string|pattern style.fg_focus +-- @tparam[opt=nil] string|pattern style.bg_focus +-- @tparam[opt=nil] string|pattern style.fg_urgent +-- @tparam[opt=nil] string|pattern style.bg_urgent +-- @tparam[opt=nil] string|pattern style.fg_minimize +-- @tparam[opt=nil] string|pattern style.bg_minimize +-- @tparam[opt=nil] string style.bg_image_normal +-- @tparam[opt=nil] string style.bg_image_focus +-- @tparam[opt=nil] string style.bg_image_urgent +-- @tparam[opt=nil] string style.bg_image_minimize +-- @tparam[opt=nil] boolean style.tasklist_disable_icon +-- @tparam[opt=nil] string style.font +-- @tparam[opt=left] string style.align *left*, *right* or *center* +-- @tparam[opt=nil] string style.font_focus +-- @tparam[opt=nil] string style.font_minimized +-- @tparam[opt=nil] string style.font_urgent +-- @tparam[opt=nil] number style.spacing The spacing between tags. +-- @tparam[opt=nil] gears.shape style.shape +-- @tparam[opt=nil] number style.shape_border_width +-- @tparam[opt=nil] string|color style.shape_border_color +-- @tparam[opt=nil] gears.shape style.shape_focus +-- @tparam[opt=nil] number style.shape_border_width_focus +-- @tparam[opt=nil] string|color style.shape_border_color_focus +-- @tparam[opt=nil] gears.shape style.shape_minimized +-- @tparam[opt=nil] number style.shape_border_width_minimized +-- @tparam[opt=nil] string|color style.shape_border_color_minimized +-- @tparam[opt=nil] gears.shape style.shape_urgent +-- @tparam[opt=nil] number style.shape_border_width_urgent +-- @tparam[opt=nil] string|color style.shape_border_color_urgent +-- @param[opt] update_function Function to create a tag widget on each +-- update. See `awful.widget.common.list_update`. +-- @tparam[opt] table base_widget Container widget for tag widgets. Default +-- is `wibox.layout.flex.horizontal`. +-- @function awful.tasklist +function tasklist.new(screen, filter, buttons, style, update_function, base_widget) + screen = get_screen(screen) + local uf = update_function or common.list_update + local w = base_widget or flex.horizontal() + + local data = setmetatable({}, { __mode = 'k' }) + + if w.set_spacing and (style and style.spacing or beautiful.taglist_spacing) then + w:set_spacing(style and style.spacing or beautiful.taglist_spacing) + end + + local queued_update = false + function w._do_tasklist_update() + -- Add a delayed callback for the first update. + if not queued_update then + timer.delayed_call(function() + queued_update = false + if screen.valid then + tasklist_update(screen, w, buttons, filter, data, style, uf) + end + end) + queued_update = true + end + end + function w._unmanage(c) + data[c] = nil + end + if instances == nil then + instances = setmetatable({}, { __mode = "k" }) + local function us(s) + local i = instances[get_screen(s)] + if i then + for _, tlist in pairs(i) do + tlist._do_tasklist_update() + end + end + end + local function u() + for s in pairs(instances) do + if s.valid then + us(s) + end + end + end + + tag.attached_connect_signal(nil, "property::selected", u) + tag.attached_connect_signal(nil, "property::activated", u) + capi.client.connect_signal("property::urgent", u) + capi.client.connect_signal("property::sticky", u) + capi.client.connect_signal("property::ontop", u) + capi.client.connect_signal("property::above", u) + capi.client.connect_signal("property::below", u) + capi.client.connect_signal("property::floating", u) + capi.client.connect_signal("property::maximized_horizontal", u) + capi.client.connect_signal("property::maximized_vertical", u) + capi.client.connect_signal("property::minimized", u) + capi.client.connect_signal("property::name", u) + capi.client.connect_signal("property::icon_name", u) + capi.client.connect_signal("property::icon", u) + capi.client.connect_signal("property::skip_taskbar", u) + capi.client.connect_signal("property::screen", function(c, old_screen) + us(c.screen) + us(old_screen) + end) + capi.client.connect_signal("property::hidden", u) + capi.client.connect_signal("tagged", u) + capi.client.connect_signal("untagged", u) + capi.client.connect_signal("unmanage", function(c) + u(c) + for _, i in pairs(instances) do + for _, tlist in pairs(i) do + tlist._unmanage(c) + end + end + end) + capi.client.connect_signal("list", u) + capi.client.connect_signal("focus", u) + capi.client.connect_signal("unfocus", u) + capi.screen.connect_signal("removed", function(s) + instances[get_screen(s)] = nil + end) + end + w._do_tasklist_update() + local list = instances[screen] + if not list then + list = setmetatable({}, { __mode = "v" }) + instances[screen] = list + end + table.insert(list, w) + return w +end + +--- Filtering function to include all clients. +-- @return true +function tasklist.filter.allscreen() + return true +end + +--- Filtering function to include the clients from all tags on the screen. +-- @param c The client. +-- @param screen The screen we are drawing on. +-- @return true if c is on screen, false otherwise +function tasklist.filter.alltags(c, screen) + -- Only print client on the same screen as this widget + return get_screen(c.screen) == get_screen(screen) +end + +--- Filtering function to include only the clients from currently selected tags. +-- @param c The client. +-- @param screen The screen we are drawing on. +-- @return true if c is in a selected tag on screen, false otherwise +function tasklist.filter.currenttags(c, screen) + screen = get_screen(screen) + -- Only print client on the same screen as this widget + if get_screen(c.screen) ~= screen then return false end + -- Include sticky client too + if c.sticky then return true end + local tags = screen.tags + for _, t in ipairs(tags) do + if t.selected then + local ctags = c:tags() + for _, v in ipairs(ctags) do + if v == t then + return true + end + end + end + end + return false +end + +--- Filtering function to include only the minimized clients from currently selected tags. +-- @param c The client. +-- @param screen The screen we are drawing on. +-- @return true if c is in a selected tag on screen and is minimized, false otherwise +function tasklist.filter.minimizedcurrenttags(c, screen) + screen = get_screen(screen) + -- Only print client on the same screen as this widget + if get_screen(c.screen) ~= screen then return false end + -- Check client is minimized + if not c.minimized then return false end + -- Include sticky client + if c.sticky then return true end + local tags = screen.tags + for _, t in ipairs(tags) do + -- Select only minimized clients + if t.selected then + local ctags = c:tags() + for _, v in ipairs(ctags) do + if v == t then + return true + end + end + end + end + return false +end + +--- Filtering function to include only the currently focused client. +-- @param c The client. +-- @param screen The screen we are drawing on. +-- @return true if c is focused on screen, false otherwise +function tasklist.filter.focused(c, screen) + -- Only print client on the same screen as this widget + return get_screen(c.screen) == get_screen(screen) and capi.client.focus == c +end + +function tasklist.mt:__call(...) + return tasklist.new(...) +end + +return setmetatable(tasklist, tasklist.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/textclock.lua b/lib/awful/widget/textclock.lua new file mode 100644 index 0000000..002aa0e --- /dev/null +++ b/lib/awful/widget/textclock.lua @@ -0,0 +1,16 @@ +--------------------------------------------------------------------------- +-- This widget has moved to `wibox.widget.textclock` +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008-2009 Julien Danjou +-- @classmod awful.widget.textclock +--------------------------------------------------------------------------- +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.widget.textclock"), + "awful.widget.textclock", + "wibox.widget.textclock" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/watch.lua b/lib/awful/widget/watch.lua new file mode 100644 index 0000000..bc4c9af --- /dev/null +++ b/lib/awful/widget/watch.lua @@ -0,0 +1,91 @@ +--------------------------------------------------------------------------- +--- Watch widget. +-- Here is an example of simple temperature widget which will update each 15 +-- seconds implemented in two different ways. +-- The first, simpler one, will just display the return command output +-- (so output is stripped by shell commands). +-- In the other example `sensors` returns to the widget its full output +-- and it's trimmed in the widget callback function: +-- +-- 211 mytextclock, +-- 212 wibox.widget.textbox(' | '), +-- 213 -- one way to do that: +-- 214 awful.widget.watch('bash -c "sensors | grep temp1"', 15), +-- 215 -- another way: +-- 216 awful.widget.watch('sensors', 15, function(widget, stdout) +-- 217 for line in stdout:gmatch("[^\r\n]+") do +-- 218 if line:match("temp1") then +-- 219 widget:set_text(line) +-- 220 return +-- 221 end +-- 222 end +-- 223 end), +-- 224 s.mylayoutbox, +-- +-- ![Example screenshot](../images/awful_widget_watch.png) +-- +-- @author Benjamin Petrenko +-- @author Yauheni Kirylau +-- @copyright 2015, 2016 Benjamin Petrenko, Yauheni Kirylau +-- @classmod awful.widget.watch +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local textbox = require("wibox.widget.textbox") +local timer = require("gears.timer") +local spawn = require("awful.spawn") + +local watch = { mt = {} } + +--- Create a textbox that shows the output of a command +-- and updates it at a given time interval. +-- +-- @tparam string|table command The command. +-- +-- @tparam[opt=5] integer timeout The time interval at which the textbox +-- will be updated. +-- +-- @tparam[opt] function callback The function that will be called after +-- the command output will be received. it is shown in the textbox. +-- Defaults to: +-- function(widget, stdout, stderr, exitreason, exitcode) +-- widget:set_text(stdout) +-- end +-- @param callback.widget Base widget instance. +-- @tparam string callback.stdout Output on stdout. +-- @tparam string callback.stderr Output on stderr. +-- @tparam string callback.exitreason Exit Reason. +-- The reason can be "exit" or "signal". +-- @tparam integer callback.exitcode Exit code. +-- For "exit" reason it's the exit code. +-- For "signal" reason — the signal causing process termination. +-- +-- @param[opt=wibox.widget.textbox()] base_widget Base widget. +-- +-- @return The widget used by this watch +function watch.new(command, timeout, callback, base_widget) + timeout = timeout or 5 + base_widget = base_widget or textbox() + callback = callback or function(widget, stdout, stderr, exitreason, exitcode) -- luacheck: no unused args + widget:set_text(stdout) + end + local t = timer { timeout = timeout } + t:connect_signal("timeout", function() + t:stop() + spawn.easy_async(command, function(stdout, stderr, exitreason, exitcode) + callback(base_widget, stdout, stderr, exitreason, exitcode) + t:again() + end) + end) + t:start() + t:emit_signal("timeout") + return base_widget +end + +function watch.mt.__call(_, ...) + return watch.new(...) +end + +return setmetatable(watch, watch.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/beautiful.lua b/lib/beautiful.lua new file mode 100644 index 0000000..f6f0106 --- /dev/null +++ b/lib/beautiful.lua @@ -0,0 +1,7 @@ +-- Work-around for broken systems which are updated by overwriting the awesome +-- installation. This would not remove beautiful.lua from older awesome versions +-- and thus breakage follows. +-- The work-around is to use a pointless beautiful.lua file. +return require("beautiful.init") + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/beautiful/init.lua b/lib/beautiful/init.lua new file mode 100644 index 0000000..0e72cbc --- /dev/null +++ b/lib/beautiful/init.lua @@ -0,0 +1,217 @@ +---------------------------------------------------------------------------- +--- Theme library. +-- +-- @author Damien Leone <damien.leone@gmail.com> +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008-2009 Damien Leone, Julien Danjou +-- @module beautiful +---------------------------------------------------------------------------- + +-- Grab environment +local os = os +local pairs = pairs +local type = type +local dofile = dofile +local setmetatable = setmetatable +local lgi = require("lgi") +local Pango = lgi.Pango +local PangoCairo = lgi.PangoCairo +local gears_debug = require("gears.debug") +local protected_call = require("gears.protected_call") + +local xresources = require("beautiful.xresources") + +local beautiful = { xresources = xresources, mt = {} } + +-- Local data +local theme = {} +local descs = setmetatable({}, { __mode = 'k' }) +local fonts = setmetatable({}, { __mode = 'v' }) +local active_font + +--- The default font. +-- @beautiful beautiful.font + +-- The default background color. +-- @beautiful beautiful.bg_normal + +-- The default focused element background color. +-- @beautiful beautiful.bg_focus + +-- The default urgent element background color. +-- @beautiful beautiful.bg_urgent + +-- The default minimized element background color. +-- @beautiful beautiful.bg_minimize + +-- The system tray background color. +-- Please note that only solid colors are currently supported. +-- @beautiful beautiful.bg_systray + +-- The default focused element foreground (text) color. +-- @beautiful beautiful.fg_normal + +-- The default focused element foreground (text) color. +-- @beautiful beautiful.fg_focus + +-- The default urgent element foreground (text) color. +-- @beautiful beautiful.fg_urgent + +-- The default minimized element foreground (text) color. +-- @beautiful beautiful.fg_minimize + +--- The gap between clients. +-- @beautiful beautiful.useless_gap +-- @param[opt=0] number + +--- The client border width. +-- @beautiful beautiful.border_width + +--- The default clients border width. +-- Note that only solid colors are supported. +-- @beautiful beautiful.border_normal + +--- The focused client border width. +-- Note that only solid colors are supported. +-- @beautiful beautiful.border_focus + +--- The marked clients border width. +-- Note that only solid colors are supported. +-- @beautiful beautiful.border_marked + +--- The wallpaper path. +-- @beautiful beautiful.wallpaper + +-- The icon theme name. +-- It has to be a directory in `/usr/share/icons` or an XDG icon folder. +-- @beautiful beautiful.icon_theme + +--- The Awesome icon path. +-- @beautiful beautiful.awesome_icon + +--- Load a font from a string or a font description. +-- +-- @see https://developer.gnome.org/pango/stable/pango-Fonts.html#pango-font-description-from-string +-- @tparam string|lgi.Pango.FontDescription name Font, which can be a +-- string or a lgi.Pango.FontDescription. +-- @treturn table A table with `name`, `description` and `height`. +local function load_font(name) + name = name or active_font + if name and type(name) ~= "string" then + if descs[name] then + name = descs[name] + else + name = name:to_string() + end + end + if fonts[name] then + return fonts[name] + end + + -- Load new font + local desc = Pango.FontDescription.from_string(name) + local ctx = PangoCairo.font_map_get_default():create_context() + ctx:set_resolution(beautiful.xresources.get_dpi()) + + -- Apply default values from the context (e.g. a default font size) + desc:merge(ctx:get_font_description(), false) + + -- Calculate font height. + local metrics = ctx:get_metrics(desc, nil) + local height = math.ceil((metrics:get_ascent() + metrics:get_descent()) / Pango.SCALE) + + local font = { name = name, description = desc, height = height } + fonts[name] = font + descs[desc] = name + return font +end + +--- Set an active font +-- +-- @param name The font +local function set_font(name) + active_font = load_font(name).name +end + +--- Get a font description. +-- +-- See https://developer.gnome.org/pango/stable/pango-Fonts.html#PangoFontDescription. +-- @tparam string|lgi.Pango.FontDescription name The name of the font. +-- @treturn lgi.Pango.FontDescription +function beautiful.get_font(name) + return load_font(name).description +end + +--- Get a new font with merged attributes, based on another one. +-- +-- See https://developer.gnome.org/pango/stable/pango-Fonts.html#pango-font-description-from-string. +-- @tparam string|Pango.FontDescription name The base font. +-- @tparam string merge Attributes that should be merged, e.g. "bold". +-- @treturn lgi.Pango.FontDescription +function beautiful.get_merged_font(name, merge) + local font = beautiful.get_font(name) + merge = Pango.FontDescription.from_string(merge) + local merged = font:copy_static() + merged:merge(merge, true) + return beautiful.get_font(merged:to_string()) +end + +--- Get the height of a font. +-- +-- @param name Name of the font +function beautiful.get_font_height(name) + return load_font(name).height +end + +--- Init function, should be runned at the beginning of configuration file. +-- @tparam string|table config The theme to load. It can be either the path to +-- the theme file (returning a table) or directly the table +-- containing all the theme values. +function beautiful.init(config) + if config then + local homedir = os.getenv("HOME") + + -- If `config` is the path to a theme file, run this file, + -- otherwise if it is a theme table, save it. + if type(config) == 'string' then + -- Expand the '~' $HOME shortcut + config = config:gsub("^~/", homedir .. "/") + theme = protected_call(dofile, config) + elseif type(config) == 'table' then + theme = config + end + + if theme then + -- expand '~' + if homedir then + for k, v in pairs(theme) do + if type(v) == "string" then theme[k] = v:gsub("^~/", homedir .. "/") end + end + end + + if theme.font then set_font(theme.font) end + else + return gears_debug.print_error("beautiful: error loading theme file " .. config) + end + else + return gears_debug.print_error("beautiful: error loading theme: no theme specified") + end +end + +--- Get the current theme. +-- +-- @treturn table The current theme table. +function beautiful.get() + return theme +end + +function beautiful.mt:__index(k) + return theme[k] +end + +-- Set the default font +set_font("sans 8") + +return setmetatable(beautiful, beautiful.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/beautiful/xresources.lua b/lib/beautiful/xresources.lua new file mode 100644 index 0000000..f0f5d78 --- /dev/null +++ b/lib/beautiful/xresources.lua @@ -0,0 +1,117 @@ +---------------------------------------------------------------------------- +--- Library for getting xrdb data. +-- +-- @author Yauhen Kirylau <yawghen@gmail.com> +-- @copyright 2015 Yauhen Kirylau +-- @module beautiful.xresources +---------------------------------------------------------------------------- + +-- Grab environment +local awesome = awesome +local screen = screen +local round = require("awful.util").round +local gears_debug = require("gears.debug") + +local xresources = {} + +local fallback = { + --black + color0 = '#000000', + color8 = '#465457', + --red + color1 = '#cb1578', + color9 = '#dc5e86', + --green + color2 = '#8ecb15', + color10 = '#9edc60', + --yellow + color3 = '#cb9a15', + color11 = '#dcb65e', + --blue + color4 = '#6f15cb', + color12 = '#7e5edc', + --purple + color5 = '#cb15c9', + color13 = '#b75edc', + --cyan + color6 = '#15b4cb', + color14 = '#5edcb4', + --white + color7 = '#888a85', + color15 = '#ffffff', + -- + background = '#0e0021', + foreground = '#bcbcbc', +} + +--- Get current base colorscheme from xrdb. +-- @treturn table Color table with keys 'background', 'foreground' and 'color0'..'color15' +function xresources.get_current_theme() + local keys = { 'background', 'foreground' } + for i=0,15 do table.insert(keys, "color"..i) end + local colors = {} + for _, key in ipairs(keys) do + colors[key] = awesome.xrdb_get_value("", key) + if not colors[key] then + gears_debug.print_warning("beautiful: can't get colorscheme from xrdb (using fallback).") + return fallback + end + if colors[key]:find("rgb:") then + colors[key] = "#"..colors[key]:gsub("[a]?rgb:", ""):gsub("/", "") + end + end + return colors +end + + +local dpi_per_screen = {} + +local function get_screen(s) + return s and screen[s] +end + +--- Get global or per-screen DPI value falling back to xrdb. +-- @tparam[opt] integer|screen s The screen. +-- @treturn number DPI value. +function xresources.get_dpi(s) + s = get_screen(s) + if dpi_per_screen[s] then + return dpi_per_screen[s] + end + if not xresources.dpi then + -- Might not be present when run under unit tests + if awesome and awesome.xrdb_get_value then + xresources.dpi = tonumber(awesome.xrdb_get_value("", "Xft.dpi")) + end + if not xresources.dpi then + xresources.dpi = 96 + end + end + return xresources.dpi +end + + +--- Set DPI for a given screen (defaults to global). +-- @tparam number dpi DPI value. +-- @tparam[opt] integer s Screen. +function xresources.set_dpi(dpi, s) + s = get_screen(s) + if not s then + xresources.dpi = dpi + else + dpi_per_screen[s] = dpi + end +end + + +--- Compute resulting size applying current DPI value (optionally per screen). +-- @tparam number size Size +-- @tparam[opt] integer|screen s The screen. +-- @treturn integer Resulting size (rounded to integer). +function xresources.apply_dpi(size, s) + return round(size / 96 * xresources.get_dpi(s)) +end + +return xresources + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/cache.lua b/lib/gears/cache.lua new file mode 100644 index 0000000..dc5add5 --- /dev/null +++ b/lib/gears/cache.lua @@ -0,0 +1,51 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2015 Uli Schlachter +-- @classmod gears.cache +--------------------------------------------------------------------------- + +local select = select +local setmetatable = setmetatable +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +local cache = {} + +--- Get an entry from the cache, creating it if it's missing. +-- @param ... Arguments for the creation callback. These are checked against the +-- cache contents for equality. +-- @return The entry from the cache +function cache:get(...) + local result = self._cache + for i = 1, select("#", ...) do + local arg = select(i, ...) + local next = result[arg] + if not next then + next = {} + result[arg] = next + end + result = next + end + local ret = result._entry + if not ret then + ret = { self._creation_cb(...) } + result._entry = ret + end + return unpack(ret) +end + +--- Create a new cache object. A cache keeps some data that can be +-- garbage-collected at any time, but might be useful to keep. +-- @param creation_cb Callback that is used for creating missing cache entries. +-- @return A new cache object. +function cache.new(creation_cb) + return setmetatable({ + _cache = setmetatable({}, { __mode = "v" }), + _creation_cb = creation_cb + }, { + __index = cache + }) +end + +return setmetatable(cache, { __call = function(_, ...) return cache.new(...) end }) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/color.lua b/lib/gears/color.lua new file mode 100644 index 0000000..f0197c1 --- /dev/null +++ b/lib/gears/color.lua @@ -0,0 +1,346 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @module gears.color +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local string = string +local table = table +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local tonumber = tonumber +local ipairs = ipairs +local pairs = pairs +local type = type +local lgi = require("lgi") +local cairo = lgi.cairo +local Pango = lgi.Pango +local surface = require("gears.surface") + +local color = { mt = {} } +local pattern_cache + +--- Create a pattern from a given string. +-- This function can create solid, linear, radial and png patterns. In general, +-- patterns are specified as strings formatted as "type:arguments". "arguments" +-- is specific to the pattern being used. For example, one can use +-- "radial:50,50,10:55,55,30:0,#ff0000:0.5,#00ff00:1,#0000ff". +-- Alternatively, patterns can be specified via tables. In this case, the +-- table's 'type' member specifies the type. For example: +-- { +-- type = "radial", +-- from = { 50, 50, 10 }, +-- to = { 55, 55, 30 }, +-- stops = { { 0, "#ff0000" }, { 0.5, "#00ff00" }, { 1, "#0000ff" } } +-- } +-- Any argument that cannot be understood is passed to @{create_solid_pattern}. +-- +-- Please note that you MUST NOT modify the returned pattern, for example by +-- calling :set_matrix() on it, because this function uses a cache and your +-- changes could thus have unintended side effects. Use @{create_pattern_uncached} +-- if you need to modify the returned pattern. +-- @see create_pattern_uncached, create_solid_pattern, create_png_pattern, +-- create_linear_pattern, create_radial_pattern +-- @tparam string col The string describing the pattern. +-- @return a cairo pattern object +-- @function gears.color + +--- Parse a HTML-color. +-- This function can parse colors like `#rrggbb` and `#rrggbbaa` and also `red`. +-- Max 4 chars per channel. +-- +-- @param col The color to parse +-- @treturn table 4 values representing color in RGBA format (each of them in +-- [0, 1] range) or nil if input is incorrect. +-- @usage -- This will return 0, 1, 0, 1 +-- gears.color.parse_color("#00ff00ff") +function color.parse_color(col) + local rgb = {} + if string.match(col, "^#%x+$") then + local hex_str = col:sub(2, #col) + local channels + if #hex_str % 3 == 0 then + channels = 3 + elseif #hex_str % 4 == 0 then + channels = 4 + else + return nil + end + local chars_per_channel = #hex_str / channels + if chars_per_channel > 4 then + return nil + end + local dividor = (0x10 ^ chars_per_channel) - 1 + for idx=1,#hex_str,chars_per_channel do + local channel_val = tonumber(hex_str:sub(idx,idx+chars_per_channel-1), 16) + table.insert(rgb, channel_val / dividor) + end + if channels == 3 then + table.insert(rgb, 1) + end + else + local c = Pango.Color() + if not c:parse(col) then + return nil + end + rgb = { + c.red / 0xffff, + c.green / 0xffff, + c.blue / 0xffff, + 1.0 + } + end + assert(#rgb == 4, col) + return unpack(rgb) +end + +--- Find all numbers in a string +-- +-- @tparam string s The string to parse +-- @return Each number found as a separate value +local function parse_numbers(s) + local res = {} + for k in string.gmatch(s, "-?[0-9]+[.]?[0-9]*") do + table.insert(res, tonumber(k)) + end + return unpack(res) +end + +--- Create a solid pattern +-- +-- @param col The color for the pattern +-- @return A cairo pattern object +function color.create_solid_pattern(col) + if col == nil then + col = "#000000" + elseif type(col) == "table" then + col = col.color + end + return cairo.Pattern.create_rgba(color.parse_color(col)) +end + +--- Create an image pattern from a png file +-- +-- @param file The filename of the file +-- @return a cairo pattern object +function color.create_png_pattern(file) + if type(file) == "table" then + file = file.file + end + local image = surface.load(file) + local pattern = cairo.Pattern.create_for_surface(image) + pattern:set_extend(cairo.Extend.REPEAT) + return pattern +end + +--- Add stops to the given pattern. +-- @param p The cairo pattern to add stops to +-- @param iterator An iterator that returns strings. Each of those strings +-- should be in the form place,color where place is in [0, 1]. +local function add_iterator_stops(p, iterator) + for k in iterator do + local sub = string.gmatch(k, "[^,]+") + local point, clr = sub(), sub() + p:add_color_stop_rgba(point, color.parse_color(clr)) + end +end + +--- Add a list of stops to a given pattern +local function add_stops_table(pat, arg) + for _, stop in ipairs(arg) do + pat:add_color_stop_rgba(stop[1], color.parse_color(stop[2])) + end +end + +--- Create a pattern from a string +local function string_pattern(creator, arg) + local iterator = string.gmatch(arg, "[^:]+") + -- Create a table where each entry is a number from the original string + local args = { parse_numbers(iterator()) } + local to = { parse_numbers(iterator()) } + -- Now merge those two tables + for _, v in pairs(to) do + table.insert(args, v) + end + -- And call our creator function with the values + local p = creator(unpack(args)) + + add_iterator_stops(p, iterator) + return p +end + +--- Create a linear pattern object. +-- The pattern is created from a string. This string should have the following +-- form: `"x0, y0:x1, y1:"` +-- Alternatively, the pattern can be specified as a table: +-- { type = "linear", from = { x0, y0 }, to = { x1, y1 }, +-- stops = { } } +-- `x0,y0` and `x1,y1` are the start and stop point of the pattern. +-- For the explanation of ``, see `color.create_pattern`. +-- @tparam string|table arg The argument describing the pattern. +-- @return a cairo pattern object +function color.create_linear_pattern(arg) + local pat + + if type(arg) == "string" then + return string_pattern(cairo.Pattern.create_linear, arg) + elseif type(arg) ~= "table" then + error("Wrong argument type: " .. type(arg)) + end + + pat = cairo.Pattern.create_linear(arg.from[1], arg.from[2], arg.to[1], arg.to[2]) + add_stops_table(pat, arg.stops) + return pat +end + +--- Create a radial pattern object. +-- The pattern is created from a string. This string should have the following +-- form: `"x0, y0, r0:x1, y1, r1:"` +-- Alternatively, the pattern can be specified as a table: +-- { type = "radial", from = { x0, y0, r0 }, to = { x1, y1, r1 }, +-- stops = { } } +-- `x0,y0` and `x1,y1` are the start and stop point of the pattern. +-- `r0` and `r1` are the radii of the start / stop circle. +-- For the explanation of ``, see `color.create_pattern`. +-- @tparam string|table arg The argument describing the pattern +-- @return a cairo pattern object +function color.create_radial_pattern(arg) + local pat + + if type(arg) == "string" then + return string_pattern(cairo.Pattern.create_radial, arg) + elseif type(arg) ~= "table" then + error("Wrong argument type: " .. type(arg)) + end + + pat = cairo.Pattern.create_radial(arg.from[1], arg.from[2], arg.from[3], + arg.to[1], arg.to[2], arg.to[3]) + add_stops_table(pat, arg.stops) + return pat +end + +--- Mapping of all supported color types. New entries can be added. +color.types = { + solid = color.create_solid_pattern, + png = color.create_png_pattern, + linear = color.create_linear_pattern, + radial = color.create_radial_pattern +} + +--- Create a pattern from a given string. +-- For full documentation of this function, please refer to +-- `color.create_pattern`. The difference between `color.create_pattern` +-- and this function is that this function does not insert the generated +-- objects into the pattern cache. Thus, you are allowed to modify the +-- returned object. +-- @see create_pattern +-- @param col The string describing the pattern. +-- @return a cairo pattern object +function color.create_pattern_uncached(col) + -- If it already is a cairo pattern, just leave it as that + if cairo.Pattern:is_type_of(col) then + return col + end + col = col or "#000000" + if type(col) == "string" then + local t = string.match(col, "[^:]+") + if color.types[t] then + local pos = string.len(t) + local arg = string.sub(col, pos + 2) + return color.types[t](arg) + end + elseif type(col) == "table" then + local t = col.type + if color.types[t] then + return color.types[t](col) + end + end + return color.create_solid_pattern(col) +end + +--- Create a pattern from a given string, same as `gears.color`. +-- @see gears.color +function color.create_pattern(col) + if cairo.Pattern:is_type_of(col) then + return col + end + return pattern_cache:get(col or "#000000") +end + +--- Check if a pattern is opaque. +-- A pattern is transparent if the background on which it gets drawn (with +-- operator OVER) doesn't influence the visual result. +-- @param col An argument that `create_pattern` accepts. +-- @return The pattern if it is surely opaque, else nil +function color.create_opaque_pattern(col) + local pattern = color.create_pattern(col) + local kind = pattern:get_type() + + if kind == "SOLID" then + local _, _, _, _, alpha = pattern:get_rgba() + if alpha ~= 1 then + return + end + return pattern + elseif kind == "SURFACE" then + local status, surf = pattern:get_surface() + if status ~= "SUCCESS" or surf.content ~= "COLOR" then + -- The surface has an alpha channel which *might* be non-opaque + return + end + + -- Only the "NONE" extend mode is forbidden, everything else doesn't + -- introduce transparent parts + if pattern:get_extend() == "NONE" then + return + end + + return pattern + elseif kind == "LINEAR" then + local _, stops = pattern:get_color_stop_count() + + -- No color stops or extend NONE -> pattern *might* contain transparency + if stops == 0 or pattern:get_extend() == "NONE" then + return + end + + -- Now check if any of the color stops contain transparency + for i = 0, stops - 1 do + local _, _, _, _, _, alpha = pattern:get_color_stop_rgba(i) + if alpha ~= 1 then + return + end + end + return pattern + end + + -- Unknown type, e.g. mesh or raster source or unsupported type (radial + -- gradients can do weird self-intersections) +end + +--- Fill non-transparent area of an image with a given color. +-- @param image Image or path to it. +-- @param new_color New color. +-- @return Recolored image. +function color.recolor_image(image, new_color) + if type(image) == 'string' then + image = surface.duplicate_surface(image) + end + local cr = cairo.Context.create(image) + cr:set_source(color.create_pattern(new_color)) + cr:mask(cairo.Pattern.create_for_surface(image), 0, 0) + return image +end + +function color.mt.__call(_, ...) + return color.create_pattern(...) +end + +pattern_cache = require("gears.cache").new(color.create_pattern_uncached) + +--- No color +color.transparent = color.create_pattern("#00000000") + +return setmetatable(color, color.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/debug.lua b/lib/gears/debug.lua new file mode 100644 index 0000000..55f72f5 --- /dev/null +++ b/lib/gears/debug.lua @@ -0,0 +1,78 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @module gears.debug +--------------------------------------------------------------------------- + +local tostring = tostring +local print = print +local type = type +local pairs = pairs + +local debug = {} + +--- Given a table (or any other data) return a string that contains its +-- tag, value and type. If data is a table then recursively call `dump_raw` +-- on each of its values. +-- @param data Value to inspect. +-- @param shift Spaces to indent lines with. +-- @param tag The name of the value. +-- @tparam[opt=10] int depth Depth of recursion. +-- @return a string which contains tag, value, value type and table key/value +-- pairs if data is a table. +local function dump_raw(data, shift, tag, depth) + depth = depth == nil and 10 or depth or 0 + local result = "" + + if tag then + result = result .. tostring(tag) .. " : " + end + + if type(data) == "table" and depth > 0 then + shift = (shift or "") .. " " + result = result .. tostring(data) + for k, v in pairs(data) do + result = result .. "\n" .. shift .. dump_raw(v, shift, k, depth - 1) + end + else + result = result .. tostring(data) .. " (" .. type(data) .. ")" + if depth == 0 and type(data) == "table" then + result = result .. " […]" + end + end + + return result +end + +--- Inspect the value in data. +-- @param data Value to inspect. +-- @param tag The name of the value. +-- @tparam[opt] int depth Depth of recursion. +-- @return string A string that contains the expanded value of data. +function debug.dump_return(data, tag, depth) + return dump_raw(data, nil, tag, depth) +end + +--- Print the table (or any other value) to the console. +-- @param data Table to print. +-- @param tag The name of the table. +-- @tparam[opt] int depth Depth of recursion. +function debug.dump(data, tag, depth) + print(debug.dump_return(data, tag, depth)) +end + +--- Print an warning message +-- @tparam string message The warning message to print +function debug.print_warning(message) + io.stderr:write(os.date("%Y-%m-%d %T W: ") .. tostring(message) .. "\n") +end + +--- Print an error message +-- @tparam string message The error message to print +function debug.print_error(message) + io.stderr:write(os.date("%Y-%m-%d %T E: ") .. tostring(message) .. "\n") +end + +return debug + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/geometry.lua b/lib/gears/geometry.lua new file mode 100644 index 0000000..a429abd --- /dev/null +++ b/lib/gears/geometry.lua @@ -0,0 +1,240 @@ +--------------------------------------------------------------------------- +-- +-- Helper functions used to compute geometries. +-- +-- When this module refer to a geometry table, this assume a table with at least +-- an *x*, *y*, *width* and *height* keys and numeric values. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module gears.geometry +--------------------------------------------------------------------------- +local math = math + +local gears = {geometry = {rectangle = {} } } + +--- Get the square distance between a rectangle and a point. +-- @tparam table geom A rectangle +-- @tparam number geom.x The horizontal coordinate +-- @tparam number geom.y The vertical coordinate +-- @tparam number geom.width The rectangle width +-- @tparam number geom.height The rectangle height +-- @tparam number x X coordinate of point +-- @tparam number y Y coordinate of point +-- @treturn number The squared distance of the rectangle to the provided point +function gears.geometry.rectangle.get_square_distance(geom, x, y) + local dist_x, dist_y = 0, 0 + if x < geom.x then + dist_x = geom.x - x + elseif x >= geom.x + geom.width then + dist_x = x - geom.x - geom.width + 1 + end + if y < geom.y then + dist_y = geom.y - y + elseif y >= geom.y + geom.height then + dist_y = y - geom.y - geom.height + 1 + end + return dist_x * dist_x + dist_y * dist_y +end + +--- Return the closest rectangle from `list` for a given point. +-- @tparam table list A list of geometry tables. +-- @tparam number x The x coordinate +-- @tparam number y The y coordinate +-- @return The key from the closest geometry. +function gears.geometry.rectangle.get_closest_by_coord(list, x, y) + local dist = math.huge + local ret = nil + + for k, v in pairs(list) do + local d = gears.geometry.rectangle.get_square_distance(v, x, y) + if d < dist then + ret, dist = k, d + end + end + + return ret +end + +--- Return the rectangle containing the [x, y] point. +-- +-- Note that if multiple element from the geometry list contains the point, the +-- returned result is nondeterministic. +-- +-- @tparam table list A list of geometry tables. +-- @tparam number x The x coordinate +-- @tparam number y The y coordinate +-- @return The key from the closest geometry. In case no result is found, *nil* +-- is returned. +function gears.geometry.rectangle.get_by_coord(list, x, y) + for k, geometry in pairs(list) do + if x >= geometry.x and x < geometry.x + geometry.width + and y >= geometry.y and y < geometry.y + geometry.height then + return k + end + end +end + +--- Return true whether rectangle B is in the right direction +-- compared to rectangle A. +-- @param dir The direction. +-- @param gA The geometric specification for rectangle A. +-- @param gB The geometric specification for rectangle B. +-- @return True if B is in the direction of A. +local function is_in_direction(dir, gA, gB) + if dir == "up" then + return gA.y > gB.y + elseif dir == "down" then + return gA.y < gB.y + elseif dir == "left" then + return gA.x > gB.x + elseif dir == "right" then + return gA.x < gB.x + end + return false +end + +--- Calculate distance between two points. +-- i.e: if we want to move to the right, we will take the right border +-- of the currently focused screen and the left side of the checked screen. +-- @param dir The direction. +-- @param _gA The first rectangle. +-- @param _gB The second rectangle. +-- @return The distance between the screens. +local function calculate_distance(dir, _gA, _gB) + local gAx = _gA.x + local gAy = _gA.y + local gBx = _gB.x + local gBy = _gB.y + + if dir == "up" then + gBy = _gB.y + _gB.height + elseif dir == "down" then + gAy = _gA.y + _gA.height + elseif dir == "left" then + gBx = _gB.x + _gB.width + elseif dir == "right" then + gAx = _gA.x + _gA.width + end + + return math.sqrt(math.pow(gBx - gAx, 2) + math.pow(gBy - gAy, 2)) +end + +--- Get the nearest rectangle in the given direction. Every rectangle is specified as a table +-- with *x*, *y*, *width*, *height* keys, the same as client or screen geometries. +-- @tparam string dir The direction, can be either *up*, *down*, *left* or *right*. +-- @tparam table recttbl A table of rectangle specifications. +-- @tparam table cur The current rectangle. +-- @return The index for the rectangle in recttbl closer to cur in the given direction. nil if none found. +function gears.geometry.rectangle.get_in_direction(dir, recttbl, cur) + local dist, dist_min + local target = nil + + -- We check each object + for i, rect in pairs(recttbl) do + -- Check geometry to see if object is located in the right direction. + if is_in_direction(dir, cur, rect) then + -- Calculate distance between current and checked object. + dist = calculate_distance(dir, cur, rect) + + -- If distance is shorter then keep the object. + if not target or dist < dist_min then + target = i + dist_min = dist + end + end + end + return target +end + +--- Check if an area intersect another area. +-- @param a The area. +-- @param b The other area. +-- @return True if they intersect, false otherwise. +local function area_intersect_area(a, b) + return (b.x < a.x + a.width + and b.x + b.width > a.x + and b.y < a.y + a.height + and b.y + b.height > a.y) +end + +--- Get the intersect area between a and b. +-- @tparam table a The area. +-- @tparam number a.x The horizontal coordinate +-- @tparam number a.y The vertical coordinate +-- @tparam number a.width The rectangle width +-- @tparam number a.height The rectangle height +-- @tparam table b The other area. +-- @tparam number b.x The horizontal coordinate +-- @tparam number b.y The vertical coordinate +-- @tparam number b.width The rectangle width +-- @tparam number b.height The rectangle height +-- @treturn table The intersect area. +function gears.geometry.rectangle.get_intersection(a, b) + local g = {} + g.x = math.max(a.x, b.x) + g.y = math.max(a.y, b.y) + g.width = math.min(a.x + a.width, b.x + b.width) - g.x + g.height = math.min(a.y + a.height, b.y + b.height) - g.y + return g +end + +--- Remove an area from a list, splitting the space between several area that +-- can overlap. +-- @tparam table areas Table of areas. +-- @tparam table elem Area to remove. +-- @tparam number elem.x The horizontal coordinate +-- @tparam number elem.y The vertical coordinate +-- @tparam number elem.width The rectangle width +-- @tparam number elem.height The rectangle height +-- @return The new area list. +function gears.geometry.rectangle.area_remove(areas, elem) + for i = #areas, 1, -1 do + -- Check if the 'elem' intersect + if area_intersect_area(areas[i], elem) then + -- It does? remove it + local r = table.remove(areas, i) + local inter = gears.geometry.rectangle.get_intersection(r, elem) + + if inter.x > r.x then + table.insert(areas, { + x = r.x, + y = r.y, + width = inter.x - r.x, + height = r.height + }) + end + + if inter.y > r.y then + table.insert(areas, { + x = r.x, + y = r.y, + width = r.width, + height = inter.y - r.y + }) + end + + if inter.x + inter.width < r.x + r.width then + table.insert(areas, { + x = inter.x + inter.width, + y = r.y, + width = (r.x + r.width) - (inter.x + inter.width), + height = r.height + }) + end + + if inter.y + inter.height < r.y + r.height then + table.insert(areas, { + x = r.x, + y = inter.y + inter.height, + width = r.width, + height = (r.y + r.height) - (inter.y + inter.height) + }) + end + end + end + + return areas +end + +return gears.geometry diff --git a/lib/gears/init.lua b/lib/gears/init.lua new file mode 100644 index 0000000..eae92ee --- /dev/null +++ b/lib/gears/init.lua @@ -0,0 +1,23 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @module gears +--------------------------------------------------------------------------- + + +return +{ + color = require("gears.color"); + debug = require("gears.debug"); + object = require("gears.object"); + surface = require("gears.surface"); + wallpaper = require("gears.wallpaper"); + timer = require("gears.timer"); + cache = require("gears.cache"); + matrix = require("gears.matrix"); + shape = require("gears.shape"); + protected_call = require("gears.protected_call"); + geometry = require("gears.geometry"); +} + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/matrix.lua b/lib/gears/matrix.lua new file mode 100644 index 0000000..a6bc975 --- /dev/null +++ b/lib/gears/matrix.lua @@ -0,0 +1,219 @@ +--------------------------------------------------------------------------- +-- An implementation of matrices for describing and working with affine +-- transformations. +-- @author Uli Schlachter +-- @copyright 2015 Uli Schlachter +-- @classmod gears.matrix +--------------------------------------------------------------------------- + +local cairo = require("lgi").cairo +local matrix = {} + +-- Metatable for matrix instances. This is set up near the end of the file. +local matrix_mt = {} + +--- Create a new matrix instance +-- @tparam number xx The xx transformation part. +-- @tparam number yx The yx transformation part. +-- @tparam number xy The xy transformation part. +-- @tparam number yy The yy transformation part. +-- @tparam number x0 The x0 transformation part. +-- @tparam number y0 The y0 transformation part. +-- @return A new matrix describing the given transformation. +function matrix.create(xx, yx, xy, yy, x0, y0) + return setmetatable({ + xx = xx, xy = xy, x0 = x0, + yx = yx, yy = yy, y0 = y0 + }, matrix_mt) +end + +--- Create a new translation matrix +-- @tparam number x The translation in x direction. +-- @tparam number y The translation in y direction. +-- @return A new matrix describing the given transformation. +function matrix.create_translate(x, y) + return matrix.create(1, 0, 0, 1, x, y) +end + +--- Create a new scaling matrix +-- @tparam number sx The scaling in x direction. +-- @tparam number sy The scaling in y direction. +-- @return A new matrix describing the given transformation. +function matrix.create_scale(sx, sy) + return matrix.create(sx, 0, 0, sy, 0, 0) +end + +--- Create a new rotation matrix +-- @tparam number angle The angle of the rotation in radians. +-- @return A new matrix describing the given transformation. +function matrix.create_rotate(angle) + local c, s = math.cos(angle), math.sin(angle) + return matrix.create(c, s, -s, c, 0, 0) +end + +--- Create a new rotation matrix rotating around a custom point +-- @tparam number x The horizontal rotation point +-- @tparam number y The vertical rotation point +-- @tparam number angle The angle of the rotation in radians. +-- @return A new matrix describing the given transformation. +function matrix.create_rotate_at(x, y, angle) + return matrix.create_translate( -x, -y ) + * matrix.create_rotate ( angle ) + * matrix.create_translate( x, y ) +end + +--- Translate this matrix +-- @tparam number x The translation in x direction. +-- @tparam number y The translation in y direction. +-- @return A new matrix describing the new transformation. +function matrix:translate(x, y) + return matrix.create_translate(x, y):multiply(self) +end + +--- Scale this matrix +-- @tparam number sx The scaling in x direction. +-- @tparam number sy The scaling in y direction. +-- @return A new matrix describing the new transformation. +function matrix:scale(sx, sy) + return matrix.create_scale(sx, sy):multiply(self) +end + +--- Rotate this matrix +-- @tparam number angle The angle of the rotation in radians. +-- @return A new matrix describing the new transformation. +function matrix:rotate(angle) + return matrix.create_rotate(angle):multiply(self) +end + +--- Rotate a shape from a custom point +-- @tparam number x The horizontal rotation point +-- @tparam number y The vertical rotation point +-- @tparam number angle The angle (in radiant: -2*math.pi to 2*math.pi) +-- @return A transformation object +function matrix:rotate_at(x, y, angle) + return self * matrix.create_rotate_at(x, y, angle) +end + +--- Invert this matrix +-- @return A new matrix describing the inverse transformation. +function matrix:invert() + -- Beware of math! (I just copied the algorithm from cairo's source code) + local a, b, c, d, x0, y0 = self.xx, self.yx, self.xy, self.yy, self.x0, self.y0 + local inv_det = 1/(a*d - b*c) + return matrix.create(inv_det * d, inv_det * -b, + inv_det * -c, inv_det * a, + inv_det * (c * y0 - d * x0), inv_det * (b * x0 - a * y0)) +end + +--- Multiply this matrix with another matrix. +-- The resulting matrix describes a transformation that is equivalent to first +-- applying this transformation and then the transformation from `other`. +-- Note that this function can also be called by directly multiplicating two +-- matrix instances: `a * b == a:multiply(b)`. +-- @tparam gears.matrix|cairo.Matrix other The other matrix to multiply with. +-- @return The multiplication result. +function matrix:multiply(other) + local ret = matrix.create(self.xx * other.xx + self.yx * other.xy, + self.xx * other.yx + self.yx * other.yy, + self.xy * other.xx + self.yy * other.xy, + self.xy * other.yx + self.yy * other.yy, + self.x0 * other.xx + self.y0 * other.xy + other.x0, + self.x0 * other.yx + self.y0 * other.yy + other.y0) + + return ret +end + +--- Check if two matrices are equal. +-- Note that this function cal also be called by directly comparing two matrix +-- instances: `a == b`. +-- @tparam gears.matrix|cairo.Matrix other The matrix to compare with. +-- @return True if this and the other matrix are equal. +function matrix:equals(other) + for _, k in pairs{ "xx", "xy", "yx", "yy", "x0", "y0" } do + if self[k] ~= other[k] then + return false + end + end + return true +end + +--- Get a string representation of this matrix +-- @return A string showing this matrix in column form. +function matrix:tostring() + return string.format("[[%g, %g], [%g, %g], [%g, %g]]", + self.xx, self.yx, self.xy, + self.yy, self.x0, self.y0) +end + +--- Transform a distance by this matrix. +-- The difference to @{matrix:transform_point} is that the translation part of +-- this matrix is ignored. +-- @tparam number x The x coordinate of the point. +-- @tparam number y The y coordinate of the point. +-- @treturn number The x coordinate of the transformed point. +-- @treturn number The x coordinate of the transformed point. +function matrix:transform_distance(x, y) + return self.xx * x + self.xy * y, self.yx * x + self.yy * y +end + +--- Transform a point by this matrix. +-- @tparam number x The x coordinate of the point. +-- @tparam number y The y coordinate of the point. +-- @treturn number The x coordinate of the transformed point. +-- @treturn number The y coordinate of the transformed point. +function matrix:transform_point(x, y) + x, y = self:transform_distance(x, y) + return self.x0 + x, self.y0 + y +end + +--- Calculate a bounding rectangle for transforming a rectangle by a matrix. +-- @tparam number x The x coordinate of the rectangle. +-- @tparam number y The y coordinate of the rectangle. +-- @tparam number width The width of the rectangle. +-- @tparam number height The height of the rectangle. +-- @treturn number X coordinate of the bounding rectangle. +-- @treturn number Y coordinate of the bounding rectangle. +-- @treturn number Width of the bounding rectangle. +-- @treturn number Height of the bounding rectangle. +function matrix:transform_rectangle(x, y, width, height) + -- Transform all four corners of the rectangle + local x1, y1 = self:transform_point(x, y) + local x2, y2 = self:transform_point(x, y + height) + local x3, y3 = self:transform_point(x + width, y + height) + local x4, y4 = self:transform_point(x + width, y) + -- Find the extremal points of the result + x = math.min(x1, x2, x3, x4) + y = math.min(y1, y2, y3, y4) + width = math.max(x1, x2, x3, x4) - x + height = math.max(y1, y2, y3, y4) - y + + return x, y, width, height +end + +--- Convert to a cairo matrix +-- @treturn cairo.Matrix A cairo matrix describing the same transformation. +function matrix:to_cairo_matrix() + local ret = cairo.Matrix() + ret:init(self.xx, self.yx, self.xy, self.yy, self.x0, self.y0) + return ret +end + +--- Convert to a cairo matrix +-- @tparam cairo.Matrix mat A cairo matrix describing the sought transformation +-- @treturn gears.matrix A matrix instance describing the same transformation. +function matrix.from_cairo_matrix(mat) + return matrix.create(mat.xx, mat.yx, mat.xy, mat.yy, mat.x0, mat.y0) +end + +matrix_mt.__index = matrix +matrix_mt.__newindex = error +matrix_mt.__eq = matrix.equals +matrix_mt.__mul = matrix.multiply +matrix_mt.__tostring = matrix.tostring + +--- A constant for the identity matrix. +matrix.identity = matrix.create(1, 0, 0, 1, 0, 0) + +return matrix + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/object.lua b/lib/gears/object.lua new file mode 100644 index 0000000..e6436e3 --- /dev/null +++ b/lib/gears/object.lua @@ -0,0 +1,285 @@ +--------------------------------------------------------------------------- +-- The object oriented programming base class used by various Awesome +-- widgets and components. +-- +-- It provide basic observer pattern, signaling and dynamic properties. +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod gears.object +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local pairs = pairs +local type = type +local error = error +local properties = require("gears.object.properties") + +local object = { properties = properties, mt = {} } + +--- Verify that obj is indeed a valid object as returned by new() +local function check(obj) + if type(obj) ~= "table" or type(obj._signals) ~= "table" then + error("called on non-object") + end +end + +--- Find a given signal +-- @tparam table obj The object to search in +-- @tparam string name The signal to find +-- @treturn table The signal table +local function find_signal(obj, name) + check(obj) + if not obj._signals[name] then + assert(type(name) == "string", "name must be a string, got: " .. type(name)) + obj._signals[name] = { + strong = {}, + weak = setmetatable({}, { __mode = "kv" }) + } + end + return obj._signals[name] +end + +function object.add_signal() + require("awful.util").deprecate("Use signals without explicitly adding them. This is now done implicitly.") +end + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +function object:connect_signal(name, func) + assert(type(func) == "function", "callback must be a function, got: " .. type(func)) + local sig = find_signal(self, name) + assert(sig.weak[func] == nil, "Trying to connect a strong callback which is already connected weakly") + sig.strong[func] = true +end + +local function make_the_gc_obey(func) + if _VERSION <= "Lua 5.1" then + -- Lua 5.1 only has the behaviour we want if a userdata is used as the + -- value in a weak table. Thus, do some magic so that we get a userdata. + + -- luacheck: globals newproxy getfenv setfenv + local userdata = newproxy(true) + getmetatable(userdata).__gc = function() end + -- Now bind the lifetime of userdata to the lifetime of func. For this, + -- we mess with the function's environment and add a table for all the + -- various userdata that it should keep alive. + local key = "_secret_key_used_by_gears_object_in_Lua51" + local old_env = getfenv(func) + if old_env[key] then + -- Assume the code in the else branch added this and the function + -- already has its own, private environment + table.insert(old_env[key], userdata) + else + -- No table yet, add it + local new_env = { [key] = { userdata } } + setmetatable(new_env, { __index = old_env, __newindex = old_env }) + setfenv(func, new_env) + end + assert(_G[key] == nil, "Something broke, things escaped to _G") + return userdata + end + -- Lua 5.2+ already behaves the way we want with functions directly, no magic + return func +end + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +function object:weak_connect_signal(name, func) + assert(type(func) == "function", "callback must be a function, got: " .. type(func)) + local sig = find_signal(self, name) + assert(sig.strong[func] == nil, "Trying to connect a weak callback which is already connected strongly") + sig.weak[func] = make_the_gc_obey(func) +end + +--- Disonnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +function object:disconnect_signal(name, func) + local sig = find_signal(self, name) + sig.weak[func] = nil + sig.strong[func] = nil +end + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +function object:emit_signal(name, ...) + local sig = find_signal(self, name) + for func in pairs(sig.strong) do + func(self, ...) + end + for func in pairs(sig.weak) do + func(self, ...) + end +end + +local function get_miss(self, key) + local class = rawget(self, "_class") + + if rawget(self, "get_"..key) then + return rawget(self, "get_"..key)(self) + elseif class and class["get_"..key] then + return class["get_"..key](self) + elseif class then + return class[key] + end + +end + +local function set_miss(self, key, value) + local class = rawget(self, "_class") + + if rawget(self, "set_"..key) then + return rawget(self, "set_"..key)(self, value) + elseif class and class["set_"..key] then + return class["set_"..key](self, value) + elseif rawget(self, "_enable_auto_signals") then + local changed = class[key] ~= value + class[key] = value + + if changed then + self:emit_signal("property::"..key, value) + end + elseif (not rawget(self, "get_"..key)) + and not (class and class["get_"..key]) then + return rawset(self, key, value) + else + error("Cannot set '" .. tostring(key) .. "' on " .. tostring(self) + .. " because it is read-only") + end +end + +--- Returns a new object. You can call `:emit_signal()`, `:disconnect_signal()` +-- and `:connect_signal()` on the resulting object. +-- +-- Note that `args.enable_auto_signals` is only supported when +-- `args.enable_properties` is true. +-- +-- +-- +-- +--**Usage example output**: +-- +-- In get foo bar +-- bar +-- In set foo 42 +-- In get foo 42 +-- 42 +-- In a mathod 1 2 3 +-- nil +-- In the connection handler! a cow +-- a cow +-- +-- +-- @usage +-- -- Create a class for this object. It will be used as a backup source for +-- -- methods and accessors. It is also possible to set them directly on the +-- -- object. +--local class = {} +--function class:get_foo() +-- print('In get foo', self._foo or 'bar') +-- return self._foo or 'bar' +--end +--function class:set_foo(value) +-- print('In set foo', value) +-- -- In case it is necessary to bypass the object property system, use +-- -- `rawset` +-- rawset(self, '_foo', value) +-- -- When using custom accessors, the signals need to be handled manually +-- self:emit_signal('property::foo', value) +--end +--function class:method(a, b, c) +-- print('In a mathod', a, b, c) +--end +--local o = gears.object { +-- class = class, +-- enable_properties = true, +-- enable_auto_signals = true, +--} +--print(o.foo) +--o.foo = 42 +--print(o.foo) +--o:method(1, 2, 3) +-- -- Random properties can also be added, the signal will be emitted automatically. +--o:connect_signal('property::something', function(obj, value) +-- assert(obj == o) +-- print('In the connection handler!', value) +--end) +--print(o.something) +--o.something = 'a cow' +--print(o.something) +-- @tparam[opt={}] table args The arguments +-- @tparam[opt=false] boolean args.enable_properties Automatically call getters and setters +-- @tparam[opt=false] boolean args.enable_auto_signals Generate "property::xxxx" signals +-- when an unknown property is set. +-- @tparam[opt=nil] table args.class +-- @treturn table A new object +-- @function gears.object +local function new(args) + args = args or {} + local ret = {} + + -- Automatic signals cannot work without both miss handlers. + assert(not (args.enable_auto_signals and args.enable_properties ~= true)) + + -- Copy all our global functions to our new object + for k, v in pairs(object) do + if type(v) == "function" then + ret[k] = v + end + end + + ret._signals = {} + + local mt = {} + + -- Look for methods in another table + ret._class = args.class + ret._enable_auto_signals = args.enable_auto_signals + + -- To catch all changes, a proxy is required + if args.enable_auto_signals then + ret._class = ret._class and setmetatable({}, {__index = args.class}) or {} + end + + if args.enable_properties then + -- Check got existing get_xxxx and set_xxxx + mt.__index = get_miss + mt.__newindex = set_miss + elseif args.class then + -- Use the class table a miss handler + mt.__index = ret._class + end + + return setmetatable(ret, mt) +end + +function object.mt.__call(_, ...) + return new(...) +end + +--- Helper function to get the module name out of `debug.getinfo`. +-- @usage +-- local mt = {} +-- mt.__tostring = function(o) +-- return require("gears.object").modulename(2) +-- end +-- return setmetatable(ret, mt) +-- +-- @tparam[opt=2] integer level Level for `debug.getinfo(level, "S")`. +-- Typically 2 or 3. +-- @treturn string The module name, e.g. "wibox.container.background". +function object.modulename(level) + return debug.getinfo(level, "S").source:gsub(".*/lib/", ""):gsub("/", "."):gsub("%.lua", "") +end + +return setmetatable(object, object.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/object/properties.lua b/lib/gears/object/properties.lua new file mode 100644 index 0000000..36b8fcb --- /dev/null +++ b/lib/gears/object/properties.lua @@ -0,0 +1,88 @@ +--------------------------------------------------------------------------- +--- An helper module to map userdata __index and __newindex entries to +-- lua classes. +-- +-- @author Emmanuel Lepage-Vallee <elv1313@gmail.com> +-- @copyright 2016 Emmanuel Lepage-Vallee +-- @module gears.object.properties +--------------------------------------------------------------------------- + +local object = {} + + +--- Add the missing properties handler to a CAPI object such as client/tag/screen. +-- Valid args: +-- +-- * **getter**: A smart getter (handle property getter itself) +-- * **getter_fallback**: A dumb getter method (don't handle individual property getter) +-- * **getter_class**: A module with individual property getter/setter +-- * **getter_prefix**: A special getter prefix (like "get" or "get_" (default)) +-- * **setter**: A smart setter (handle property setter itself) +-- * **setter_fallback**: A dumb setter method (don't handle individual property setter) +-- * **setter_class**: A module with individual property getter/setter +-- * **setter_prefix**: A special setter prefix (like "set" or "set_" (default)) +-- * **auto_emit**: Emit "property::___" automatically (default: false). This is +-- ignored when setter_fallback is set or a setter is found +-- +-- @param class A standard luaobject derived object +-- @tparam[opt={}] table args A set of accessors configuration parameters +function object.capi_index_fallback(class, args) + args = args or {} + + local getter_prefix = args.getter_prefix or "get_" + local setter_prefix = args.setter_prefix or "set_" + + local getter = args.getter or function(cobj, prop) + -- Look for a getter method + if args.getter_class and args.getter_class[getter_prefix..prop] then + return args.getter_class[getter_prefix..prop](cobj) + elseif args.getter_class and args.getter_class["is_"..prop] then + return args.getter_class["is_"..prop](cobj) + end + + -- Make sure something like c:a_mutator() works + if args.getter_class and args.getter_class[prop] then + return args.getter_class[prop] + end + -- In case there is already a "dumb" getter like `awful.tag.getproperty' + if args.getter_fallback then + return args.getter_fallback(cobj, prop) + end + + -- Use the fallback property table + return cobj.data[prop] + end + + local setter = args.setter or function(cobj, prop, value) + -- Look for a setter method + if args.setter_class and args.setter_class[setter_prefix..prop] then + return args.setter_class[setter_prefix..prop](cobj, value) + end + + -- In case there is already a "dumb" setter like `awful.client.property.set' + if args.setter_fallback then + return args.setter_fallback(cobj, prop, value) + end + + -- If a getter exists but not a setter, then the property is read-only + if args.getter_class and args.getter_class[getter_prefix..prop] then + return + end + + -- Use the fallback property table + cobj.data[prop] = value + + -- Emit the signal + if args.auto_emit then + cobj:emit_signal("property::"..prop, value) + end + end + + -- Attach the accessor methods + class.set_index_miss_handler(getter) + class.set_newindex_miss_handler(setter) +end + +return setmetatable( object, {__call = function(_,...) object.capi_index_fallback(...) end}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/protected_call.lua b/lib/gears/protected_call.lua new file mode 100644 index 0000000..c182e14 --- /dev/null +++ b/lib/gears/protected_call.lua @@ -0,0 +1,57 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2016 Uli Schlachter +-- @module gears.protected_call +--------------------------------------------------------------------------- + +local gdebug = require("gears.debug") +local tostring = tostring +local traceback = debug.traceback +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local xpcall = xpcall + +local protected_call = {} + +local function error_handler(err) + gdebug.print_error(traceback("Error during a protected call: " .. tostring(err), 2)) +end + +local function handle_result(success, ...) + if success then + return ... + end +end + +local do_pcall +if _VERSION <= "Lua 5.1" then + -- Lua 5.1 doesn't support arguments in xpcall :-( + do_pcall = function(func, ...) + local args = { ... } + return handle_result(xpcall(function() + return func(unpack(args)) + end, error_handler)) + end +else + do_pcall = function(func, ...) + return handle_result(xpcall(func, error_handler, ...)) + end +end + +--- Call a function in protected mode and handle error-reporting. +-- If the function call succeeds, all results of the function are returned. +-- Otherwise, an error message is printed and nothing is returned. +-- @tparam function func The function to call +-- @param ... Arguments to the function +-- @return The result of the given function, or nothing if an error occurred. +function protected_call.call(func, ...) + return do_pcall(func, ...) +end + +local pcall_mt = {} +function pcall_mt:__call(...) + return do_pcall(...) +end + +return setmetatable(protected_call, pcall_mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/shape.lua b/lib/gears/shape.lua new file mode 100644 index 0000000..4962d78 --- /dev/null +++ b/lib/gears/shape.lua @@ -0,0 +1,785 @@ +--------------------------------------------------------------------------- +--- Module dedicated to gather common shape painters. +-- +-- It add the concept of "shape" to Awesome. A shape can be applied to a +-- background, a margin, a mask or a drawable shape bounding. +-- +-- The functions exposed by this module always take a context as first +-- parameter followed by the widget and height and additional parameters. +-- +-- The functions provided by this module only create a path in the content. +-- to actually draw the content, use `cr:fill()`, `cr:mask()`, `cr:clip()` or +-- `cr:stroke()` +-- +-- In many case, it is necessary to apply the shape using a transformation +-- such as a rotation. The preferred way to do this is to wrap the function +-- in another function calling `cr:rotate()` (or any other transformation +-- matrix). +-- +-- To specialize a shape where the API doesn't allows extra arguments to be +-- passed, it is possible to wrap the shape function like: +-- +-- local new_shape = function(cr, width, height) +-- gears.shape.rounded_rect(cr, width, height, 2) +-- end +-- +-- Many elements can be shaped. This include: +-- +-- * `client`s (see `gears.surface.apply_shape_bounding`) +-- * `wibox`es (see `wibox.shape`) +-- * All widgets (see `wibox.container.background`) +-- * The progressbar (see `wibox.widget.progressbar.bar_shape`) +-- * The graph (see `wibox.widget.graph.step_shape`) +-- * The checkboxes (see `wibox.widget.checkbox.check_shape`) +-- * Images (see `wibox.widget.imagebox.clip_shape`) +-- * The taglist tags (see `awful.widget.taglist`) +-- * The tasklist clients (see `awful.widget.tasklist`) +-- * The tooltips (see `awful.tooltip`) +-- +-- @author Emmanuel Lepage Vallee +-- @copyright 2011-2016 Emmanuel Lepage Vallee +-- @module gears.shape +--------------------------------------------------------------------------- +local g_matrix = require( "gears.matrix" ) +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local atan2 = math.atan2 or math.atan -- lua 5.3 compat + +local module = {} + +--- Add a rounded rectangle to the current path. +-- Note: If the radius is bigger than either half side, it will be reduced. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_rounded_rect.svg) +-- +-- @usage +--shape.rounded_rect(cr, 70, 70, 10) +--shape.rounded_rect(cr,20,70, 5) +--shape.transform(shape.rounded_rect) : translate(0,25) (cr,70,20, 5) +-- +-- @param cr A cairo content +-- @tparam number width The rectangle width +-- @tparam number height The rectangle height +-- @tparam number radius the corner radius +function module.rounded_rect(cr, width, height, radius) + + radius = radius or 10 + + if width / 2 < radius then + radius = width / 2 + end + + if height / 2 < radius then + radius = height / 2 + end + + cr:move_to(0, radius) + + cr:arc( radius , radius , radius, math.pi , 3*(math.pi/2) ) + cr:arc( width-radius, radius , radius, 3*(math.pi/2), math.pi*2 ) + cr:arc( width-radius, height-radius, radius, math.pi*2 , math.pi/2 ) + cr:arc( radius , height-radius, radius, math.pi/2 , math.pi ) + + cr:close_path() +end + +--- Add a rectangle delimited by 2 180 degree arcs to the path. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_rounded_bar.svg) +-- +-- @usage +--shape.rounded_bar(cr, 70, 70) +--shape.rounded_bar(cr, 20, 70) +--shape.rounded_bar(cr, 70, 20) +-- +-- @param cr A cairo content +-- @param width The rectangle width +-- @param height The rectangle height +function module.rounded_bar(cr, width, height) + module.rounded_rect(cr, width, height, height / 2) +end + +--- A rounded rect with only some of the corners rounded. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_partially_rounded_rect.svg) +-- +-- @usage +--shape.partially_rounded_rect(cr, 70, 70) +--shape.partially_rounded_rect(cr, 70, 70, true) +--shape.partially_rounded_rect(cr, 70, 70, true, true, false, true, 30) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam boolean tl If the top left corner is rounded +-- @tparam boolean tr If the top right corner is rounded +-- @tparam boolean br If the bottom right corner is rounded +-- @tparam boolean bl If the bottom left corner is rounded +-- @tparam number rad The corner radius +function module.partially_rounded_rect(cr, width, height, tl, tr, br, bl, rad) + rad = rad or 10 + if width / 2 < rad then + rad = width / 2 + end + + if height / 2 < rad then + rad = height / 2 + end + + -- Top left + if tl then + cr:arc( rad, rad, rad, math.pi, 3*(math.pi/2)) + else + cr:move_to(0,0) + end + + -- Top right + if tr then + cr:arc( width-rad, rad, rad, 3*(math.pi/2), math.pi*2) + else + cr:line_to(width, 0) + end + + -- Bottom right + if br then + cr:arc( width-rad, height-rad, rad, math.pi*2 , math.pi/2) + else + cr:line_to(width, height) + end + + -- Bottom left + if bl then + cr:arc( rad, height-rad, rad, math.pi/2, math.pi) + else + cr:line_to(0, height) + end + + cr:close_path() +end + +--- A rounded rectangle with a triangle at the top. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_infobubble.svg) +-- +-- @usage +--shape.infobubble(cr, 70, 70) +--shape.transform(shape.infobubble) : translate(0, 20) +-- : rotate_at(35,35,math.pi) (cr,70,20,10, 5, 35 - 5) +--shape.transform(shape.infobubble) +-- : rotate_at(35,35,3*math.pi/2) (cr,70,70, nil, nil, 40) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=5] number corner_radius The corner radius +-- @tparam[opt=10] number arrow_size The width and height of the arrow +-- @tparam[opt=width/2 - arrow_size/2] number arrow_position The position of the arrow +function module.infobubble(cr, width, height, corner_radius, arrow_size, arrow_position) + arrow_size = arrow_size or 10 + corner_radius = math.min((height-arrow_size)/2, corner_radius or 5) + arrow_position = arrow_position or width/2 - arrow_size/2 + + + cr:move_to(0 ,corner_radius+arrow_size) + + -- Top left corner + cr:arc(corner_radius, corner_radius+arrow_size, (corner_radius), math.pi, 3*(math.pi/2)) + + -- The arrow triangle (still at the top) + cr:line_to(arrow_position , arrow_size ) + cr:line_to(arrow_position + arrow_size , 0 ) + cr:line_to(arrow_position + 2*arrow_size , arrow_size ) + + -- Complete the rounded rounded rectangle + cr:arc(width-corner_radius, corner_radius+arrow_size , (corner_radius) , 3*(math.pi/2) , math.pi*2 ) + cr:arc(width-corner_radius, height-(corner_radius) , (corner_radius) , math.pi*2 , math.pi/2 ) + cr:arc(corner_radius , height-(corner_radius) , (corner_radius) , math.pi/2 , math.pi ) + + -- Close path + cr:close_path() +end + +--- A rectangle terminated by an arrow. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_rectangular_tag.svg) +-- +-- @usage +--shape.rectangular_tag(cr, 70, 70) +--shape.transform(shape.rectangular_tag) : translate(0, 30) (cr, 70, 10, 10) +--shape.transform(shape.rectangular_tag) : translate(0, 30) (cr, 70, 10, -10) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=height/2] number arrow_length The length of the arrow part +function module.rectangular_tag(cr, width, height, arrow_length) + arrow_length = arrow_length or height/2 + if arrow_length > 0 then + cr:move_to(0 , height/2 ) + cr:line_to(arrow_length , 0 ) + cr:line_to(width , 0 ) + cr:line_to(width , height ) + cr:line_to(arrow_length , height ) + else + cr:move_to(0 , 0 ) + cr:line_to(-arrow_length, height/2 ) + cr:line_to(0 , height ) + cr:line_to(width , height ) + cr:line_to(width , 0 ) + end + + cr:close_path() +end + +--- A simple arrow shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_arrow.svg) +-- +-- @usage +--shape.arrow(cr, 70, 70) +--shape.arrow(cr,70,70, 30, 10, 60) +--shape.transform(shape.arrow) : rotate_at(35,35,math.pi/2)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=head_width] number head_width The width of the head (/\) of the arrow +-- @tparam[opt=width /2] number shaft_width The width of the shaft of the arrow +-- @tparam[opt=height/2] number shaft_length The head_length of the shaft (the rest is the head) +function module.arrow(cr, width, height, head_width, shaft_width, shaft_length) + shaft_length = shaft_length or height / 2 + shaft_width = shaft_width or width / 2 + head_width = head_width or width + local head_length = height - shaft_length + + cr:move_to ( width/2 , 0 ) + cr:rel_line_to( head_width/2 , head_length ) + cr:rel_line_to( -(head_width-shaft_width)/2 , 0 ) + cr:rel_line_to( 0 , shaft_length ) + cr:rel_line_to( -shaft_width , 0 ) + cr:rel_line_to( 0 , -shaft_length ) + cr:rel_line_to( -(head_width-shaft_width)/2 , 0 ) + + cr:close_path() +end + +--- A squeezed hexagon filling the rectangle. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_hexagon.svg) +-- +-- @usage +--shape.hexagon(cr, 70, 70) +--shape.transform(shape.hexagon) : translate(0,15)(cr,70,20) +--shape.transform(shape.hexagon) : rotate_at(35,35,math.pi/2)(cr,70,40) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +function module.hexagon(cr, width, height) + cr:move_to(height/2,0) + cr:line_to(width-height/2,0) + cr:line_to(width,height/2) + cr:line_to(width-height/2,height) + cr:line_to(height/2,height) + cr:line_to(0,height/2) + cr:line_to(height/2,0) + cr:close_path() +end + +--- Double arrow popularized by the vim-powerline module. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_powerline.svg) +-- +-- @usage +--shape.powerline(cr, 70, 70) +--shape.transform(shape.powerline) : translate(0, 25) (cr,70,20) +--shape.transform(shape.powerline) : translate(0, 25) (cr,70,20, -20) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=height/2] number arrow_depth The width of the arrow part of the shape +function module.powerline(cr, width, height, arrow_depth) + arrow_depth = arrow_depth or height/2 + local offset = 0 + + -- Avoid going out of the (potential) clip area + if arrow_depth < 0 then + width = width + 2*arrow_depth + offset = -arrow_depth + end + + cr:move_to(offset , 0 ) + cr:line_to(offset + width - arrow_depth , 0 ) + cr:line_to(offset + width , height/2 ) + cr:line_to(offset + width - arrow_depth , height ) + cr:line_to(offset , height ) + cr:line_to(offset + arrow_depth , height/2 ) + + cr:close_path() +end + +--- An isosceles triangle. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_isosceles_triangle.svg) +-- +-- @usage +--shape.isosceles_triangle(cr, 70, 70) +--shape.isosceles_triangle(cr,20,70) +--shape.transform(shape.isosceles_triangle) : rotate_at(35, 35, math.pi/2)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +function module.isosceles_triangle(cr, width, height) + cr:move_to( width/2, 0 ) + cr:line_to( width , height ) + cr:line_to( 0 , height ) + cr:close_path() +end + +--- A cross (**+**) symbol. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_cross.svg) +-- +-- @usage +--shape.cross(cr, 70, 70) +--shape.cross(cr,20,70) +--shape.transform(shape.cross) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=width/3] number thickness The cross section thickness +function module.cross(cr, width, height, thickness) + thickness = thickness or width/3 + local xpadding = (width - thickness) / 2 + local ypadding = (height - thickness) / 2 + cr:move_to(xpadding, 0) + cr:line_to(width - xpadding, 0) + cr:line_to(width - xpadding, ypadding) + cr:line_to(width , ypadding) + cr:line_to(width , height-ypadding) + cr:line_to(width - xpadding, height-ypadding) + cr:line_to(width - xpadding, height ) + cr:line_to(xpadding , height ) + cr:line_to(xpadding , height-ypadding) + cr:line_to(0 , height-ypadding) + cr:line_to(0 , ypadding ) + cr:line_to(xpadding , ypadding ) + cr:close_path() +end + +--- A similar shape to the `rounded_rect`, but with sharp corners. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_octogon.svg) +-- +-- @usage +--shape.octogon(cr, 70, 70) +--shape.octogon(cr,70,70,70/2.5) +--shape.transform(shape.octogon) : translate(0, 25) (cr,70,20) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam number corner_radius +function module.octogon(cr, width, height, corner_radius) + corner_radius = corner_radius or math.min(10, math.min(width, height)/4) + local offset = math.sqrt( (corner_radius*corner_radius) / 2 ) + + cr:move_to(offset, 0) + cr:line_to(width-offset, 0) + cr:line_to(width, offset) + cr:line_to(width, height-offset) + cr:line_to(width-offset, height) + cr:line_to(offset, height) + cr:line_to(0, height-offset) + cr:line_to(0, offset) + cr:close_path() +end + +--- A circle shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_circle.svg) +-- +-- @usage +--shape.circle(cr, 70, 70) +--shape.circle(cr,20,70) +--shape.transform(shape.circle) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=math.min(width height) / 2)] number radius The radius +function module.circle(cr, width, height, radius) + radius = radius or math.min(width, height) / 2 + cr:move_to(width/2+radius, height/2) + cr:arc(width / 2, height / 2, radius, 0, 2*math.pi) + cr:close_path() +end + +--- A simple rectangle. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_rectangle.svg) +-- +-- @usage +--shape.rectangle(cr, 70, 70) +--shape.rectangle(cr,20,70) +--shape.transform(shape.rectangle) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +function module.rectangle(cr, width, height) + cr:rectangle(0, 0, width, height) +end + +--- A diagonal parallelogram with the bottom left corner at x=0 and top right +-- at x=width. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_parallelogram.svg) +-- +-- @usage +--shape.parallelogram(cr, 70, 70) +--shape.parallelogram(cr,70,20) +--shape.transform(shape.parallelogram) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=width/3] number base_width The parallelogram base width +function module.parallelogram(cr, width, height, base_width) + base_width = base_width or width/3 + cr:move_to(width-base_width, 0 ) + cr:line_to(width , 0 ) + cr:line_to(base_width , height ) + cr:line_to(0 , height ) + cr:close_path() +end + +--- A losange. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_losange.svg) +-- +-- @usage +--shape.losange(cr, 70, 70) +--shape.losange(cr,20,70) +--shape.transform(shape.losange) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +function module.losange(cr, width, height) + cr:move_to(width/2 , 0 ) + cr:line_to(width , height/2 ) + cr:line_to(width/2 , height ) + cr:line_to(0 , height/2 ) + cr:close_path() +end + +--- A pie. +-- +-- The pie center is the center of the area. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_pie.svg) +-- +-- @usage +--shape.pie(cr, 70, 70) +--shape.pie(cr,70,70, 1.0471975511966, 4.1887902047864) +--shape.pie(cr,70,70, 0, 2*math.pi, 10) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=0] number start_angle The start angle (in radian) +-- @tparam[opt=math.pi/2] number end_angle The end angle (in radian) +-- @tparam[opt=math.min(width height)/2] number radius The shape height +function module.pie(cr, width, height, start_angle, end_angle, radius) + radius = radius or math.floor(math.min(width, height)/2) + start_angle, end_angle = start_angle or 0, end_angle or math.pi/2 + + -- If the shape is a circle, then avoid the lines + if math.abs(start_angle + end_angle - 2*math.pi) <= 0.01 then + cr:arc(width/2, height/2, radius, 0, 2*math.pi) + else + cr:move_to(width/2, height/2) + cr:line_to( + width/2 + math.cos(start_angle)*radius, + height/2 + math.sin(start_angle)*radius + ) + cr:arc(width/2, height/2, radius, start_angle, end_angle) + end + + cr:close_path() +end + +--- A rounded arc. +-- +-- The pie center is the center of the area. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_arc.svg) +-- +-- @usage +--shape.arc(cr,70,70, 10) +--shape.arc(cr,70,70, 10, nil, nil, true, true) +--shape.arc(cr,70,70, nil, 0, 2*math.pi) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=math.min(width height)/2] number thickness The arc thickness +-- @tparam[opt=0] number start_angle The start angle (in radian) +-- @tparam[opt=math.pi/2] number end_angle The end angle (in radian) +-- @tparam[opt=false] boolean start_rounded if the arc start rounded +-- @tparam[opt=false] boolean end_rounded if the arc end rounded +function module.arc(cr, width, height, thickness, start_angle, end_angle, start_rounded, end_rounded) + start_angle = start_angle or 0 + end_angle = end_angle or math.pi/2 + + -- This shape is a partial circle + local radius = math.min(width, height)/2 + + thickness = thickness or radius/2 + + local inner_radius = radius - thickness + + -- As the edge of the small arc need to touch the [start_p1, start_p2] + -- line, a small subset of the arc circumference has to be substracted + -- that's (less or more) equal to the thickness/2 (a little longer given + -- it is an arc and not a line, but it wont show) + local arc_percent = math.abs(end_angle-start_angle)/(2*math.pi) + local arc_length = ((radius-thickness/2)*2*math.pi)*arc_percent + + if start_rounded then + arc_length = arc_length - thickness/2 + + -- And back to angles + start_angle = end_angle - (arc_length/(radius - thickness/2)) + end + + if end_rounded then + arc_length = arc_length - thickness/2 + + -- And back to angles + end_angle = start_angle + (arc_length/(radius - thickness/2)) + end + + -- The path is a curcular arc joining 4 points + + -- Outer first corner + local start_p1 = { + width /2 + math.cos(start_angle)*radius, + height/2 + math.sin(start_angle)*radius + } + + if start_rounded then + + -- Inner first corner + local start_p2 = { + width /2 + math.cos(start_angle)*inner_radius, + height/2 + math.sin(start_angle)*inner_radius + } + + local median_angle = atan2( + start_p2[1] - start_p1[1], + -(start_p2[2] - start_p1[2]) + ) + + local arc_center = { + (start_p1[1] + start_p2[1])/2, + (start_p1[2] + start_p2[2])/2, + } + + cr:arc(arc_center[1], arc_center[2], thickness/2, + median_angle-math.pi/2, median_angle+math.pi/2 + ) + + else + cr:move_to(unpack(start_p1)) + end + + cr:arc(width/2, height/2, radius, start_angle, end_angle) + + if end_rounded then + + -- Outer second corner + local end_p1 = { + width /2 + math.cos(end_angle)*radius, + height/2 + math.sin(end_angle)*radius + } + + -- Inner first corner + local end_p2 = { + width /2 + math.cos(end_angle)*inner_radius, + height/2 + math.sin(end_angle)*inner_radius + } + local median_angle = atan2( + end_p2[1] - end_p1[1], + -(end_p2[2] - end_p1[2]) + ) - math.pi + + local arc_center = { + (end_p1[1] + end_p2[1])/2, + (end_p1[2] + end_p2[2])/2, + } + + cr:arc(arc_center[1], arc_center[2], thickness/2, + median_angle-math.pi/2, median_angle+math.pi/2 + ) + + end + + cr:arc_negative(width/2, height/2, inner_radius, end_angle, start_angle) + + cr:close_path() +end + +--- A partial rounded bar. How much of the rounded bar is visible depends on +-- the given percentage value. +-- +-- Note that this shape is not closed and thus filling it doesn't make much +-- sense. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_radial_progress.svg) +-- +-- @usage +--shape.radial_progress(cr, 70, 20, .3) +--shape.radial_progress(cr, 70, 20, .6) +--shape.radial_progress(cr, 70, 20, .9) +-- +-- @param cr A cairo context +-- @tparam number w The shape width +-- @tparam number h The shape height +-- @tparam number percent The progressbar percent +-- @tparam boolean hide_left Do not draw the left side of the shape +function module.radial_progress(cr, w, h, percent, hide_left) + percent = percent or 1 + local total_length = (2*(w-h))+2*((h/2)*math.pi) + local bar_percent = (w-h)/total_length + local arc_percent = ((h/2)*math.pi)/total_length + + -- Bottom line + if percent > bar_percent then + cr:move_to(h/2,h) + cr:line_to((h/2) + (w-h),h) + cr:stroke() + elseif percent < bar_percent then + cr:move_to(h/2,h) + cr:line_to(h/2+(total_length*percent),h) + cr:stroke() + end + + -- Right arc + if percent >= bar_percent+arc_percent then + cr:arc(w-h/2 , h/2, h/2,3*(math.pi/2),math.pi/2) + cr:stroke() + elseif percent > bar_percent and percent < bar_percent+(arc_percent/2) then + cr:arc(w-h/2 , h/2, h/2,(math.pi/2)-((math.pi/2)*((percent-bar_percent)/(arc_percent/2))),math.pi/2) + cr:stroke() + elseif percent >= bar_percent+arc_percent/2 and percent < bar_percent+arc_percent then + cr:arc(w-h/2 , h/2, h/2,0,math.pi/2) + cr:stroke() + local add = (math.pi/2)*((percent-bar_percent-arc_percent/2)/(arc_percent/2)) + cr:arc(w-h/2 , h/2, h/2,2*math.pi-add,0) + cr:stroke() + end + + -- Top line + if percent > 2*bar_percent+arc_percent then + cr:move_to((h/2) + (w-h),0) + cr:line_to(h/2,0) + cr:stroke() + elseif percent > bar_percent+arc_percent and percent < 2*bar_percent+arc_percent then + cr:move_to((h/2) + (w-h),0) + cr:line_to(((h/2) + (w-h))-total_length*(percent-bar_percent-arc_percent),0) + cr:stroke() + end + + -- Left arc + if not hide_left then + if percent > 0.985 then + cr:arc(h/2, h/2, h/2,math.pi/2,3*(math.pi/2)) + cr:stroke() + elseif percent > 2*bar_percent+arc_percent then + local relpercent = (percent - 2*bar_percent - arc_percent)/arc_percent + cr:arc(h/2, h/2, h/2,3*(math.pi/2)-(math.pi)*relpercent,3*(math.pi/2)) + cr:stroke() + end + end +end + +--- Adjust the shape using a transformation object +-- +-- Apply various transformations to the shape +-- +-- @usage gears.shape.transform(gears.shape.rounded_bar) +-- : rotate(math.pi/2) +-- : translate(10, 10) +-- +-- @param shape A shape function +-- @return A transformation handle, also act as a shape function +function module.transform(shape) + + -- Apply the transformation matrix and apply the shape, then restore + local function apply(self, cr, width, height, ...) + cr:save() + cr:transform(self.matrix:to_cairo_matrix()) + shape(cr, width, height, ...) + cr:restore() + end + -- Redirect function calls like :rotate() to the underlying matrix + local function index(_, key) + return function(self, ...) + self.matrix = self.matrix[key](self.matrix, ...) + return self + end + end + + local result = setmetatable({ + matrix = g_matrix.identity + }, { + __call = apply, + __index = index + }) + + return result +end + +return module + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/surface.lua b/lib/gears/surface.lua new file mode 100644 index 0000000..78f2216 --- /dev/null +++ b/lib/gears/surface.lua @@ -0,0 +1,252 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2012 Uli Schlachter +-- @module gears.surface +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local type = type +local capi = { awesome = awesome } +local cairo = require("lgi").cairo +local color = nil +local gdebug = require("gears.debug") +local hierarchy = require("wibox.hierarchy") + +-- Keep this in sync with build-utils/lgi-check.sh! +local ver_major, ver_minor, ver_patch = string.match(require('lgi.version'), '(%d)%.(%d)%.(%d)') +if tonumber(ver_major) <= 0 and (tonumber(ver_minor) < 7 or (tonumber(ver_minor) == 7 and tonumber(ver_patch) < 1)) then + error("lgi too old, need at least version 0.7.1") +end + +local surface = { mt = {} } +local surface_cache = setmetatable({}, { __mode = 'v' }) + +local function get_default(arg) + if type(arg) == 'nil' then + return cairo.ImageSurface(cairo.Format.ARGB32, 0, 0) + end + return arg +end + +--- Try to convert the argument into an lgi cairo surface. +-- This is usually needed for loading images by file name. +-- @param _surface The surface to load or nil +-- @param default The default value to return on error; when nil, then a surface +-- in an error state is returned. +-- @return The loaded surface, or the replacement default +-- @return An error message, or nil on success +function surface.load_uncached_silently(_surface, default) + local file + -- On nil, return some sane default + if not _surface then + return get_default(default) + end + -- lgi cairo surfaces don't get changed either + if cairo.Surface:is_type_of(_surface) then + return _surface + end + -- Strings are assumed to be file names and get loaded + if type(_surface) == "string" then + local err + file = _surface + _surface, err = capi.awesome.load_image(file) + if not _surface then + return get_default(default), err + end + end + -- Everything else gets forced into a surface + return cairo.Surface(_surface, true) +end + +--- Try to convert the argument into an lgi cairo surface. +-- This is usually needed for loading images by file name and uses a cache. +-- In contrast to `load()`, errors are returned to the caller. +-- @param _surface The surface to load or nil +-- @param default The default value to return on error; when nil, then a surface +-- in an error state is returned. +-- @return The loaded surface, or the replacement default, or nil if called with +-- nil. +-- @return An error message, or nil on success +function surface.load_silently(_surface, default) + if type(_surface) == "string" then + local cache = surface_cache[_surface] + if cache then + return cache + end + local result, err = surface.load_uncached_silently(_surface, default) + if not err then + -- Cache the file + surface_cache[_surface] = result + end + return result, err + end + return surface.load_uncached_silently(_surface, default) +end + +local function do_load_and_handle_errors(_surface, func) + if type(_surface) == 'nil' then + return get_default() + end + local result, err = func(_surface, false) + if result then + return result + end + gdebug.print_error(debug.traceback( + "Failed to load '" .. tostring(_surface) .. "': " .. tostring(err))) + return get_default() +end + +--- Try to convert the argument into an lgi cairo surface. +-- This is usually needed for loading images by file name. Errors are handled +-- via `gears.debug.print_error`. +-- @param _surface The surface to load or nil +-- @return The loaded surface, or nil +function surface.load_uncached(_surface) + return do_load_and_handle_errors(_surface, surface.load_uncached_silently) +end + +--- Try to convert the argument into an lgi cairo surface. +-- This is usually needed for loading images by file name. Errors are handled +-- via `gears.debug.print_error`. +-- @param _surface The surface to load or nil +-- @return The loaded surface, or nil +function surface.load(_surface) + return do_load_and_handle_errors(_surface, surface.load_silently) +end + +function surface.mt.__call(_, ...) + return surface.load(...) +end + +--- Get the size of a cairo surface +-- @param surf The surface you are interested in +-- @return The surface's width and height +function surface.get_size(surf) + local cr = cairo.Context(surf) + local x, y, w, h = cr:clip_extents() + return w - x, h - y +end + +--- Create a copy of a cairo surface. +-- The surfaces returned by `surface.load` are cached and must not be +-- modified to avoid unintended side-effects. This function allows to create +-- a copy of a cairo surface. This copy can then be freely modified. +-- The surface returned will be as compatible as possible to the input +-- surface. For example, it will likely be of the same surface type as the +-- input. The details are explained in the `create_similar` function on a cairo +-- surface. +-- @param s Source surface. +-- @return The surface's duplicate. +function surface.duplicate_surface(s) + s = surface.load(s) + + -- Figure out surface size (this does NOT work for unbounded recording surfaces) + local cr = cairo.Context(s) + local x, y, w, h = cr:clip_extents() + + -- Create a copy + local result = s:create_similar(s.content, w - x, h - y) + cr = cairo.Context(result) + cr:set_source_surface(s, 0, 0) + cr.operator = cairo.Operator.SOURCE + cr:paint() + return result +end + +--- Create a surface from a `gears.shape` +-- Any additional parameters will be passed to the shape function +-- @tparam number width The surface width +-- @tparam number height The surface height +-- @param shape A `gears.shape` compatible function +-- @param[opt=white] shape_color The shape color or pattern +-- @param[opt=transparent] bg_color The surface background color +-- @treturn cairo.surface the new surface +function surface.load_from_shape(width, height, shape, shape_color, bg_color, ...) + color = color or require("gears.color") + + local img = cairo.ImageSurface(cairo.Format.ARGB32, width, height) + local cr = cairo.Context(img) + + cr:set_source(color(bg_color or "#00000000")) + cr:paint() + + cr:set_source(color(shape_color or "#000000")) + + shape(cr, width, height, ...) + + cr:fill() + + return img +end + +--- Apply a shape to a client or a wibox. +-- +-- If the wibox or client size change, this function need to be called +-- again. +-- @param draw A wibox or a client +-- @param shape or gears.shape function or a custom function with a context, +-- width and height as parameter. +-- @param[opt] Any additional parameters will be passed to the shape function +function surface.apply_shape_bounding(draw, shape, ...) + local geo = draw:geometry() + + local img = cairo.ImageSurface(cairo.Format.A1, geo.width, geo.height) + local cr = cairo.Context(img) + + cr:set_operator(cairo.Operator.CLEAR) + cr:set_source_rgba(0,0,0,1) + cr:paint() + cr:set_operator(cairo.Operator.SOURCE) + cr:set_source_rgba(1,1,1,1) + + shape(cr, geo.width, geo.height, ...) + + cr:fill() + + draw.shape_bounding = img._native +end + +local function no_op() end + +local function run_in_hierarchy(self, cr, width, height) + local context = {dpi=96} + local h = hierarchy.new(context, self, width, height, no_op, no_op, {}) + h:draw(context, cr) + return h +end + +--- Create an SVG file with this widget content. +-- This is dynamic, so the SVG will be updated along with the widget content. +-- because of this, the painting may happen hover multiple event loop cycles. +-- @tparam widget widget A widget +-- @tparam string path The output file path +-- @tparam number width The surface width +-- @tparam number height The surface height +-- @return The cairo surface +-- @return The hierarchy +function surface.widget_to_svg(widget, path, width, height) + local img = cairo.SvgSurface.create(path, width, height) + local cr = cairo.Context(img) + + return img, run_in_hierarchy(widget, cr, width, height) +end + +--- Create a cairo surface with this widget content. +-- This is dynamic, so the SVG will be updated along with the widget content. +-- because of this, the painting may happen hover multiple event loop cycles. +-- @tparam widget widget A widget +-- @tparam number width The surface width +-- @tparam number height The surface height +-- @param[opt=cairo.Format.ARGB32] format The surface format +-- @return The cairo surface +-- @return The hierarchy +function surface.widget_to_surface(widget, width, height, format) + local img = cairo.ImageSurface(format or cairo.Format.ARGB32, width, height) + local cr = cairo.Context(img) + + return img, run_in_hierarchy(widget, cr, width, height) +end + +return setmetatable(surface, surface.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/timer.lua b/lib/gears/timer.lua new file mode 100644 index 0000000..110c39a --- /dev/null +++ b/lib/gears/timer.lua @@ -0,0 +1,187 @@ +--------------------------------------------------------------------------- +--- Timer objects and functions. +-- +-- @author Uli Schlachter +-- @copyright 2014 Uli Schlachter +-- @classmod gears.timer +--------------------------------------------------------------------------- + +local capi = { awesome = awesome } +local ipairs = ipairs +local pairs = pairs +local setmetatable = setmetatable +local table = table +local tonumber = tonumber +local traceback = debug.traceback +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local glib = require("lgi").GLib +local object = require("gears.object") +local protected_call = require("gears.protected_call") + +--- Timer objects. This type of object is useful when triggering events repeatedly. +-- The timer will emit the "timeout" signal every N seconds, N being the timeout +-- value. Note that a started timer will not be garbage collected. Call `:stop` +-- to enable garbage collection. +-- @tfield number timeout Interval in seconds to emit the timeout signal. +-- Can be any value, including floating point ones (e.g. 1.5 seconds). +-- @tfield boolean started Read-only boolean field indicating if the timer has been +-- started. +-- @table timer + +--- When the timer is started. +-- @signal .start + +--- When the timer is stopped. +-- @signal .stop + +--- When the timer had a timeout event. +-- @signal .timeout + +local timer = { mt = {} } + +--- Start the timer. +function timer:start() + if self.data.source_id ~= nil then + print(traceback("timer already started")) + return + end + self.data.source_id = glib.timeout_add(glib.PRIORITY_DEFAULT, self.data.timeout * 1000, function() + protected_call(self.emit_signal, self, "timeout") + return true + end) + self:emit_signal("start") +end + +--- Stop the timer. +function timer:stop() + if self.data.source_id == nil then + print(traceback("timer not started")) + return + end + glib.source_remove(self.data.source_id) + self.data.source_id = nil + self:emit_signal("stop") +end + +--- Restart the timer. +-- This is equivalent to stopping the timer if it is running and then starting +-- it. +function timer:again() + if self.data.source_id ~= nil then + self:stop() + end + self:start() +end + +--- The timer is started. +-- @property started +-- @param boolean + +--- The timer timeout value. +-- **Signal:** property::timeout +-- @property timeout +-- @param number + +local timer_instance_mt = { + __index = function(self, property) + if property == "timeout" then + return self.data.timeout + elseif property == "started" then + return self.data.source_id ~= nil + end + + return timer[property] + end, + + __newindex = function(self, property, value) + if property == "timeout" then + self.data.timeout = tonumber(value) + self:emit_signal("property::timeout") + end + end +} + +--- Create a new timer object. +-- @tparam table args Arguments. +-- @tparam number args.timeout Timeout in seconds (e.g. 1.5). +-- @treturn timer +-- @function gears.timer +timer.new = function(args) + local ret = object() + + ret.data = { timeout = 0 } + setmetatable(ret, timer_instance_mt) + + for k, v in pairs(args) do + ret[k] = v + end + + return ret +end + +--- Create a timeout for calling some callback function. +-- When the callback function returns true, it will be called again after the +-- same timeout. If false is returned, no more calls will be done. If the +-- callback function causes an error, no more calls are done. +-- @tparam number timeout Timeout in seconds (e.g. 1.5). +-- @tparam function callback Function to run. +-- @treturn timer The timer object that was set up. +-- @see timer.weak_start_new +-- @function gears.timer.start_new +function timer.start_new(timeout, callback) + local t = timer.new({ timeout = timeout }) + t:connect_signal("timeout", function() + local cont = protected_call(callback) + if not cont then + t:stop() + end + end) + t:start() + return t +end + +--- Create a timeout for calling some callback function. +-- This function is almost identical to `timer.start_new`. The only difference +-- is that this does not prevent the callback function from being garbage +-- collected. After the callback function was collected, the timer returned +-- will automatically be stopped. +-- @tparam number timeout Timeout in seconds (e.g. 1.5). +-- @tparam function callback Function to start. +-- @treturn timer The timer object that was set up. +-- @see timer.start_new +-- @function gears.timer.weak_start_new +function timer.weak_start_new(timeout, callback) + local indirection = setmetatable({}, { __mode = "v" }) + indirection.callback = callback + return timer.start_new(timeout, function() + local cb = indirection.callback + if cb then + return cb() + end + end) +end + +local delayed_calls = {} +capi.awesome.connect_signal("refresh", function() + for _, callback in ipairs(delayed_calls) do + protected_call(unpack(callback)) + end + delayed_calls = {} +end) + +--- Call the given function at the end of the current main loop iteration +-- @tparam function callback The function that should be called +-- @param ... Arguments to the callback function +-- @function gears.timer.delayed_call +function timer.delayed_call(callback, ...) + assert(type(callback) == "function", "callback must be a function, got: " .. type(callback)) + table.insert(delayed_calls, { callback, ... }) +end + +function timer.mt.__call(_, ...) + return timer.new(...) +end + +return setmetatable(timer, timer.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/wallpaper.lua b/lib/gears/wallpaper.lua new file mode 100644 index 0000000..70ecf48 --- /dev/null +++ b/lib/gears/wallpaper.lua @@ -0,0 +1,221 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2012 Uli Schlachter +-- @module gears.wallpaper +--------------------------------------------------------------------------- + +local cairo = require("lgi").cairo +local color = require("gears.color") +local surface = require("gears.surface") +local timer = require("gears.timer") +local root = root + +local wallpaper = { mt = {} } + +local function root_geometry() + local width, height = root.size() + return { x = 0, y = 0, width = width, height = height } +end + +-- Information about a pending wallpaper change, see prepare_context() +local pending_wallpaper = nil + +local function get_screen(s) + return s and screen[s] +end + +--- Prepare the needed state for setting a wallpaper. +-- This function returns a cairo context through which a wallpaper can be drawn. +-- The context is only valid for a short time and should not be saved in a +-- global variable. +-- @param s The screen to set the wallpaper on or nil for all screens +-- @return[1] The available geometry (table with entries width and height) +-- @return[1] A cairo context that the wallpaper should be drawn to +function wallpaper.prepare_context(s) + s = get_screen(s) + + local root_width, root_height = root.size() + local geom = s and s.geometry or root_geometry() + local source, target, cr + + if not pending_wallpaper then + -- Prepare a pending wallpaper + source = surface(root.wallpaper()) + target = source:create_similar(cairo.Content.COLOR, root_width, root_height) + + -- Set the wallpaper (delayed) + timer.delayed_call(function() + local paper = pending_wallpaper + pending_wallpaper = nil + wallpaper.set(paper.surface) + paper.surface:finish() + end) + elseif root_width > pending_wallpaper.width or root_height > pending_wallpaper.height then + -- The root window was resized while a wallpaper is pending + source = pending_wallpaper.surface + target = source:create_similar(cairo.Content.COLOR, root_width, root_height) + else + -- Draw to the already-pending wallpaper + source = nil + target = pending_wallpaper.surface + end + + cr = cairo.Context(target) + + if source then + -- Copy the old wallpaper to the new one + cr:save() + cr.operator = cairo.Operator.SOURCE + cr:set_source_surface(source, 0, 0) + cr:paint() + cr:restore() + end + + pending_wallpaper = { + surface = target, + width = root_width, + height = root_height + } + + -- Only draw to the selected area + cr:translate(geom.x, geom.y) + cr:rectangle(0, 0, geom.width, geom.height) + cr:clip() + + return geom, cr +end + +--- Set the current wallpaper. +-- @param pattern The wallpaper that should be set. This can be a cairo surface, +-- a description for gears.color or a cairo pattern. +-- @see gears.color +function wallpaper.set(pattern) + if cairo.Surface:is_type_of(pattern) then + pattern = cairo.Pattern.create_for_surface(pattern) + end + if type(pattern) == "string" or type(pattern) == "table" then + pattern = color(pattern) + end + if not cairo.Pattern:is_type_of(pattern) then + error("wallpaper.set() called with an invalid argument") + end + root.wallpaper(pattern._native) +end + +--- Set a centered wallpaper. +-- @param surf The wallpaper to center. Either a cairo surface or a file name. +-- @param s The screen whose wallpaper should be set. Can be nil, in which case +-- all screens are set. +-- @param background The background color that should be used. Gets handled via +-- gears.color. The default is black. +-- @see gears.color +function wallpaper.centered(surf, s, background) + local geom, cr = wallpaper.prepare_context(s) + surf = surface.load_uncached(surf) + background = color(background) + + -- Fill the area with the background + cr.operator = cairo.Operator.SOURCE + cr.source = background + cr:paint() + + -- Now center the surface + local w, h = surface.get_size(surf) + cr:translate((geom.width - w) / 2, (geom.height - h) / 2) + cr:rectangle(0, 0, w, h) + cr:clip() + cr:set_source_surface(surf, 0, 0) + cr:paint() + surf:finish() +end + +--- Set a tiled wallpaper. +-- @param surf The wallpaper to tile. Either a cairo surface or a file name. +-- @param s The screen whose wallpaper should be set. Can be nil, in which case +-- all screens are set. +-- @param offset This can be set to a table with entries x and y. +function wallpaper.tiled(surf, s, offset) + local _, cr = wallpaper.prepare_context(s) + + if offset then + cr:translate(offset.x, offset.y) + end + + surf = surface.load_uncached(surf) + local pattern = cairo.Pattern.create_for_surface(surf) + pattern.extend = cairo.Extend.REPEAT + cr.source = pattern + cr.operator = cairo.Operator.SOURCE + cr:paint() + surf:finish() +end + +--- Set a maximized wallpaper. +-- @param surf The wallpaper to set. Either a cairo surface or a file name. +-- @param s The screen whose wallpaper should be set. Can be nil, in which case +-- all screens are set. +-- @param ignore_aspect If this is true, the image's aspect ratio is ignored. +-- The default is to honor the aspect ratio. +-- @param offset This can be set to a table with entries x and y. +function wallpaper.maximized(surf, s, ignore_aspect, offset) + local geom, cr = wallpaper.prepare_context(s) + surf = surface.load_uncached(surf) + local w, h = surface.get_size(surf) + local aspect_w = geom.width / w + local aspect_h = geom.height / h + + if not ignore_aspect then + aspect_h = math.max(aspect_w, aspect_h) + aspect_w = math.max(aspect_w, aspect_h) + end + cr:scale(aspect_w, aspect_h) + + if offset then + cr:translate(offset.x, offset.y) + elseif not ignore_aspect then + local scaled_width = geom.width / aspect_w + local scaled_height = geom.height / aspect_h + cr:translate((scaled_width - w) / 2, (scaled_height - h) / 2) + end + + cr:set_source_surface(surf, 0, 0) + cr.operator = cairo.Operator.SOURCE + cr:paint() + surf:finish() +end + +--- Set a fitting wallpaper. +-- @param surf The wallpaper to set. Either a cairo surface or a file name. +-- @param s The screen whose wallpaper should be set. Can be nil, in which case +-- all screens are set. +-- @param background The background color that should be used. Gets handled via +-- gears.color. The default is black. +-- @see gears.color +function wallpaper.fit(surf, s, background) + local geom, cr = wallpaper.prepare_context(s) + surf = surface.load_uncached(surf) + background = color(background) + + -- Fill the area with the background + cr.operator = cairo.Operator.SOURCE + cr.source = background + cr:paint() + + -- Now fit the surface + local w, h = surface.get_size(surf) + local scale = geom.width / w + if h * scale > geom.height then + scale = geom.height / h + end + cr:translate((geom.width - (w * scale)) / 2, (geom.height - (h * scale)) / 2) + cr:rectangle(0, 0, w * scale, h * scale) + cr:clip() + cr:scale(scale, scale) + cr:set_source_surface(surf, 0, 0) + cr:paint() + surf:finish() +end + +return wallpaper + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/icon_theme.lua b/lib/menubar/icon_theme.lua new file mode 100644 index 0000000..f76252f --- /dev/null +++ b/lib/menubar/icon_theme.lua @@ -0,0 +1,251 @@ +--------------------------------------------------------------------------- +--- Class module for icon lookup for menubar +-- +-- @author Kazunobu Kuriyama +-- @copyright 2015 Kazunobu Kuriyama +-- @classmod menubar.icon_theme +--------------------------------------------------------------------------- + +-- This implementation is based on the specifications: +-- Icon Theme Specification 0.12 +-- http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-0.12.html + +local beautiful = require("beautiful") +local awful_util = require("awful.util") +local GLib = require("lgi").GLib +local index_theme = require("menubar.index_theme") + +local ipairs = ipairs +local setmetatable = setmetatable +local string = string +local table = table +local math = math + +local get_pragmatic_base_directories = function() + local dirs = {} + + local dir = GLib.build_filenamev({GLib.get_home_dir(), ".icons"}) + if awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + + dir = GLib.build_filenamev({GLib.get_user_data_dir(), "icons"}) + if awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + + for _, v in ipairs(GLib.get_system_data_dirs()) do + dir = GLib.build_filenamev({v, "icons"}) + if awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + end + + local need_usr_share_pixmaps = true + for _, v in ipairs(GLib.get_system_data_dirs()) do + dir = GLib.build_filenamev({v, "pixmaps"}) + if awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + if dir == "/usr/share/pixmaps" then + need_usr_share_pixmaps = false + end + end + + dir = "/usr/share/pixmaps" + if need_usr_share_pixmaps and awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + + return dirs +end + +local get_default_icon_theme_name = function() + local icon_theme_names = { "Adwaita", "gnome", "hicolor" } + for _, dir in ipairs(get_pragmatic_base_directories()) do + for _, icon_theme_name in ipairs(icon_theme_names) do + local filename = string.format("%s/%s/index.theme", dir, icon_theme_name) + if awful_util.file_readable(filename) then + return icon_theme_name + end + end + end + return nil +end + +local icon_theme = { mt = {} } + +local index_theme_cache = {} + +--- Class constructor of `icon_theme` +-- @tparam string icon_theme_name Internal name of icon theme +-- @tparam table base_directories Paths used for lookup +-- @treturn table An instance of the class `icon_theme` +icon_theme.new = function(icon_theme_name, base_directories) + icon_theme_name = icon_theme_name or beautiful.icon_theme or get_default_icon_theme_name() + base_directories = base_directories or get_pragmatic_base_directories() + + local self = {} + self.icon_theme_name = icon_theme_name + self.base_directories = base_directories + self.extensions = { "png", "svg", "xpm" } + + -- Instantiate index_theme (cached). + if not index_theme_cache[self.icon_theme_name] then + index_theme_cache[self.icon_theme_name] = {} + end + local cache_key = table.concat(self.base_directories, ':') + if not index_theme_cache[self.icon_theme_name][cache_key] then + index_theme_cache[self.icon_theme_name][cache_key] = index_theme( + self.icon_theme_name, + self.base_directories) + end + self.index_theme = index_theme_cache[self.icon_theme_name][cache_key] + + return setmetatable(self, { __index = icon_theme }) +end + +local directory_matches_size = function(self, subdirectory, icon_size) + local kind, size, min_size, max_size, threshold = self.index_theme:get_per_directory_keys(subdirectory) + + if kind == "Fixed" then + return icon_size == size + elseif kind == "Scalable" then + return icon_size >= min_size and icon_size <= max_size + elseif kind == "Threshold" then + return icon_size >= size - threshold and icon_size <= size + threshold + end + + return false +end + +local directory_size_distance = function(self, subdirectory, icon_size) + local kind, size, min_size, max_size, threshold = self.index_theme:get_per_directory_keys(subdirectory) + + if kind == "Fixed" then + return math.abs(icon_size - size) + elseif kind == "Scalable" then + if icon_size < min_size then + return min_size - icon_size + elseif icon_size > max_size then + return icon_size - max_size + end + return 0 + elseif kind == "Threshold" then + if icon_size < size - threshold then + return min_size - icon_size + elseif icon_size > size + threshold then + return icon_size - max_size + end + return 0 + end + + return 0xffffffff -- Any large number will do. +end + +local lookup_icon = function(self, icon_name, icon_size) + local checked_already = {} + for _, subdir in ipairs(self.index_theme:get_subdirectories()) do + for _, basedir in ipairs(self.base_directories) do + for _, ext in ipairs(self.extensions) do + if directory_matches_size(self, subdir, icon_size) then + local filename = string.format("%s/%s/%s/%s.%s", + basedir, self.icon_theme_name, subdir, + icon_name, ext) + if awful_util.file_readable(filename) then + return filename + else + checked_already[filename] = true + end + end + end + end + end + + local minimal_size = 0xffffffff -- Any large number will do. + local closest_filename = nil + for _, subdir in ipairs(self.index_theme:get_subdirectories()) do + local dist = directory_size_distance(self, subdir, icon_size) + if dist < minimal_size then + for _, basedir in ipairs(self.base_directories) do + for _, ext in ipairs(self.extensions) do + local filename = string.format("%s/%s/%s/%s.%s", + basedir, self.icon_theme_name, subdir, + icon_name, ext) + if not checked_already[filename] then + if awful_util.file_readable(filename) then + closest_filename = filename + minimal_size = dist + end + end + end + end + end + end + return closest_filename +end + +local find_icon_path_helper -- Gets called recursively. +find_icon_path_helper = function(self, icon_name, icon_size) + local filename = lookup_icon(self, icon_name, icon_size) + if filename then + return filename + end + + for _, parent in ipairs(self.index_theme:get_inherits()) do + local parent_icon_theme = icon_theme(parent, self.base_directories) + filename = find_icon_path_helper(parent_icon_theme, icon_name, icon_size) + if filename then + return filename + end + end + + return nil +end + +local lookup_fallback_icon = function(self, icon_name) + for _, dir in ipairs(self.base_directories) do + for _, ext in ipairs(self.extensions) do + local filename = string.format("%s/%s.%s", + dir, + icon_name, ext) + if awful_util.file_readable(filename) then + return filename + end + end + end + return nil +end + +--- Look up an image file based on a given icon name and/or a preferable size. +-- @tparam string icon_name Icon name to be looked up +-- @tparam number icon_size Prefereable icon size +-- @treturn string Absolute path to the icon file, or nil if not found +function icon_theme:find_icon_path(icon_name, icon_size) + icon_size = icon_size or 16 + if not icon_name or icon_name == "" then + return nil + end + + local filename = find_icon_path_helper(self, icon_name, icon_size) + if filename then + return filename + end + + if self.icon_theme_name ~= "hicolor" then + filename = find_icon_path_helper(icon_theme("hicolor", self.base_directories), icon_name, icon_size) + if filename then + return filename + end + end + + return lookup_fallback_icon(self, icon_name) +end + +icon_theme.mt.__call = function(_, ...) + return icon_theme.new(...) +end + +return setmetatable(icon_theme, icon_theme.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/index_theme.lua b/lib/menubar/index_theme.lua new file mode 100644 index 0000000..633964a --- /dev/null +++ b/lib/menubar/index_theme.lua @@ -0,0 +1,164 @@ +--------------------------------------------------------------------------- +--- Class module for parsing an index.theme file +-- +-- @author Kazunobu Kuriyama +-- @copyright 2015 Kazunobu Kuriyama +-- @classmod menubar.index_theme +--------------------------------------------------------------------------- + +-- This implementation is based on the specifications: +-- Icon Theme Specification 0.12 +-- http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-0.12.html + +local ipairs = ipairs +local setmetatable = setmetatable +local string = string +local table = table +local io = io + +-- index.theme groups +local ICON_THEME = "Icon Theme" +-- index.theme keys +local DIRECTORIES = "Directories" +local INHERITS = "Inherits" +-- per-directory subkeys +local TYPE = "Type" +local SIZE = "Size" +local MINSIZE = "MinSize" +local MAXSIZE = "MaxSize" +local THRESHOLD = "Threshold" + +local index_theme = { mt = {} } + +--- Class constructor of `index_theme` +-- @tparam table cls Metatable that will be used. Should always be `index_theme.mt`. +-- @tparam string icon_theme_name Internal name of icon theme +-- @tparam table base_directories Paths used for lookup +-- @treturn table An instance of the class `index_theme` +index_theme.new = function(cls, icon_theme_name, base_directories) + local self = {} + setmetatable(self, { __index = cls }) + + -- Initialize the fields + self.icon_theme_name = icon_theme_name + self.base_directory = nil + self[DIRECTORIES] = {} + self[INHERITS] = {} + self.per_directory_keys = {} + + -- base_directory + local basedir = nil + local handler = nil + for _, dir in ipairs(base_directories) do + basedir = dir .. "/" .. self.icon_theme_name + handler = io.open(basedir .. "/index.theme", "r") + if handler then + -- Use the index.theme which is found first. + break + end + end + if not handler then + return self + end + self.base_directory = basedir + + -- Parse index.theme. + while true do + local line = handler:read() + if not line then + break + end + + local group_header = "^%[(.+)%]$" + local group = line:match(group_header) + if group then + if group == ICON_THEME then + while true do + local item = handler:read() + if not item then + break + end + if item:match(group_header) then + handler:seek("cur", -string.len(item) - 1) + break + end + + local k, v = item:match("^(%w+)=(.*)$") + if k == DIRECTORIES or k == INHERITS then + string.gsub(v, "([^,]+),?", function(match) + table.insert(self[k], match) + end) + end + end + else + -- This must be a 'per-directory keys' group + local keys = {} + + while true do + local item = handler:read() + if not item then + break + end + if item:match(group_header) then + handler:seek("cur", -string.len(item) - 1) + break + end + + local k, v = item:match("^(%w+)=(%w+)$") + if k == SIZE or k == MINSIZE or k == MAXSIZE or k == THRESHOLD then + keys[k] = tonumber(v) + elseif k == TYPE then + keys[k] = v + end + end + + -- Size is a must. Other keys are optional. + if keys[SIZE] then + -- Set unset keys to the default values. + if not keys[TYPE] then keys[TYPE] = THRESHOLD end + if not keys[MINSIZE] then keys[MINSIZE] = keys[SIZE] end + if not keys[MAXSIZE] then keys[MAXSIZE] = keys[SIZE] end + if not keys[THRESHOLD] then keys[THRESHOLD] = 2 end + + self.per_directory_keys[group] = keys + end + end + end + end + + handler:close() + + return self +end + +--- Table of the values of the `Directories` key +-- @treturn table Values of the `Directories` key +index_theme.get_subdirectories = function(self) + return self[DIRECTORIES] +end + +--- Table of the values of the `Inherits` key +-- @treturn table Values of the `Inherits` key +index_theme.get_inherits = function(self) + return self[INHERITS] +end + +--- Query (part of) per-directory keys of a given subdirectory name. +-- @tparam table subdirectory Icon theme's subdirectory +-- @treturn[1] string Value of the `Type` key +-- @treturn[2] number Value of the `Size` key +-- @treturn[3] number VAlue of the `MinSize` key +-- @treturn[4] number Value of the `MaxSize` key +-- @treturn[5] number Value of the `Threshold` key +function index_theme:get_per_directory_keys(subdirectory) + local keys = self.per_directory_keys[subdirectory] + return keys[TYPE], keys[SIZE], keys[MINSIZE], keys[MAXSIZE], keys[THRESHOLD] +end + +index_theme.mt.__call = function(cls, icon_theme_name, base_directories) + return index_theme.new(cls, icon_theme_name, base_directories) +end + +return setmetatable(index_theme, index_theme.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/init.lua b/lib/menubar/init.lua new file mode 100644 index 0000000..10ad65c --- /dev/null +++ b/lib/menubar/init.lua @@ -0,0 +1,480 @@ +--------------------------------------------------------------------------- +--- Menubar module, which aims to provide a freedesktop menu alternative +-- +-- List of menubar keybindings: +-- --- +-- +-- * "Left" | "C-j" select an item on the left +-- * "Right" | "C-k" select an item on the right +-- * "Backspace" exit the current category if we are in any +-- * "Escape" exit the current directory or exit menubar +-- * "Home" select the first item +-- * "End" select the last +-- * "Return" execute the entry +-- * "C-Return" execute the command with awful.spawn +-- * "C-M-Return" execute the command in a terminal +-- +-- @author Alexander Yakushev <yakushev.alex@gmail.com> +-- @copyright 2011-2012 Alexander Yakushev +-- @module menubar +--------------------------------------------------------------------------- + +-- Grab environment we need +local capi = { + client = client, + mouse = mouse, + screen = screen +} +local awful = require("awful") +local common = require("awful.widget.common") +local theme = require("beautiful") +local wibox = require("wibox") + +local function get_screen(s) + return s and capi.screen[s] +end + +-- menubar +local menubar = { mt = {}, menu_entries = {} } +menubar.menu_gen = require("menubar.menu_gen") +menubar.utils = require("menubar.utils") +local compute_text_width = menubar.utils.compute_text_width + +-- Options section + +--- When true the .desktop files will be reparsed only when the +-- extension is initialized. Use this if menubar takes much time to +-- open. +-- @tfield[opt=true] boolean cache_entries +menubar.cache_entries = true + +--- When true the categories will be shown alongside application +-- entries. +-- @tfield[opt=true] boolean show_categories +menubar.show_categories = true + +--- Specifies the geometry of the menubar. This is a table with the keys +-- x, y, width and height. Missing values are replaced via the screen's +-- geometry. However, missing height is replaced by the font size. +-- @table geometry +-- @tfield number geometry.x A forced horizontal position +-- @tfield number geometry.y A forced vertical position +-- @tfield number geometry.width A forced width +-- @tfield number geometry.height A forced height +menubar.geometry = { width = nil, + height = nil, + x = nil, + y = nil } + +--- Width of blank space left in the right side. +-- @tfield number right_margin +menubar.right_margin = theme.xresources.apply_dpi(8) + +--- Label used for "Next page", default "▶▶". +-- @tfield[opt="▶▶"] string right_label +menubar.right_label = "▶▶" + +--- Label used for "Previous page", default "◀◀". +-- @tfield[opt="◀◀"] string left_label +menubar.left_label = "◀◀" + +-- awful.widget.common.list_update adds three times a margin of dpi(4) +-- for each item: +-- @tfield number list_interspace +local list_interspace = theme.xresources.apply_dpi(4) * 3 + +--- Allows user to specify custom parameters for prompt.run function +-- (like colors). +-- @see awful.prompt +menubar.prompt_args = {} + +-- Private section +local current_item = 1 +local previous_item = nil +local current_category = nil +local shownitems = nil +local instance = { prompt = nil, + widget = nil, + wibox = nil } + +local common_args = { w = wibox.layout.fixed.horizontal(), + data = setmetatable({}, { __mode = 'kv' }) } + +--- Wrap the text with the color span tag. +-- @param s The text. +-- @param c The desired text color. +-- @return the text wrapped in a span tag. +local function colortext(s, c) + return "" .. s .. "" +end + +--- Get how the menu item should be displayed. +-- @param o The menu item. +-- @return item name, item background color, background image, item icon. +local function label(o) + if o.focused then + return colortext(o.name, (theme.menu_fg_focus or theme.fg_focus)), (theme.menu_bg_focus or theme.bg_focus), nil, o.icon + else + return o.name, (theme.menu_bg_normal or theme.bg_normal), nil, o.icon + end +end + +local function load_count_table() + local count_file_name = awful.util.getdir("cache") .. "/menu_count_file" + + local count_file = io.open (count_file_name, "r") + local count_table = {} + + -- read weight file + if count_file then + io.input (count_file) + for line in io.lines() do + local name, count = string.match(line, "([^;]+);([^;]+)") + if name ~= nil and count ~= nil then + count_table[name] = count + end + end + end + + return count_table +end + +local function write_count_table(count_table) + local count_file_name = awful.util.getdir("cache") .. "/menu_count_file" + + local count_file = io.open (count_file_name, "w") + + if count_file then + io.output (count_file) + + for name, count in pairs(count_table) do + local str = string.format("%s;%d\n", name, count) + io.write(str) + end + io.flush() + end +end + +--- Perform an action for the given menu item. +-- @param o The menu item. +-- @return if the function processed the callback, new awful.prompt command, new awful.prompt prompt text. +local function perform_action(o) + if not o then return end + if o.key then + current_category = o.key + local new_prompt = shownitems[current_item].name .. ": " + previous_item = current_item + current_item = 1 + return true, "", new_prompt + elseif shownitems[current_item].cmdline then + awful.spawn(shownitems[current_item].cmdline) + + -- load count_table from cache file + local count_table = load_count_table() + + -- increase count + local curname = shownitems[current_item].name + if count_table[curname] ~= nil then + count_table[curname] = count_table[curname] + 1 + else + count_table[curname] = 1 + end + + -- write updated count table to cache file + write_count_table(count_table) + + -- Let awful.prompt execute dummy exec_callback and + -- done_callback to stop the keygrabber properly. + return false + end +end + +-- Cut item list to return only current page. +-- @tparam table all_items All items list. +-- @tparam str query Search query. +-- @tparam number|screen scr Screen +-- @return table List of items for current page. +local function get_current_page(all_items, query, scr) + scr = get_screen(scr) + if not instance.prompt.width then + instance.prompt.width = compute_text_width(instance.prompt.prompt, scr) + end + if not menubar.left_label_width then + menubar.left_label_width = compute_text_width(menubar.left_label, scr) + end + if not menubar.right_label_width then + menubar.right_label_width = compute_text_width(menubar.right_label, scr) + end + local available_space = instance.geometry.width - menubar.right_margin - + menubar.right_label_width - menubar.left_label_width - + compute_text_width(query, scr) - instance.prompt.width + + local width_sum = 0 + local current_page = {} + for i, item in ipairs(all_items) do + item.width = item.width or + compute_text_width(item.name, scr) + + (item.icon and instance.geometry.height or 0) + list_interspace + if width_sum + item.width > available_space then + if current_item < i then + table.insert(current_page, { name = menubar.right_label, icon = nil }) + break + end + current_page = { { name = menubar.left_label, icon = nil }, item, } + width_sum = item.width + else + table.insert(current_page, item) + width_sum = width_sum + item.width + end + end + return current_page +end + +--- Update the menubar according to the command entered by user. +-- @tparam str query Search query. +-- @tparam number|screen scr Screen +local function menulist_update(query, scr) + query = query or "" + shownitems = {} + local pattern = awful.util.query_to_pattern(query) + + -- All entries are added to a list that will be sorted + -- according to the priority (first) and weight (second) of its + -- entries. + -- If categories are used in the menu, we add the entries matching + -- the current query with high priority as to ensure they are + -- displayed first. Afterwards the non-category entries are added. + -- All entries are weighted according to the number of times they + -- have been executed previously (stored in count_table). + + local count_table = load_count_table() + local command_list = {} + + local PRIO_NONE = 0 + local PRIO_CATEGORY_MATCH = 2 + + -- Add the categories + if menubar.show_categories then + for _, v in pairs(menubar.menu_gen.all_categories) do + v.focused = false + if not current_category and v.use then + + -- check if current query matches a category + if string.match(v.name, pattern) then + + v.weight = 0 + v.prio = PRIO_CATEGORY_MATCH + + -- get use count from count_table if present + -- and use it as weight + if string.len(pattern) > 0 and count_table[v.name] ~= nil then + v.weight = tonumber(count_table[v.name]) + end + + -- check for prefix match + if string.match(v.name, "^" .. pattern) then + -- increase default priority + v.prio = PRIO_CATEGORY_MATCH + 1 + else + v.prio = PRIO_CATEGORY_MATCH + end + + table.insert (command_list, v) + end + end + end + end + + -- Add the applications according to their name and cmdline + for _, v in ipairs(menubar.menu_entries) do + v.focused = false + if not current_category or v.category == current_category then + + -- check if the query matches either the name or the commandline + -- of some entry + if string.match(v.name, pattern) + or string.match(v.cmdline, pattern) then + + v.weight = 0 + v.prio = PRIO_NONE + + -- get use count from count_table if present + -- and use it as weight + if string.len(pattern) > 0 and count_table[v.name] ~= nil then + v.weight = tonumber(count_table[v.name]) + end + + -- check for prefix match + if string.match(v.name, "^" .. pattern) + or string.match(v.cmdline, "^" .. pattern) then + -- increase default priority + v.prio = PRIO_NONE + 1 + else + v.prio = PRIO_NONE + end + + table.insert (command_list, v) + end + end + end + + local function compare_counts(a, b) + if a.prio == b.prio then + return a.weight > b.weight + end + return a.prio > b.prio + end + + -- sort command_list by weight (highest first) + table.sort(command_list, compare_counts) + -- copy into showitems + shownitems = command_list + + if #shownitems > 0 then + -- Insert a run item value as the last choice + table.insert(shownitems, { name = "Exec: " .. query, cmdline = query, icon = nil }) + + if current_item > #shownitems then + current_item = #shownitems + end + shownitems[current_item].focused = true + else + table.insert(shownitems, { name = "", cmdline = query, icon = nil }) + end + + common.list_update(common_args.w, nil, label, + common_args.data, + get_current_page(shownitems, query, scr)) +end + +--- Create the menubar wibox and widgets. +-- @tparam[opt] screen scr Screen. +local function initialize(scr) + instance.wibox = wibox({}) + instance.widget = menubar.get(scr) + instance.wibox.ontop = true + instance.prompt = awful.widget.prompt() + local layout = wibox.layout.fixed.horizontal() + layout:add(instance.prompt) + layout:add(instance.widget) + instance.wibox:set_widget(layout) +end + +--- Refresh menubar's cache by reloading .desktop files. +-- @tparam[opt] screen scr Screen. +function menubar.refresh(scr) + menubar.menu_gen.generate(function(entries) + menubar.menu_entries = entries + menulist_update(nil, scr) + end) +end + +--- Awful.prompt keypressed callback to be used when the user presses a key. +-- @param mod Table of key combination modifiers (Control, Shift). +-- @param key The key that was pressed. +-- @param comm The current command in the prompt. +-- @return if the function processed the callback, new awful.prompt command, new awful.prompt prompt text. +local function prompt_keypressed_callback(mod, key, comm) + if key == "Left" or (mod.Control and key == "j") then + current_item = math.max(current_item - 1, 1) + return true + elseif key == "Right" or (mod.Control and key == "k") then + current_item = current_item + 1 + return true + elseif key == "BackSpace" then + if comm == "" and current_category then + current_category = nil + current_item = previous_item + return true, nil, "Run: " + end + elseif key == "Escape" then + if current_category then + current_category = nil + current_item = previous_item + return true, nil, "Run: " + end + elseif key == "Home" then + current_item = 1 + return true + elseif key == "End" then + current_item = #shownitems + return true + elseif key == "Return" or key == "KP_Enter" then + if mod.Control then + current_item = #shownitems + if mod.Mod1 then + -- add a terminal to the cmdline + shownitems[current_item].cmdline = menubar.utils.terminal + .. " -e " .. shownitems[current_item].cmdline + end + end + return perform_action(shownitems[current_item]) + end + return false +end + +--- Show the menubar on the given screen. +-- @param scr Screen. +function menubar.show(scr) + if not instance.wibox then + initialize(scr) + elseif instance.wibox.visible then -- Menu already shown, exit + return + elseif not menubar.cache_entries then + menubar.refresh(scr) + end + + -- Set position and size + scr = scr or awful.screen.focused() or 1 + scr = get_screen(scr) + local scrgeom = scr.workarea + local geometry = menubar.geometry + instance.geometry = {x = geometry.x or scrgeom.x, + y = geometry.y or scrgeom.y, + height = geometry.height or awful.util.round(theme.get_font_height() * 1.5), + width = geometry.width or scrgeom.width} + instance.wibox:geometry(instance.geometry) + + current_item = 1 + current_category = nil + menulist_update(nil, scr) + + local prompt_args = menubar.prompt_args or {} + + awful.prompt.run(setmetatable({ + prompt = "Run: ", + textbox = instance.prompt.widget, + completion_callback = awful.completion.shell, + history_path = awful.util.get_cache_dir() .. "/history_menu", + done_callback = menubar.hide, + changed_callback = function(query) menulist_update(query, scr) end, + keypressed_callback = prompt_keypressed_callback + }, {__index=prompt_args})) + + instance.wibox.visible = true +end + +--- Hide the menubar. +function menubar.hide() + instance.wibox.visible = false +end + +--- Get a menubar wibox. +-- @tparam[opt] screen scr Screen. +-- @return menubar wibox. +function menubar.get(scr) + menubar.refresh(scr) + -- Add to each category the name of its key in all_categories + for k, v in pairs(menubar.menu_gen.all_categories) do + v.key = k + end + return common_args.w +end + +function menubar.mt.__call(_, ...) + return menubar.get(...) +end + +return setmetatable(menubar, menubar.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/menu_gen.lua b/lib/menubar/menu_gen.lua new file mode 100644 index 0000000..ed2aa14 --- /dev/null +++ b/lib/menubar/menu_gen.lua @@ -0,0 +1,141 @@ +--------------------------------------------------------------------------- +--- Menu generation module for menubar +-- +-- @author Antonio Terceiro +-- @copyright 2009, 2011-2012 Antonio Terceiro, Alexander Yakushev +-- @module menubar.menu_gen +--------------------------------------------------------------------------- + +-- Grab environment +local utils = require("menubar.utils") +local icon_theme = require("menubar.icon_theme") +local pairs = pairs +local ipairs = ipairs +local string = string +local table = table + +local menu_gen = {} + +-- Options section + +local data_dir = os.getenv("XDG_DATA_HOME") +if not data_dir then + data_dir = os.getenv("HOME") .. '/.local/share/' +end + +--- Specifies all directories where menubar should look for .desktop +-- files. The search is recursive. +menu_gen.all_menu_dirs = { data_dir .. 'applications/', '/usr/share/applications/', '/usr/local/share/applications/' } + +--- Specify the mapping of .desktop Categories section to the +-- categories in the menubar. If "use" flag is set to false then any of +-- the applications that fall only to this category will not be shown. +menu_gen.all_categories = { + multimedia = { app_type = "AudioVideo", name = "Multimedia", + icon_name = "applications-multimedia", use = true }, + development = { app_type = "Development", name = "Development", + icon_name = "applications-development", use = true }, + education = { app_type = "Education", name = "Education", + icon_name = "applications-science", use = true }, + games = { app_type = "Game", name = "Games", + icon_name = "applications-games", use = true }, + graphics = { app_type = "Graphics", name = "Graphics", + icon_name = "applications-graphics", use = true }, + office = { app_type = "Office", name = "Office", + icon_name = "applications-office", use = true }, + internet = { app_type = "Network", name = "Internet", + icon_name = "applications-internet", use = true }, + settings = { app_type = "Settings", name = "Settings", + icon_name = "applications-utilities", use = true }, + tools = { app_type = "System", name = "System Tools", + icon_name = "applications-system", use = true }, + utility = { app_type = "Utility", name = "Accessories", + icon_name = "applications-accessories", use = true } +} + +--- Find icons for category entries. +function menu_gen.lookup_category_icons() + for _, v in pairs(menu_gen.all_categories) do + v.icon = icon_theme():find_icon_path(v.icon_name) + end +end + +--- Get category key name and whether it is used by its app_type. +-- @param app_type Application category as written in .desktop file. +-- @return category key name in all_categories, whether the category is used +local function get_category_name_and_usage_by_type(app_type) + for k, v in pairs(menu_gen.all_categories) do + if app_type == v.app_type then + return k, v.use + end + end +end + +--- Remove CR\LF newline from the end of the string. +-- @param s string to trim +local function trim(s) + if not s then return end + if string.byte(s, #s) == 13 then + return string.sub(s, 1, #s - 1) + end + return s +end + +--- Generate an array of all visible menu entries. +-- @tparam function callback Will be fired when all menu entries were parsed +-- with the resulting list of menu entries as argument. +-- @tparam table callback.entries All menu entries. +function menu_gen.generate(callback) + -- Update icons for category entries + menu_gen.lookup_category_icons() + + local result = {} + local unique_entries = {} + local dirs_parsed = 0 + + for _, dir in ipairs(menu_gen.all_menu_dirs) do + utils.parse_dir(dir, function(entries) + entries = entries or {} + for _, entry in ipairs(entries) do + -- Check whether to include program in the menu + if entry.show and entry.Name and entry.cmdline then + local unique_key = entry.Name .. '\0' .. entry.cmdline + if not unique_entries[unique_key] then + local target_category = nil + -- Check if the program falls into at least one of the + -- usable categories. Set target_category to be the id + -- of the first category it finds. + if entry.categories then + for _, category in pairs(entry.categories) do + local cat_key, cat_use = + get_category_name_and_usage_by_type(category) + if cat_key and cat_use then + target_category = cat_key + break + end + end + end + if target_category then + local name = trim(entry.Name) or "" + local cmdline = trim(entry.cmdline) or "" + local icon = entry.icon_path or nil + table.insert(result, { name = name, + cmdline = cmdline, + icon = icon, + category = target_category }) + unique_entries[unique_key] = true + end + end + end + end + dirs_parsed = dirs_parsed + 1 + if dirs_parsed == #menu_gen.all_menu_dirs then + callback(result) + end + end) + end +end + +return menu_gen + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/utils.lua b/lib/menubar/utils.lua new file mode 100644 index 0000000..6f80e86 --- /dev/null +++ b/lib/menubar/utils.lua @@ -0,0 +1,316 @@ +--------------------------------------------------------------------------- +--- Utility module for menubar +-- +-- @author Antonio Terceiro +-- @copyright 2009, 2011-2012 Antonio Terceiro, Alexander Yakushev +-- @module menubar.utils +--------------------------------------------------------------------------- + +-- Grab environment +local io = io +local table = table +local ipairs = ipairs +local string = string +local screen = screen +local awful_util = require("awful.util") +local theme = require("beautiful") +local lgi = require("lgi") +local gio = lgi.Gio +local glib = lgi.GLib +local wibox = require("wibox") +local debug = require("gears.debug") +local protected_call = require("gears.protected_call") + +local utils = {} + +-- NOTE: This icons/desktop files module was written according to the +-- following freedesktop.org specifications: +-- Icons: http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-0.11.html +-- Desktop files: http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html + +-- Options section + +--- Terminal which applications that need terminal would open in. +utils.terminal = 'xterm' + +--- The default icon for applications that don't provide any icon in +-- their .desktop files. +local default_icon = nil + +--- Name of the WM for the OnlyShownIn entry in the .desktop file. +utils.wm_name = "awesome" + +-- Private section + +local all_icon_sizes = { + '128x128' , + '96x96', + '72x72', + '64x64', + '48x48', + '36x36', + '32x32', + '24x24', + '22x22', + '16x16' +} + +--- List of supported icon formats. +local icon_formats = { "png", "xpm", "svg" } + +--- Check whether the icon format is supported. +-- @param icon_file Filename of the icon. +-- @return true if format is supported, false otherwise. +local function is_format_supported(icon_file) + for _, f in ipairs(icon_formats) do + if icon_file:match('%.' .. f) then + return true + end + end + return false +end + +local icon_lookup_path = nil +--- Get a list of icon lookup paths. +-- @treturn table A list of directories, without trailing slash. +local function get_icon_lookup_path() + if not icon_lookup_path then + local add_if_readable = function(t, path) + if awful_util.dir_readable(path) then + table.insert(t, path) + end + end + icon_lookup_path = {} + local icon_theme_paths = {} + local icon_theme = theme.icon_theme + local paths = glib.get_system_data_dirs() + table.insert(paths, 1, glib.get_user_data_dir()) + table.insert(paths, 1, glib.build_filenamev({glib.get_home_dir(), + '.icons'})) + for _,dir in ipairs(paths) do + local icons_dir = glib.build_filenamev({dir, 'icons'}) + if awful_util.dir_readable(icons_dir) then + if icon_theme then + add_if_readable(icon_theme_paths, + glib.build_filenamev({icons_dir, + icon_theme})) + end + -- Fallback theme. + add_if_readable(icon_theme_paths, + glib.build_filenamev({icons_dir, 'hicolor'})) + end + end + for _, icon_theme_directory in ipairs(icon_theme_paths) do + for _, size in ipairs(all_icon_sizes) do + add_if_readable(icon_lookup_path, + glib.build_filenamev({icon_theme_directory, + size, 'apps'})) + end + end + for _,dir in ipairs(paths)do + -- lowest priority fallbacks + add_if_readable(icon_lookup_path, + glib.build_filenamev({dir, 'pixmaps'})) + add_if_readable(icon_lookup_path, + glib.build_filenamev({dir, 'icons'})) + end + end + return icon_lookup_path +end + +--- Lookup an icon in different folders of the filesystem. +-- @tparam string icon_file Short or full name of the icon. +-- @treturn string|boolean Full name of the icon, or false on failure. +function utils.lookup_icon_uncached(icon_file) + if not icon_file or icon_file == "" then + return false + end + + if icon_file:sub(1, 1) == '/' and is_format_supported(icon_file) then + -- If the path to the icon is absolute and its format is + -- supported, do not perform a lookup. + return awful_util.file_readable(icon_file) and icon_file or nil + else + for _, directory in ipairs(get_icon_lookup_path()) do + if is_format_supported(icon_file) and + awful_util.file_readable(directory .. "/" .. icon_file) then + return directory .. "/" .. icon_file + else + -- Icon is probably specified without path and format, + -- like 'firefox'. Try to add supported extensions to + -- it and see if such file exists. + for _, format in ipairs(icon_formats) do + local possible_file = directory .. "/" .. icon_file .. "." .. format + if awful_util.file_readable(possible_file) then + return possible_file + end + end + end + end + return false + end +end + +local lookup_icon_cache = {} +--- Lookup an icon in different folders of the filesystem (cached). +-- @param icon Short or full name of the icon. +-- @return full name of the icon. +function utils.lookup_icon(icon) + if not lookup_icon_cache[icon] and lookup_icon_cache[icon] ~= false then + lookup_icon_cache[icon] = utils.lookup_icon_uncached(icon) + end + return lookup_icon_cache[icon] or default_icon +end + +--- Parse a .desktop file. +-- @param file The .desktop file. +-- @return A table with file entries. +function utils.parse_desktop_file(file) + local program = { show = true, file = file } + local desktop_entry = false + + -- Parse the .desktop file. + -- We are interested in [Desktop Entry] group only. + for line in io.lines(file) do + if line:find("^%s*#") then + -- Skip comments. + (function() end)() -- I haven't found a nice way to silence luacheck here + elseif not desktop_entry and line == "[Desktop Entry]" then + desktop_entry = true + else + if line:sub(1, 1) == "[" and line:sub(-1) == "]" then + -- A declaration of new group - stop parsing + break + end + + -- Grab the values + for key, value in line:gmatch("(%w+)%s*=%s*(.+)") do + program[key] = value + end + end + end + + -- In case [Desktop Entry] was not found + if not desktop_entry then return nil end + + -- In case the (required) 'Name' entry was not found + if not program.Name or program.Name == '' then return nil end + + -- Don't show program if NoDisplay attribute is false + if program.NoDisplay and string.lower(program.NoDisplay) == "true" then + program.show = false + end + + -- Only show the program if there is no OnlyShowIn attribute + -- or if it's equal to utils.wm_name + if program.OnlyShowIn ~= nil and not program.OnlyShowIn:match(utils.wm_name) then + program.show = false + end + + -- Look up for a icon. + if program.Icon then + program.icon_path = utils.lookup_icon(program.Icon) + end + + -- Split categories into a table. Categories are written in one + -- line separated by semicolon. + if program.Categories then + program.categories = {} + for category in program.Categories:gmatch('[^;]+') do + table.insert(program.categories, category) + end + end + + if program.Exec then + -- Substitute Exec special codes as specified in + -- http://standards.freedesktop.org/desktop-entry-spec/1.1/ar01s06.html + if program.Name == nil then + program.Name = '['.. file:match("([^/]+)%.desktop$") ..']' + end + local cmdline = program.Exec:gsub('%%c', program.Name) + cmdline = cmdline:gsub('%%[fuFU]', '') + cmdline = cmdline:gsub('%%k', program.file) + if program.icon_path then + cmdline = cmdline:gsub('%%i', '--icon ' .. program.icon_path) + else + cmdline = cmdline:gsub('%%i', '') + end + if program.Terminal == "true" then + cmdline = utils.terminal .. ' -e ' .. cmdline + end + program.cmdline = cmdline + end + + return program +end + +--- Parse a directory with .desktop files recursively. +-- @tparam string dir_path The directory path. +-- @tparam function callback Will be fired when all the files were parsed +-- with the resulting list of menu entries as argument. +-- @tparam table callback.programs Paths of found .desktop files. +function utils.parse_dir(dir_path, callback) + + local function parser(dir, programs) + local f = gio.File.new_for_path(dir) + -- Except for "NONE" there is also NOFOLLOW_SYMLINKS + local query = gio.FILE_ATTRIBUTE_STANDARD_NAME .. "," .. gio.FILE_ATTRIBUTE_STANDARD_TYPE + local enum, err = f:async_enumerate_children(query, gio.FileQueryInfoFlags.NONE) + if not enum then + debug.print_error(err) + return + end + local files_per_call = 100 -- Actual value is not that important + while true do + local list, enum_err = enum:async_next_files(files_per_call) + if enum_err then + debug.print_error(enum_err) + return + end + for _, info in ipairs(list) do + local file_type = info:get_file_type() + local file_path = enum:get_child(info):get_path() + if file_type == 'REGULAR' then + local program = utils.parse_desktop_file(file_path) + if program then + table.insert(programs, program) + end + elseif file_type == 'DIRECTORY' then + parser(file_path, programs) + end + end + if #list == 0 then + break + end + end + enum:async_close() + end + + gio.Async.start(function() + local result = {} + parser(dir_path, result) + protected_call.call(callback, result) + end)() +end + +--- Compute textbox width. +-- @tparam wibox.widget.textbox textbox Textbox instance. +-- @tparam number|screen s Screen +-- @treturn int Text width. +function utils.compute_textbox_width(textbox, s) + s = screen[s or mouse.screen] + local w, _ = textbox:get_preferred_size(s) + return w +end + +--- Compute text width. +-- @tparam str text Text. +-- @tparam number|screen s Screen +-- @treturn int Text width. +function utils.compute_text_width(text, s) + return utils.compute_textbox_width(wibox.widget.textbox(awful_util.escape(text)), s) +end + +return utils + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/naughty.lua b/lib/naughty.lua new file mode 100644 index 0000000..89eac7c --- /dev/null +++ b/lib/naughty.lua @@ -0,0 +1,7 @@ +-- Work-around for broken systems which are updated by overwriting the awesome +-- installation. This would not remove naughty.lua from older awesome versions +-- and thus breakage follows. +-- The work-around is to use a pointless naughty.lua file. +return require("naughty.init") + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua new file mode 100644 index 0000000..764afe2 --- /dev/null +++ b/lib/naughty/core.lua @@ -0,0 +1,688 @@ +---------------------------------------------------------------------------- +--- Notification library +-- +-- @author koniu <gkusnierz@gmail.com> +-- @copyright 2008 koniu +-- @module naughty +---------------------------------------------------------------------------- + +-- Package environment +local pairs = pairs +local table = table +local type = type +local string = string +local pcall = pcall +local capi = { screen = screen, + awesome = awesome } +local timer = require("gears.timer") +local button = require("awful.button") +local screen = require("awful.screen") +local util = require("awful.util") +local bt = require("beautiful") +local wibox = require("wibox") +local surface = require("gears.surface") +local cairo = require("lgi").cairo +local dpi = require("beautiful").xresources.apply_dpi + +local function get_screen(s) + return s and capi.screen[s] +end + +local naughty = {} + +--[[-- +Naughty configuration - a table containing common popup settings. + +@table naughty.config +@tfield[opt=apply_dpi(4)] int padding Space between popups and edge of the + workarea. +@tfield[opt=apply_dpi(1)] int spacing Spacing between popups. +@tfield[opt={"/usr/share/pixmaps/"}] table icon_dirs List of directories + that will be checked by `getIcon()`. +@tfield[opt={ "png", "gif" }] table icon_formats List of formats that will be + checked by `getIcon()`. +@tfield[opt] function notify_callback Callback used to modify or reject +notifications, e.g. + naughty.config.notify_callback = function(args) + args.text = 'prefix: ' .. args.text + return args + end + +@tfield table presets Notification presets. See `config.presets`. + +@tfield table defaults Default values for the params to `notify()`. These can + optionally be overridden by specifying a preset. See `config.defaults`. + +--]] +-- +naughty.config = { + padding = dpi(4), + spacing = dpi(1), + icon_dirs = { "/usr/share/pixmaps/", }, + icon_formats = { "png", "gif" }, + notify_callback = nil, +} + +--- Notification presets for `naughty.notify`. +-- This holds presets for different purposes. A preset is a table of any +-- parameters for `notify()`, overriding the default values +-- (`naughty.config.defaults`). +-- +-- You have to pass a reference of a preset in your `notify()` as the `preset` +-- argument. +-- +-- The presets `"low"`, `"normal"` and `"critical"` are used for notifications +-- over DBUS. +-- +-- @table config.presets +-- @tfield table low The preset for notifications with low urgency level. +-- @tfield[opt=5] int low.timeout +-- @tfield[opt=empty] table normal The default preset for every notification without a +-- preset that will also be used for normal urgency level. +-- @tfield table critical The preset for notifications with a critical urgency +-- level. +-- @tfield[opt="#ff0000"] string critical.bg +-- @tfield[opt="#ffffff"] string critical.fg +-- @tfield[opt=0] string critical.timeout +naughty.config.presets = { + low = { + timeout = 5 + }, + normal = {}, + critical = { + bg = "#ff0000", + fg = "#ffffff", + timeout = 0, + } +} + +--- Defaults for `naughty.notify`. +-- +-- @table config.defaults +-- @tfield[opt=5] int timeout +-- @tfield[opt=""] string text +-- @tfield[opt] int screen Defaults to `awful.screen.focused`. +-- @tfield[opt=true] boolean ontop +-- @tfield[opt=apply_dpi(5)] int margin +-- @tfield[opt=apply_dpi(1)] int border_width +-- @tfield[opt="top_right"] string position +naughty.config.defaults = { + timeout = 5, + text = "", + screen = nil, + ontop = true, + margin = dpi(5), + border_width = dpi(1), + position = "top_right" +} + +naughty.notificationClosedReason = { + silent = -1, + expired = 1, + dismissedByUser = 2, + dismissedByCommand = 3, + undefined = 4 +} + +-- Counter for the notifications +-- Required for later access via DBUS +local counter = 1 + +-- True if notifying is suspended +local suspended = false + +--- Index of notifications per screen and position. +-- See config table for valid 'position' values. +-- Each element is a table consisting of: +-- +-- @field box Wibox object containing the popup +-- @field height Popup height +-- @field width Popup width +-- @field die Function to be executed on timeout +-- @field id Unique notification id based on a counter +-- @table notifications +naughty.notifications = { suspended = { } } +screen.connect_for_each_screen(function(s) + naughty.notifications[s] = { + top_left = {}, + top_middle = {}, + top_right = {}, + bottom_left = {}, + bottom_middle = {}, + bottom_right = {}, + } +end) + +capi.screen.connect_signal("removed", function(scr) + -- Destroy all notifications on this screen + for _, list in pairs(naughty.notifications[scr]) do + while #list > 0 do + naughty.destroy(list[1]) + end + end + naughty.notifications[scr] = nil +end) + +--- Notification state +function naughty.is_suspended() + return suspended +end + +--- Suspend notifications +function naughty.suspend() + suspended = true +end + +--- Resume notifications +function naughty.resume() + suspended = false + for _, v in pairs(naughty.notifications.suspended) do + v.box.visible = true + if v.timer then v.timer:start() end + end + naughty.notifications.suspended = { } +end + +--- Toggle notification state +function naughty.toggle() + if suspended then + naughty.resume() + else + naughty.suspend() + end +end + +--- Evaluate desired position of the notification by index - internal +-- +-- @param s Screen to use +-- @param position top_right | top_left | bottom_right | bottom_left +-- | top_middle | bottom_middle +-- @param idx Index of the notification +-- @param[opt] width Popup width. +-- @param height Popup height +-- @return Absolute position and index in { x = X, y = Y, idx = I } table +local function get_offset(s, position, idx, width, height) + s = get_screen(s) + local ws = s.workarea + local v = {} + idx = idx or #naughty.notifications[s][position] + 1 + width = width or naughty.notifications[s][position][idx].width + + -- calculate x + if position:match("left") then + v.x = ws.x + naughty.config.padding + elseif position:match("middle") then + v.x = (ws.width / 2) - (width / 2) + else + v.x = ws.x + ws.width - (width + naughty.config.padding) + end + + -- calculate existing popups' height + local existing = 0 + for i = 1, idx-1, 1 do + existing = existing + naughty.notifications[s][position][i].height + naughty.config.spacing + end + + -- calculate y + if position:match("top") then + v.y = ws.y + naughty.config.padding + existing + else + v.y = ws.y + ws.height - (naughty.config.padding + height + existing) + end + + -- Find old notification to replace in case there is not enough room. + -- This tries to skip permanent notifications (without a timeout), + -- e.g. critical ones. + local find_old_to_replace = function() + for i = 1, idx-1 do + local n = naughty.notifications[s][position][i] + if n.timeout > 0 then + return n + end + end + -- Fallback to first one. + return naughty.notifications[s][position][1] + end + + -- if positioned outside workarea, destroy oldest popup and recalculate + if v.y + height > ws.y + ws.height or v.y < ws.y then + naughty.destroy(find_old_to_replace()) + idx = idx - 1 + v = get_offset(s, position, idx, width, height) + end + if not v.idx then v.idx = idx end + + return v +end + +--- Re-arrange notifications according to their position and index - internal +-- +-- @return None +local function arrange(s) + for p in pairs(naughty.notifications[s]) do + for i,notification in pairs(naughty.notifications[s][p]) do + local offset = get_offset(s, p, i, notification.width, notification.height) + notification.box:geometry({ x = offset.x, y = offset.y }) + notification.idx = offset.idx + end + end +end + +--- Destroy notification by notification object +-- +-- @param notification Notification object to be destroyed +-- @param reason One of the reasons from notificationClosedReason +-- @return True if the popup was successfully destroyed, nil otherwise +function naughty.destroy(notification, reason) + if notification and notification.box.visible then + if suspended then + for k, v in pairs(naughty.notifications.suspended) do + if v.box == notification.box then + table.remove(naughty.notifications.suspended, k) + break + end + end + end + local scr = notification.screen + table.remove(naughty.notifications[scr][notification.position], notification.idx) + if notification.timer then + notification.timer:stop() + end + notification.box.visible = false + arrange(scr) + if notification.destroy_cb and reason ~= naughty.notificationClosedReason.silent then + notification.destroy_cb(reason or naughty.notificationClosedReason.undefined) + end + return true + end +end + +--- Get notification by ID +-- +-- @param id ID of the notification +-- @return notification object if it was found, nil otherwise +function naughty.getById(id) + -- iterate the notifications to get the notfications with the correct ID + for s in pairs(naughty.notifications) do + for p in pairs(naughty.notifications[s]) do + for _, notification in pairs(naughty.notifications[s][p]) do + if notification.id == id then + return notification + end + end + end + end +end + +--- Install expiration timer for notification object. +-- @tparam notification notification Notification object. +-- @tparam number timeout Time in seconds to be set as expiration timeout. +local function set_timeout(notification, timeout) + local die = function (reason) + naughty.destroy(notification, reason) + end + if timeout > 0 then + local timer_die = timer { timeout = timeout } + timer_die:connect_signal("timeout", function() die(naughty.notificationClosedReason.expired) end) + if not suspended then + timer_die:start() + end + notification.timer = timer_die + end + notification.die = die +end + +--- Set new notification timeout. +-- @tparam notification notification Notification object, which timer is to be reset. +-- @tparam number new_timeout Time in seconds after which notification disappears. +-- @return None. +function naughty.reset_timeout(notification, new_timeout) + if notification.timer then notification.timer:stop() end + + local timeout = new_timeout or notification.timeout + set_timeout(notification, timeout) + notification.timeout = timeout + + notification.timer:start() +end + +--- Escape and set title and text for notification object. +-- @tparam notification notification Notification object. +-- @tparam string title Title of notification. +-- @tparam string text Main text of notification. +-- @return None. +local function set_text(notification, title, text) + local escape_pattern = "[<>&]" + local escape_subs = { ['<'] = "<", ['>'] = ">", ['&'] = "&" } + + local textbox = notification.textbox + + local function setMarkup(pattern, replacements) + return textbox:set_markup_silently(string.format('%s%s', title, text:gsub(pattern, replacements))) + end + local function setText() + textbox:set_text(string.format('%s %s', title, text)) + end + + -- Since the title cannot contain markup, it must be escaped first so that + -- it is not interpreted by Pango later. + title = title:gsub(escape_pattern, escape_subs) + -- Try to set the text while only interpreting
. + if not setMarkup("", "\n") then + -- That failed, escape everything which might cause an error from pango + if not setMarkup(escape_pattern, escape_subs) then + -- Ok, just ignore all pango markup. If this fails, we got some invalid utf8 + if not pcall(setText) then + textbox:set_markup("<Invalid markup or UTF8, cannot display message>") + end + end + end +end + +--- Replace title and text of an existing notification. +-- @tparam notification notification Notification object, which contents are to be replaced. +-- @tparam string new_title New title of notification. If not specified, old title remains unchanged. +-- @tparam string new_text New text of notification. If not specified, old text remains unchanged. +-- @return None. +function naughty.replace_text(notification, new_title, new_text) + local title = new_title + + if title then title = title .. "\n" else title = "" end + + set_text(notification, title, new_text) +end + +--- Create a notification. +-- +-- @tab args The argument table containing any of the arguments below. +-- @string[opt=""] args.text Text of the notification. +-- @string[opt] args.title Title of the notification. +-- @int[opt=5] args.timeout Time in seconds after which popup expires. +-- Set 0 for no timeout. +-- @int[opt] args.hover_timeout Delay in seconds after which hovered popup disappears. +-- @tparam[opt=focused] integer|screen args.screen Target screen for the notification. +-- @string[opt="top_right"] args.position Corner of the workarea displaying the popups. +-- Values: `"top_right"`, `"top_left"`, `"bottom_left"`, +-- `"bottom_right"`, `"top_middle"`, `"bottom_middle"`. +-- @bool[opt=true] args.ontop Boolean forcing popups to display on top. +-- @int[opt=auto] args.height Popup height. +-- @int[opt=auto] args.width Popup width. +-- @string[opt=beautiful.font or awesome.font] args.font Notification font. +-- @string[opt] args.icon Path to icon. +-- @int[opt] args.icon_size Desired icon size in px. +-- @string[opt=`beautiful.fg_focus` or `'#ffffff'`] args.fg Foreground color. +-- @string[opt=`beautiful.bg_focus` or `'#535d6c'`] args.bg Background color. +-- @int[opt=1] args.border_width Border width. +-- @string[opt=`beautiful.border_focus` or `'#535d6c'`] args.border_color Border color. +-- @tparam[opt] func args.run Function to run on left click. The notification +-- object will be passed to it as an argument. +-- You need to call e.g. +-- `notification.die(naughty.notificationClosedReason.dismissedByUser)` from +-- there to dismiss the notification yourself. +-- @tparam[opt] func args.destroy Function to run when notification is destroyed. +-- @tparam[opt] table args.preset Table with any of the above parameters. +-- Note: Any parameters specified directly in args will override ones defined +-- in the preset. +-- @tparam[opt] int args.replaces_id Replace the notification with the given ID. +-- @tparam[opt] func args.callback Function that will be called with all arguments. +-- The notification will only be displayed if the function returns true. +-- Note: this function is only relevant to notifications sent via dbus. +-- @tparam[opt] table args.actions Mapping that maps a string to a callback when this +-- action is selected. +-- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 }) +-- @treturn ?table The notification object, or nil in case a notification was +-- not displayed. +function naughty.notify(args) + if naughty.config.notify_callback then + args = naughty.config.notify_callback(args) + if not args then return end + end + + -- gather variables together + local preset = util.table.join(naughty.config.defaults or {}, + args.preset or naughty.config.presets.normal or {}) + local timeout = args.timeout or preset.timeout + local icon = args.icon or preset.icon + local icon_size = args.icon_size or preset.icon_size + local text = args.text or preset.text + local title = args.title or preset.title + local s = get_screen(args.screen or preset.screen or screen.focused()) + if not s then + local err = "naughty.notify: there is no screen available to display the following notification:" + err = string.format("%s title='%s' text='%s'", err, tostring(title or ""), tostring(text or "")) + require("gears.debug").print_warning(err) + return + end + local ontop = args.ontop or preset.ontop + local width = args.width or preset.width + local height = args.height or preset.height + local hover_timeout = args.hover_timeout or preset.hover_timeout + local opacity = args.opacity or preset.opacity + local margin = args.margin or preset.margin + local border_width = args.border_width or preset.border_width + local position = args.position or preset.position + local actions = args.actions + local destroy_cb = args.destroy + + -- beautiful + local beautiful = bt.get() + local font = args.font or preset.font or beautiful.font or capi.awesome.font + local fg = args.fg or preset.fg or beautiful.fg_normal or '#ffffff' + local bg = args.bg or preset.bg or beautiful.bg_normal or '#535d6c' + local border_color = args.border_color or preset.border_color or beautiful.bg_focus or '#535d6c' + local notification = { screen = s, destroy_cb = destroy_cb, timeout = timeout } + + -- replace notification if needed + if args.replaces_id then + local obj = naughty.getById(args.replaces_id) + if obj then + -- destroy this and ... + naughty.destroy(obj, naughty.notificationClosedReason.silent) + end + -- ... may use its ID + if args.replaces_id <= counter then + notification.id = args.replaces_id + else + counter = counter + 1 + notification.id = counter + end + else + -- get a brand new ID + counter = counter + 1 + notification.id = counter + end + + notification.position = position + + if title then title = title .. "\n" else title = "" end + + -- hook destroy + set_timeout(notification, timeout) + local die = notification.die + + local run = function () + if args.run then + args.run(notification) + else + die(naughty.notificationClosedReason.dismissedByUser) + end + end + + local hover_destroy = function () + if hover_timeout == 0 then + die(naughty.notificationClosedReason.expired) + else + if notification.timer then notification.timer:stop() end + notification.timer = timer { timeout = hover_timeout } + notification.timer:connect_signal("timeout", function() die(naughty.notificationClosedReason.expired) end) + notification.timer:start() + end + end + + -- create textbox + local textbox = wibox.widget.textbox() + local marginbox = wibox.container.margin() + marginbox:set_margins(margin) + marginbox:set_widget(textbox) + textbox:set_valign("middle") + textbox:set_font(font) + + notification.textbox = textbox + + set_text(notification, title, text) + + local actionslayout = wibox.layout.fixed.vertical() + local actions_max_width = 0 + local actions_total_height = 0 + if actions then + for action, callback in pairs(actions) do + local actiontextbox = wibox.widget.textbox() + local actionmarginbox = wibox.container.margin() + actionmarginbox:set_margins(margin) + actionmarginbox:set_widget(actiontextbox) + actiontextbox:set_valign("middle") + actiontextbox:set_font(font) + actiontextbox:set_markup(string.format('☛ %s', action)) + -- calculate the height and width + local w, h = actiontextbox:get_preferred_size(s) + local action_height = h + 2 * margin + local action_width = w + 2 * margin + + actionmarginbox:buttons(util.table.join( + button({ }, 1, callback), + button({ }, 3, callback) + )) + actionslayout:add(actionmarginbox) + + actions_total_height = actions_total_height + action_height + if actions_max_width < action_width then + actions_max_width = action_width + end + end + end + + -- create iconbox + local iconbox = nil + local iconmargin = nil + local icon_w, icon_h = 0, 0 + if icon then + -- Is this really an URI instead of a path? + if type(icon) == "string" and string.sub(icon, 1, 7) == "file://" then + icon = string.sub(icon, 8) + end + -- try to guess icon if the provided one is non-existent/readable + if type(icon) == "string" and not util.file_readable(icon) then + icon = util.geticonpath(icon, naughty.config.icon_formats, naughty.config.icon_dirs, icon_size) or icon + end + + -- is the icon file readable? + icon = surface.load_uncached(icon) + + -- if we have an icon, use it + if icon then + iconbox = wibox.widget.imagebox() + iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin) + if icon_size then + local scaled = cairo.ImageSurface(cairo.Format.ARGB32, icon_size, icon_size) + local cr = cairo.Context(scaled) + cr:scale(icon_size / icon:get_height(), icon_size / icon:get_width()) + cr:set_source_surface(icon, 0, 0) + cr:paint() + icon = scaled + end + iconbox:set_resize(false) + iconbox:set_image(icon) + icon_w = icon:get_width() + icon_h = icon:get_height() + end + end + + -- create container wibox + notification.box = wibox({ fg = fg, + bg = bg, + border_color = border_color, + border_width = border_width, + type = "notification" }) + + if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end + + -- calculate the width + if not width then + local w, _ = textbox:get_preferred_size(s) + width = w + (iconbox and icon_w + 2 * margin or 0) + 2 * margin + end + + if width < actions_max_width then + width = actions_max_width + end + + -- calculate the height + if not height then + local w = width - (iconbox and icon_w + 2 * margin or 0) - 2 * margin + local h = textbox:get_height_for_width(w, s) + if iconbox and icon_h + 2 * margin > h + 2 * margin then + height = icon_h + 2 * margin + else + height = h + 2 * margin + end + end + + height = height + actions_total_height + + -- crop to workarea size if too big + local workarea = s.workarea + if width > workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then + width = workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) + end + if height > workarea.height - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then + height = workarea.height - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) + end + + -- set size in notification object + notification.height = height + 2 * (border_width or 0) + notification.width = width + 2 * (border_width or 0) + + -- position the wibox + local offset = get_offset(s, notification.position, nil, notification.width, notification.height) + notification.box.ontop = ontop + notification.box:geometry({ width = width, + height = height, + x = offset.x, + y = offset.y }) + notification.box.opacity = opacity + notification.box.visible = true + notification.idx = offset.idx + + -- populate widgets + local layout = wibox.layout.fixed.horizontal() + if iconmargin then + layout:add(iconmargin) + end + layout:add(marginbox) + + local completelayout = wibox.layout.fixed.vertical() + completelayout:add(layout) + completelayout:add(actionslayout) + notification.box:set_widget(completelayout) + + -- Setup the mouse events + layout:buttons(util.table.join(button({ }, 1, run), + button({ }, 3, function() + die(naughty.notificationClosedReason.dismissedByUser) + end))) + + -- insert the notification to the table + table.insert(naughty.notifications[s][notification.position], notification) + + if suspended then + notification.box.visible = false + table.insert(naughty.notifications.suspended, notification) + end + + -- return the notification + return notification +end + +return naughty + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/naughty/dbus.lua b/lib/naughty/dbus.lua new file mode 100644 index 0000000..c49dc7e --- /dev/null +++ b/lib/naughty/dbus.lua @@ -0,0 +1,261 @@ +--------------------------------------------------------------------------- +-- DBUS/Notification support +-- Notify +-- +-- @author koniu <gkusnierz@gmail.com> +-- @copyright 2008 koniu +-- @module naughty.dbus +--------------------------------------------------------------------------- + +assert(dbus) + +-- Package environment +local pairs = pairs +local type = type +local string = string +local capi = { awesome = awesome, + dbus = dbus } +local util = require("awful.util") +local cairo = require("lgi").cairo + +local schar = string.char +local sbyte = string.byte +local tcat = table.concat +local tins = table.insert +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local naughty = require("naughty.core") + +--- Notification library, dbus bindings +local dbus = { config = {} } + +-- DBUS Notification constants +local urgency = { + low = "\0", + normal = "\1", + critical = "\2" +} + +--- DBUS notification to preset mapping. +-- The first element is an object containing the filter. +-- If the rules in the filter match, the associated preset will be applied. +-- The rules object can contain the following keys: urgency, category, appname. +-- The second element is the preset. +-- @tfield table 1 low urgency +-- @tfield table 2 normal urgency +-- @tfield table 3 critical urgency +-- @table config.mapping +dbus.config.mapping = { + {{urgency = urgency.low}, naughty.config.presets.low}, + {{urgency = urgency.normal}, naughty.config.presets.normal}, + {{urgency = urgency.critical}, naughty.config.presets.critical} +} + +local function sendActionInvoked(notificationId, action) + if capi.dbus then + capi.dbus.emit_signal("session", "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", "ActionInvoked", + "u", notificationId, + "s", action) + end +end + +local function sendNotificationClosed(notificationId, reason) + if capi.dbus then + capi.dbus.emit_signal("session", "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", "NotificationClosed", + "u", notificationId, + "u", reason) + end +end + +local function convert_icon(w, h, rowstride, channels, data) + -- Do the arguments look sane? (e.g. we have enough data) + local expected_length = rowstride * (h - 1) + w * channels + if w < 0 or h < 0 or rowstride < 0 or (channels ~= 3 and channels ~= 4) or + string.len(data) < expected_length then + w = 0 + h = 0 + end + + local format = cairo.Format[channels == 4 and 'ARGB32' or 'RGB24'] + + -- Figure out some stride magic (cairo dictates rowstride) + local stride = cairo.Format.stride_for_width(format, w) + local append = schar(0):rep(stride - 4 * w) + local offset = 0 + + -- Now convert each row on its own + local rows = {} + + for _ = 1, h do + local this_row = {} + + for i = 1 + offset, w * channels + offset, channels do + local R, G, B, A = sbyte(data, i, i + channels - 1) + tins(this_row, schar(B, G, R, A or 255)) + end + + -- Handle rowstride, offset is stride for the input, append for output + tins(this_row, append) + tins(rows, tcat(this_row)) + + offset = offset + rowstride + end + + return cairo.ImageSurface.create_for_data(tcat(rows), format, w, h, stride) +end + +capi.dbus.connect_signal("org.freedesktop.Notifications", function (data, appname, replaces_id, icon, title, text, actions, hints, expire) + local args = { } + if data.member == "Notify" then + if text ~= "" then + args.text = text + if title ~= "" then + args.title = title + end + else + if title ~= "" then + args.text = title + else + return + end + end + if appname ~= "" then + args.appname = appname + end + for _, obj in pairs(dbus.config.mapping) do + local filter, preset = obj[1], obj[2] + if (not filter.urgency or filter.urgency == hints.urgency) and + (not filter.category or filter.category == hints.category) and + (not filter.appname or filter.appname == appname) then + args.preset = util.table.join(args.preset, preset) + end + end + local preset = args.preset or naughty.config.defaults + local notification + if actions then + args.actions = {} + + for i = 1,#actions,2 do + local action_id = actions[i] + local action_text = actions[i + 1] + + if action_id == "default" then + args.run = function() + sendActionInvoked(notification.id, "default") + naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser) + end + elseif action_id ~= nil and action_text ~= nil then + args.actions[action_text] = function() + sendActionInvoked(notification.id, action_id) + naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser) + end + end + end + end + args.destroy = function(reason) + sendNotificationClosed(notification.id, reason) + end + if not preset.callback or (type(preset.callback) == "function" and + preset.callback(data, appname, replaces_id, icon, title, text, actions, hints, expire)) then + if icon ~= "" then + args.icon = icon + elseif hints.icon_data or hints.image_data then + if hints.icon_data == nil then hints.icon_data = hints.image_data end + + -- icon_data is an array: + -- 1 -> width + -- 2 -> height + -- 3 -> rowstride + -- 4 -> has alpha + -- 5 -> bits per sample + -- 6 -> channels + -- 7 -> data + local w, h, rowstride, _, _, channels, icon_data = unpack(hints.icon_data) + args.icon = convert_icon(w, h, rowstride, channels, icon_data) + end + if replaces_id and replaces_id ~= "" and replaces_id ~= 0 then + args.replaces_id = replaces_id + end + if expire and expire > -1 then + args.timeout = expire / 1000 + end + notification = naughty.notify(args) + return "u", notification.id + end + return "u", "0" + elseif data.member == "CloseNotification" then + local obj = naughty.getById(appname) + if obj then + naughty.destroy(obj, naughty.notificationClosedReason.dismissedByCommand) + end + elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then + -- name of notification app, name of vender, version, specification version + return "s", "naughty", "s", "awesome", "s", capi.awesome.version, "s", "1.0" + elseif data.member == "GetCapabilities" then + -- We actually do display the body of the message, we support , + -- and in the body and we handle static (non-animated) icons. + return "as", { "s", "body", "s", "body-markup", "s", "icon-static", "s", "actions" } + end +end) + +capi.dbus.connect_signal("org.freedesktop.DBus.Introspectable", function (data) + if data.member == "Introspect" then + local xml = [=[ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]=] + return "s", xml + end +end) + +-- listen for dbus notification requests +capi.dbus.request_name("session", "org.freedesktop.Notifications") + +return dbus + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/naughty/init.lua b/lib/naughty/init.lua new file mode 100644 index 0000000..71dee9f --- /dev/null +++ b/lib/naughty/init.lua @@ -0,0 +1,14 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter <psychon@znc.in> +-- @copyright 2014 Uli Schlachter +-- @module naughty +--------------------------------------------------------------------------- + +local naughty = require("naughty.core") +if dbus then + naughty.dbus = require("naughty.dbus") +end + +return naughty + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/shifty b/lib/shifty new file mode 160000 index 0000000..8ca24ad --- /dev/null +++ b/lib/shifty @@ -0,0 +1 @@ +Subproject commit 8ca24ad1bfde153a9902209aa6b0027145541eb4 diff --git a/lib/shifty.lua b/lib/shifty.lua new file mode 100644 index 0000000..9320295 --- /dev/null +++ b/lib/shifty.lua @@ -0,0 +1,1138 @@ +--- Shifty: Dynamic tagging library, version for awesome v3.5 +-- @author koniu <gkusnierz@gmail.com> +-- @author resixian (aka bioe007) <resixian@gmail.com> +-- @author cdump <andreevmaxim@gmail.com> +-- +-- https://github.com/cdump/awesome-shifty +-- http://awesome.naquadah.org/wiki/index.php?title=Shifty + +-- environment +local type = type +local ipairs = ipairs +local table = table +local string = string +local beautiful = require("beautiful") +local awful = require("awful") +local wibox = require("wibox") +local pairs = pairs +local io = io +local math = math +local tonumber = tonumber +local dbg= dbg +local capi = { + client = client, + tag = tag, + screen = screen, + button = button, + mouse = mouse, + root = root, + timer = timer +} + +local shifty = {} + +-- variables +shifty.config = {} +shifty.config.tags = {} +shifty.config.apps = {} +shifty.config.defaults = {} +shifty.config.float_bars = false +shifty.config.guess_name = true +shifty.config.guess_position = true +shifty.config.remember_index = true +shifty.config.sloppy = true +shifty.config.default_name = "new" +shifty.config.clientkeys = {} +shifty.config.globalkeys = nil +shifty.config.layouts = {} +shifty.config.prompt_sources = { + "config_tags", + "config_apps", + "existing", + "history" +} +shifty.config.prompt_matchers = { + "^", + ":", + "" +} +shifty.config.delete_deserted = true + +local matchp = "" +local index_cache = {} +for i = 1, capi.screen.count() do index_cache[i] = {} end + +--name2tags: matches string 'name' to tag objects +-- @param name : tag name to find +-- @param scr : screen to look for tags on +-- @return table of tag objects or nil +function name2tags(name, scr) + local ret = {} + local a, b = scr or 1, scr or capi.screen.count() + for s = a, b do + for i, t in ipairs(awful.tag.gettags(s)) do + if name == t.name then + table.insert(ret, t) + end + end + end + if #ret > 0 then return ret end +end + +function name2tag(name, scr, idx) + local ts = name2tags(name, scr) + if ts then return ts[idx or 1] end +end + +--tag2index: finds index of a tag object +-- @param scr : screen number to look for tag on +-- @param tag : the tag object to find +-- @return the index [or zero] or end of the list +function tag2index(scr, tag) + for i, t in ipairs(awful.tag.gettags(scr)) do + if t == tag then return i end + end +end + +--rename +--@param tag: tag object to be renamed +--@param prefix: if any prefix is to be added +--@param no_selectall: +function shifty.rename(tag, prefix, no_selectall) + local theme = beautiful.get() + local t = tag or awful.tag.selected(capi.mouse.screen) + + if t == nil then return end + + local scr = awful.tag.getscreen(t) + local bg = nil + local fg = nil + local text = prefix or t.name + local before = t.name + + if t == awful.tag.selected(scr) then + bg = theme.bg_focus or '#535d6c' + fg = theme.fg_urgent or '#ffffff' + else + bg = theme.bg_normal or '#222222' + fg = theme.fg_urgent or '#ffffff' + end + + local tag_index = tag2index(scr, t) + -- Access to textbox widget in taglist + local tb_widget = shifty.taglist[scr].widgets[tag_index].widget.widgets[2].widget + awful.prompt.run({ + fg_cursor = fg, bg_cursor = bg, ul_cursor = "single", + text = text, selectall = not no_selectall}, + tb_widget, + function (name) if name:len() > 0 then t.name = name; end end, + completion, + awful.util.getdir("cache") .. "/history_tags", + nil, + function () + if t.name == before then + if awful.tag.getproperty(t, "initial") then shifty.del(t) end + else + awful.tag.setproperty(t, "initial", true) + set(t) + end + tagkeys(capi.screen[scr]) + t:emit_signal("property::name") + end + ) +end + +--send: moves client to tag[idx] +-- maybe this isn't needed here in shifty? +-- @param idx the tag number to send a client to +function send(idx) + local scr = capi.client.focus.screen or capi.mouse.screen + local sel = awful.tag.selected(scr) + local sel_idx = tag2index(scr, sel) + local tags = awful.tag.gettags(scr) + local target = awful.util.cycle(#tags, sel_idx + idx) + awful.client.movetotag(tags[target], capi.client.focus) + awful.tag.viewonly(tags[target]) +end + +function shifty.send_next() send(1) end +function shifty.send_prev() send(-1) end + +--pos2idx: translate shifty position to tag index +--@param pos: position (an integer) +--@param scr: screen number +function pos2idx(pos, scr) + local v = 1 + if pos and scr then + local tags = awful.tag.gettags(scr) + for i = #tags , 1, -1 do + local t = tags[i] + if awful.tag.getproperty(t, "position") and + awful.tag.getproperty(t, "position") <= pos then + v = i + 1 + break + end + end + end + return v +end + +--select : helper function chooses the first non-nil argument +--@param args - table of arguments +local function select(args) + for i, a in pairs(args) do + if a ~= nil then + return a + end + end +end + +--tagtoscr : move an entire tag to another screen +-- +--@param scr : the screen to move tag to +--@param t : the tag to be moved [awful.tag.selected()] +--@return the tag +function shifty.tagtoscr(scr, t) + -- break if called with an invalid screen number + if not scr or scr < 1 or scr > capi.screen.count() then return end + -- tag to move + local otag = t or awful.tag.selected() + + awful.tag.setscreen(otag, scr) + -- set screen and then reset tag to order properly + if #otag:clients() > 0 then + for _ , c in ipairs(otag:clients()) do + if not c.sticky then + c.screen = scr + c:tags({otag}) + else + awful.client.toggletag(otag, c) + end + end + end + return otag +end + +--set : set a tags properties +--@param t: the tag +--@param args : a table of optional (?) tag properties +--@return t - the tag object +function set(t, args) + if not t then return end + if not args then args = {} end + + -- set the name + t.name = args.name or t.name + + -- attempt to load preset on initial run + local preset = (awful.tag.getproperty(t, "initial") and + shifty.config.tags[t.name]) or {} + + -- pick screen and get its tag table + local scr = args.screen or + (not awful.tag.getscreen(t) and awful.tag.getscreen(preset)) or + awful.tag.getscreen(t) or + capi.mouse.screen + + local clientstomove = nil + if scr > capi.screen.count() then scr = capi.screen.count() end + if awful.tag.getscreen(t) and scr ~= awful.tag.getscreen(t) then + shifty.tagtoscr(scr, t) + awful.tag.setscreen(t, nil) + end + local tags = awful.tag.gettags(scr) + + -- try to guess position from the name + local guessed_position = nil + if not (args.position or preset.position) and shifty.config.guess_position then + local num = t.name:find('^[1-9]') + if num then guessed_position = tonumber(t.name:sub(1, 1)) end + end + + -- allow preset.layout to be a table to provide a different layout per + -- screen for a given tag + local preset_layout = preset.layout + if preset_layout and preset_layout[scr] then + preset_layout = preset.layout[scr] + end + + -- select from args, preset, getproperty, + -- config.defaults.configs or defaults + local props = { + layout = select{args.layout, preset_layout, + awful.tag.getproperty(t, "layout"), + shifty.config.defaults.layout, awful.layout.suit.tile}, + mwfact = select{args.mwfact, preset.mwfact, + awful.tag.getproperty(t, "mwfact"), + shifty.config.defaults.mwfact, 0.55}, + nmaster = select{args.nmaster, preset.nmaster, + awful.tag.getproperty(t, "nmaster"), + shifty.config.defaults.nmaster, 1}, + ncol = select{args.ncol, preset.ncol, + awful.tag.getproperty(t, "ncol"), + shifty.config.defaults.ncol, 1}, + matched = select{args.matched, awful.tag.getproperty(t, "matched")}, + exclusive = select{args.exclusive, preset.exclusive, + awful.tag.getproperty(t, "exclusive"), + shifty.config.defaults.exclusive}, + persist = select{args.persist, preset.persist, + awful.tag.getproperty(t, "persist"), + shifty.config.defaults.persist}, + nopopup = select{args.nopopup, preset.nopopup, + awful.tag.getproperty(t, "nopopup"), + shifty.config.defaults.nopopup}, + leave_kills = select{args.leave_kills, preset.leave_kills, + awful.tag.getproperty(t, "leave_kills"), + shifty.config.defaults.leave_kills}, + max_clients = select{args.max_clients, preset.max_clients, + awful.tag.getproperty(t, "max_clients"), + shifty.config.defaults.max_clients}, + position = select{args.position, preset.position, guessed_position, + awful.tag.getproperty(t, "position")}, + icon = select{args.icon and args.icon, + preset.icon and preset.icon, + awful.tag.getproperty(t, "icon"), + shifty.config.defaults.icon and shifty.config.defaults.icon}, + icon_only = select{args.icon_only, preset.icon_only, + awful.tag.getproperty(t, "icon_only"), + shifty.config.defaults.icon_only}, + sweep_delay = select{args.sweep_delay, preset.sweep_delay, + awful.tag.getproperty(t, "sweep_delay"), + shifty.config.defaults.sweep_delay}, + overload_keys = select{args.overload_keys, preset.overload_keys, + awful.tag.getproperty(t, "overload_keys"), + shifty.config.defaults.overload_keys}, + } + + -- get layout by name if given as string + if type(props.layout) == "string" then + props.layout = getlayout(props.layout) + end + + -- set keys + if args.keys or preset.keys then + local keys = awful.util.table.join(shifty.config.globalkeys, + args.keys or preset.keys) + if props.overload_keys then + props.keys = keys + else + props.keys = squash_keys(keys) + end + end + + -- calculate desired taglist index + local index = args.index or preset.index or shifty.config.defaults.index + local rel_index = args.rel_index or + preset.rel_index or + shifty.config.defaults.rel_index + local sel = awful.tag.selected(scr) + --TODO: what happens with rel_idx if no tags selected + local sel_idx = (sel and tag2index(scr, sel)) or 0 + local t_idx = tag2index(scr, t) + local limit = (not t_idx and #tags + 1) or #tags + local idx = nil + + if rel_index then + idx = awful.util.cycle(limit, (t_idx or sel_idx) + rel_index) + elseif index then + idx = awful.util.cycle(limit, index) + elseif props.position then + idx = pos2idx(props.position, scr) + if t_idx and t_idx < idx then idx = idx - 1 end + elseif shifty.config.remember_index and index_cache[scr][t.name] then + idx = math.min(index_cache[scr][t.name], #tags+1) + elseif not t_idx then + idx = #tags + 1 + end + + -- if we have a new index, remove from old index and insert + if idx then + if t_idx then table.remove(tags, t_idx) end + table.insert(tags, idx, t) + index_cache[scr][t.name] = idx + end + + -- set tag properties and push the new tag table + for i, tmp_tag in ipairs(tags) do + awful.tag.setscreen(tmp_tag, scr) + awful.tag.setproperty(tmp_tag, "index", i) + end + for prop, val in pairs(props) do awful.tag.setproperty(t, prop, val) end + + -- execute run/spawn + if awful.tag.getproperty(t, "initial") then + local spawn = args.spawn or preset.spawn or shifty.config.defaults.spawn + local run = args.run or preset.run or shifty.config.defaults.run + if spawn and args.matched ~= true then + awful.util.spawn_with_shell(spawn, scr) + end + if run then run(t) end + awful.tag.setproperty(t, "initial", nil) + end + + + return t +end + +function shift_next() set(awful.tag.selected(), {rel_index = 1}) end +function shift_prev() set(awful.tag.selected(), {rel_index = -1}) end + +--add : adds a tag +--@param args: table of optional arguments +function shifty.add(args) + if not args then args = {} end + local name = args.name or " " + + -- initialize a new tag object and its data structure + local t = awful.tag.add(name, { initial = true }) + + + -- apply tag settings + set(t, args) + + -- unless forbidden or if first tag on the screen, show the tag + if not (awful.tag.getproperty(t, "nopopup") or args.noswitch) or + #awful.tag.gettags(awful.tag.getscreen(t)) == 1 then + awful.tag.viewonly(t) + end + + -- get the name or rename + if args.name then + t.name = args.name + else + -- FIXME: hack to delay rename for un-named tags for + -- tackling taglist refresh which disabled prompt + -- from being rendered until input + awful.tag.setproperty(t, "initial", true) + local f + local tmr + if args.position then + f = function() shifty.rename(t, args.rename, true); tmr:stop() end + else + f = function() shifty.rename(t); tmr:stop() end + end + tmr = capi.timer({timeout = 0.01}) + tmr:connect_signal("timeout", f) + tmr:start() + end + + return t +end + +--del : delete a tag +--@param tag : the tag to be deleted [current tag] +function shifty.del(tag) + local scr = (tag and awful.tag.getscreen(tag)) or capi.mouse.screen or 1 + local tags = awful.tag.gettags(scr) + local sel = awful.tag.selected(scr) + local t = tag or sel + local idx = tag2index(scr, t) + + -- return if tag not empty (except sticky) + local clients = t:clients() + local sticky = 0 + for i, c in ipairs(clients) do + if c.sticky then sticky = sticky + 1 end + end + if #clients > sticky then return end + + -- store index for later + index_cache[scr][t.name] = idx + + -- remove tag + awful.tag.delete(t) + + -- if the current tag is being deleted, restore from history + if t == sel and #tags > 1 then + awful.tag.history.restore(scr, 1) + -- this is supposed to cycle if history is invalid? + -- e.g. if many tags are deleted in a row + if not awful.tag.selected(scr) then + awful.tag.viewonly(tags[awful.util.cycle(#tags, idx - 1)]) + end + end + + -- FIXME: what is this for?? + if capi.client.focus then capi.client.focus:raise() end +end + +--is_client_tagged : replicate behavior in tag.c - returns true if the +--given client is tagged with the given tag +function is_client_tagged(tag, client) + for i, c in ipairs(tag:clients()) do + if c == client then + return true + end + end + return false +end + +--match : handles app->tag matching, a replacement for the manage hook in +-- rc.lua +--@param c : client to be matched +function match(c, startup) + local nopopup, intrusive, nofocus, run, slave + local wfact, struts, geom, float + local target_tag_names, target_tags = {}, {} + local typ = c.type + local cls = c.class + local inst = c.instance + local role = c.role + local name = c.name + local keys = shifty.config.clientkeys or c:keys() or {} + local target_screen = capi.mouse.screen + + c.border_color = beautiful.border_normal + c.border_width = beautiful.border_width + + -- try matching client to config.apps + for i, a in ipairs(shifty.config.apps) do + if a.match then + local matched = false + -- match only class + if not matched and cls and a.match.class then + for k, w in ipairs(a.match.class) do + matched = cls:find(w) + if matched then + break + end + end + end + -- match only instance + if not matched and inst and a.match.instance then + for k, w in ipairs(a.match.instance) do + matched = inst:find(w) + if matched then + break + end + end + end + -- match only name + if not matched and name and a.match.name then + for k, w in ipairs(a.match.name) do + matched = name:find(w) + if matched then + break + end + end + end + -- match only role + if not matched and role and a.match.role then + for k, w in ipairs(a.match.role) do + matched = role:find(w) + if matched then + break + end + end + end + -- match only type + if not matched and typ and a.match.type then + for k, w in ipairs(a.match.type) do + matched = typ:find(w) + if matched then + break + end + end + end + -- check everything else against all attributes + if not matched then + for k, w in ipairs(a.match) do + matched = (cls and cls:find(w)) or + (inst and inst:find(w)) or + (name and name:find(w)) or + (role and role:find(w)) or + (typ and typ:find(w)) + if matched then + break + end + end + end + -- set attributes + if matched then + if a.screen then target_screen = a.screen end + if a.tag then + if type(a.tag) == "string" then + target_tag_names = {a.tag} + else + target_tag_names = a.tag + end + end + if a.startup and startup then + a = awful.util.table.join(a, a.startup) + end + if a.geometry ~=nil then + geom = {x = a.geometry[1], + y = a.geometry[2], + width = a.geometry[3], + height = a.geometry[4]} + end + if a.float ~= nil then float = a.float end + if a.slave ~=nil then slave = a.slave end + if a.border_width ~= nil then + c.border_width = a.border_width + end + if a.nopopup ~=nil then nopopup = a.nopopup end + if a.intrusive ~=nil then + intrusive = a.intrusive + end + if a.fullscreen ~=nil then + c.fullscreen = a.fullscreen + end + if a.honorsizehints ~=nil then + c.size_hints_honor = a.honorsizehints + end + if a.kill ~=nil then c:kill(); return end + if a.ontop ~= nil then c.ontop = a.ontop end + if a.above ~= nil then c.above = a.above end + if a.below ~= nil then c.below = a.below end + if a.buttons ~= nil then + c:buttons(a.buttons) + end + if a.nofocus ~= nil then nofocus = a.nofocus end + if a.keys ~= nil then + keys = awful.util.table.join(keys, a.keys) + end + if a.hidden ~= nil then c.hidden = a.hidden end + if a.minimized ~= nil then + c.minimized = a.minimized + end + if a.dockable ~= nil then + awful.client.dockable.set(c, a.dockable) + end + if a.urgent ~= nil then + c.urgent = a.urgent + end + if a.opacity ~= nil then + c.opacity = a.opacity + end + if a.run ~= nil then run = a.run end + if a.sticky ~= nil then c.sticky = a.sticky end + if a.wfact ~= nil then wfact = a.wfact end + if a.struts then struts = a.struts end + if a.skip_taskbar ~= nil then + c.skip_taskbar = a.skip_taskbar + end + if a.props then + for kk, vv in pairs(a.props) do + awful.client.property.set(c, kk, vv) + end + end + end + end + end + + -- set key bindings + c:keys(keys) + + -- Add titlebars to all clients when the float, remove when they are + -- tiled. + if shifty.config.float_bars then + shifty.create_titlebar(c) + + c:connect_signal("property::floating", function(c) + if awful.client.floating.get(c) then + awful.titlebar(c) + else + awful.titlebar(c, { size = 0 }) + end + awful.placement.no_offscreen(c) + end) + end + + -- set properties of floating clients + if float ~= nil then + awful.client.floating.set(c, float) + awful.placement.no_offscreen(c) + end + + local sel = awful.tag.selectedlist(target_screen) + if not target_tag_names or #target_tag_names == 0 then + -- if not matched to some names try putting + -- client in c.transient_for or current tags + if c.transient_for then + target_tags = c.transient_for:tags() + elseif #sel > 0 then + for i, t in ipairs(sel) do + local mc = awful.tag.getproperty(t, "max_clients") + if intrusive or + not (awful.tag.getproperty(t, "exclusive") or + (mc and mc >= #t:clients())) then + table.insert(target_tags, t) + end + end + end + end + + if (not target_tag_names or #target_tag_names == 0) and + (not target_tags or #target_tags == 0) then + -- if we still don't know any target names/tags guess + -- name from class or use default + if shifty.config.guess_name and cls then + target_tag_names = {cls:lower()} + else + target_tag_names = {shifty.config.default_name} + end + end + + if #target_tag_names > 0 and #target_tags == 0 then + -- translate target names to tag objects, creating + -- missing ones + for i, tn in ipairs(target_tag_names) do + local res = {} + for j, t in ipairs(name2tags(tn, target_screen) or + name2tags(tn) or {}) do + local mc = awful.tag.getproperty(t, "max_clients") + local tagged = is_client_tagged(t, c) + if intrusive or + not (mc and (((#t:clients() >= mc) and not + tagged) or + (#t:clients() > mc))) or + intrusive then + if awful.tag.getscreen(t) == mouse.screen then + table.insert(res, t) + end + end + end + if #res == 0 then + table.insert(target_tags, + shifty.add({name = tn, + noswitch = true, + matched = true})) + else + target_tags = awful.util.table.join(target_tags, res) + end + end + end + + -- set client's screen/tag if needed + target_screen = awful.tag.getscreen(target_tags[1]) or target_screen + if c.screen ~= target_screen then c.screen = target_screen end + if slave then awful.client.setslave(c) end + c:tags(target_tags) + + if wfact then awful.client.setwfact(wfact, c) end + if geom then c:geometry(geom) end + if struts then c:struts(struts) end + + local showtags = {} + local u = nil + if #target_tags > 0 and not startup then + -- switch or highlight + for i, t in ipairs(target_tags) do + if not (nopopup or awful.tag.getproperty(t, "nopopup")) then + table.insert(showtags, t) + elseif not startup then + c.urgent = true + end + end + if #showtags > 0 then + local ident = false + -- iterate selected tags and and see if any targets + -- currently selected + for kk, vv in pairs(showtags) do + for _, tag in pairs(sel) do + if tag == vv then + ident = true + end + end + end + if not ident then + awful.tag.viewmore(showtags, c.screen) + end + end + end + + if not (nofocus or c.hidden or c.minimized) then + --focus and raise accordingly or lower if supressed + if (target and target ~= sel) and + (awful.tag.getproperty(target, "nopopup") or nopopup) then + awful.client.focus.history.add(c) + else + capi.client.focus = c + end + c:raise() + else + c:lower() + end + + if shifty.config.sloppy then + -- Enable sloppy focus + c:connect_signal("mouse::enter", function(c) + if awful.client.focus.filter(c) and + awful.layout.get(c.screen) ~= awful.layout.suit.magnifier then + capi.client.focus = c + end + end) + end + + -- execute run function if specified + if run then run(c, target) end + +end + +--sweep : hook function that marks tags as used, visited, +--deserted also handles deleting used and empty tags +function sweep() + for s = 1, capi.screen.count() do + for i, t in ipairs(awful.tag.gettags(s)) do + local clients = t:clients() + local sticky = 0 + for i, c in ipairs(clients) do + if c.sticky then sticky = sticky + 1 end + end + if #clients == sticky then + if awful.tag.getproperty(t, "used") and + not awful.tag.getproperty(t, "persist") then + if awful.tag.getproperty(t, "deserted") or + not awful.tag.getproperty(t, "leave_kills") then + local delay = awful.tag.getproperty(t, "sweep_delay") + if delay then + local tmr + local f = function() + shifty.del(t); tmr:stop() + end + tmr = capi.timer({timeout = delay}) + tmr:connect_signal("timeout", f) + tmr:start() + else + if shifty.config.delete_deserted then + shifty.del(t) + end + end + else + if awful.tag.getproperty(t, "visited") and + not t.selected then + awful.tag.setproperty(t, "deserted", true) + end + end + end + else + awful.tag.setproperty(t, "used", true) + end + if t.selected then + awful.tag.setproperty(t, "visited", true) + end + end + end +end + +--getpos : returns a tag to match position +-- @param pos : the index to find +-- @return v : the tag (found or created) at position == 'pos' +function shifty.getpos(pos, scr_arg) + local v = nil + local existing = {} + local selected = nil + local scr = scr_arg or capi.mouse.screen or 1 + + -- search for existing tag assigned to pos + for i = 1, capi.screen.count() do + for j, t in ipairs(awful.tag.gettags(i)) do + if awful.tag.getproperty(t, "position") == pos then + table.insert(existing, t) + if t.selected and i == scr then + selected = #existing + end + end + end + end + + if #existing > 0 then + -- if there is no selected tag on current screen, look for the first one + if not selected then + for _, tag in pairs(existing) do + if awful.tag.getscreen(tag) == scr then return tag end + end + + -- no tag found, loop through the other tags + selected = #existing + end + + -- look for the next unselected tag + i = selected + repeat + i = awful.util.cycle(#existing, i + 1) + tag = existing[i] + + if (scr_arg == nil or awful.tag.getscreen(tag) == scr_arg) and not tag.selected then return tag end + until i == selected + + -- if the screen is not specified or + -- if a selected tag exists on the specified screen + -- return the selected tag + if scr_arg == nil or awful.tag.getscreen(existing[selected]) == scr then return existing[selected] end + + -- if scr_arg ~= nil and no tag exists on this screen, continue + end + + local screens = {} + for s = 1, capi.screen.count() do table.insert(screens, s) end + + -- search for preconf with 'pos' on current screen and create it + for i, j in pairs(shifty.config.tags) do + local tag_scr = j.screen or screens + if type(tag_scr) ~= 'table' then tag_scr = {tag_scr} end + + if j.position == pos and awful.util.table.hasitem(tag_scr, scr) then + return shifty.add({name = i, + position = pos, + noswitch = not switch}) + end + end + + -- not existing, not preconfigured + return shifty.add({position = pos, + rename = pos .. ':', + no_selectall = true, + noswitch = not switch}) +end + +--init : search config.tags for initial set of +--tags to open +function shifty.init() + local numscr = capi.screen.count() + + local screens = {} + for s = 1, capi.screen.count() do table.insert(screens, s) end + + for i, j in pairs(shifty.config.tags) do + local scr = j.screen or screens + if type(scr) ~= 'table' then + scr = {scr} + end + for _, s in pairs(scr) do + if j.init and (s <= numscr) then + shifty.add({name = i, + persist = true, + screen = s, + layout = j.layout, + mwfact = j.mwfact}) + end + end + end +end + +-- Create a titlebar for the given client +-- By default, make it invisible (size = 0) + +function shifty.create_titlebar(c) + -- Widgets that are aligned to the left + local left_layout = wibox.layout.fixed.horizontal() + left_layout:add(awful.titlebar.widget.iconwidget(c)) + + -- Widgets that are aligned to the right + local right_layout = wibox.layout.fixed.horizontal() + right_layout:add(awful.titlebar.widget.floatingbutton(c)) + right_layout:add(awful.titlebar.widget.maximizedbutton(c)) + right_layout:add(awful.titlebar.widget.stickybutton(c)) + right_layout:add(awful.titlebar.widget.ontopbutton(c)) + right_layout:add(awful.titlebar.widget.closebutton(c)) + + -- The title goes in the middle + local title = awful.titlebar.widget.titlewidget(c) + title:buttons(awful.util.table.join( + awful.button({ }, 1, function() + client.focus = c + c:raise() + awful.mouse.client.move(c) + end), + awful.button({ }, 3, function() + client.focus = c + c:raise() + awful.mouse.client.resize(c) + end) + )) + + -- Now bring it all together + local layout = wibox.layout.align.horizontal() + layout:set_left(left_layout) + layout:set_right(right_layout) + layout:set_middle(title) + + awful.titlebar(c, { size = 0 }):set_widget(layout) +end + +--count : utility function returns the index of a table element +--FIXME: this is currently used only in remove_dup, so is it really +--necessary? +function count(table, element) + local v = 0 + for i, e in pairs(table) do + if element == e then v = v + 1 end + end + return v +end + +--remove_dup : used by shifty.completion when more than one +--tag at a position exists +function remove_dup(table) + local v = {} + for i, entry in ipairs(table) do + if count(v, entry) == 0 then v[#v+ 1] = entry end + end + return v +end + +--completion : prompt completion +-- +function completion(cmd, cur_pos, ncomp, sources, matchers) + + -- get sources and matches tables + sources = sources or shifty.config.prompt_sources + matchers = matchers or shifty.config.prompt_matchers + + local get_source = { + -- gather names from config.tags + config_tags = function() + local ret = {} + for n, p in pairs(shifty.config.tags) do + table.insert(ret, n) + end + return ret + end, + -- gather names from config.apps + config_apps = function() + local ret = {} + for i, p in pairs(shifty.config.apps) do + if p.tag then + if type(p.tag) == "string" then + table.insert(ret, p.tag) + else + ret = awful.util.table.join(ret, p.tag) + end + end + end + return ret + end, + -- gather names from existing tags, starting with the + -- current screen + existing = function() + local ret = {} + for i = 1, capi.screen.count() do + local s = awful.util.cycle(capi.screen.count(), + capi.mouse.screen + i - 1) + local tags = awful.tag.gettags(s) + for j, t in pairs(tags) do + table.insert(ret, t.name) + end + end + return ret + end, + -- gather names from history + history = function() + local ret = {} + local f = io.open(awful.util.getdir("cache") .. + "/history_tags") + for name in f:lines() do table.insert(ret, name) end + f:close() + return ret + end, + } + + -- if empty, match all + if #cmd == 0 or cmd == " " then cmd = "" end + + -- match all up to the cursor if moved or no matchphrase + if matchp == "" or + cmd:sub(cur_pos, cur_pos+#matchp) ~= matchp then + matchp = cmd:sub(1, cur_pos) + end + + -- find matching commands + local matches = {} + for i, src in ipairs(sources) do + local source = get_source[src]() + for j, matcher in ipairs(matchers) do + for k, name in ipairs(source) do + if name:find(matcher .. matchp) then + table.insert(matches, name) + end + end + end + end + + -- no matches + if #matches == 0 then return cmd, cur_pos end + + -- remove duplicates + matches = remove_dup(matches) + + -- cycle + while ncomp > #matches do ncomp = ncomp - #matches end + + -- put cursor at the end of the matched phrase + if #matches == 1 then + cur_pos = #matches[ncomp] + 1 + else + cur_pos = matches[ncomp]:find(matchp) + #matchp + end + + -- return match and position + return matches[ncomp], cur_pos +end + +-- tagkeys : hook function that sets keybindings per tag +function tagkeys(s) + local sel = awful.tag.selected(s.index) + local keys = awful.tag.getproperty(sel, "keys") or + shifty.config.globalkeys + if keys and sel.selected then capi.root.keys(keys) end +end + +-- squash_keys: helper function which removes duplicate +-- keybindings by picking only the last one to be listed in keys +-- table arg +function squash_keys(keys) + local squashed = {} + local ret = {} + for i, k in ipairs(keys) do + squashed[table.concat(k.modifiers) .. k.key] = k + end + for i, k in pairs(squashed) do + table.insert(ret, k) + end + return ret +end + +-- getlayout: returns a layout by name +function getlayout(name) + for _, layout in ipairs(shifty.config.layouts) do + if awful.layout.getname(layout) == name then + return layout + end + end +end + +-- add signals before using them +-- Note: these signals are emitted when tag properties +-- are accessed through awful.tag.setproperty +capi.tag.add_signal("property::initial") +capi.tag.add_signal("property::used") +capi.tag.add_signal("property::visited") +capi.tag.add_signal("property::deserted") +capi.tag.add_signal("property::matched") +capi.tag.add_signal("property::selected") +capi.tag.add_signal("property::position") +capi.tag.add_signal("property::exclusive") +capi.tag.add_signal("property::persist") +capi.tag.add_signal("property::index") +capi.tag.add_signal("property::nopopup") +capi.tag.add_signal("property::leave_kills") +capi.tag.add_signal("property::max_clients") +capi.tag.add_signal("property::icon_only") +capi.tag.add_signal("property::sweep_delay") +capi.tag.add_signal("property::overload_keys") + +-- replace awful's default hook +capi.client.connect_signal("manage", match) +capi.client.connect_signal("unmanage", sweep) +capi.client.disconnect_signal("manage", awful.tag.withcurrent) + +for s = 1, capi.screen.count() do + awful.tag.attached_connect_signal(s, "property::selected", sweep) + awful.tag.attached_connect_signal(s, "tagged", sweep) + capi.screen[s]:connect_signal("tag::history::update", tagkeys) +end + +return shifty + diff --git a/lib/wibox/container/arcchart.lua b/lib/wibox/container/arcchart.lua new file mode 100644 index 0000000..3794630 --- /dev/null +++ b/lib/wibox/container/arcchart.lua @@ -0,0 +1,566 @@ +--------------------------------------------------------------------------- +-- +-- A circular chart (arc chart). +-- +-- It can contain a central widget (or not) and display multiple values. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_defaults_arcchart.svg) +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2013 Emmanuel Lepage Vallee +-- @classmod wibox.container.arcchart +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local base = require("wibox.widget.base") +local shape = require("gears.shape" ) +local util = require( "awful.util" ) +local color = require( "gears.color" ) +local beautiful = require("beautiful" ) + + +local arcchart = { mt = {} } + +--- The progressbar border background color. +-- @beautiful beautiful.arcchart_border_color + +--- The progressbar foreground color. +-- @beautiful beautiful.arcchart_color + +--- The progressbar border width. +-- @beautiful beautiful.arcchart_border_width + +--- The padding between the outline and the progressbar. +-- @beautiful beautiful.arcchart_paddings +-- @tparam[opt=0] table|number paddings A number or a table +-- @tparam[opt=0] number paddings.top +-- @tparam[opt=0] number paddings.bottom +-- @tparam[opt=0] number paddings.left +-- @tparam[opt=0] number paddings.right + +--- The arc thickness. +-- @beautiful beautiful.thickness +-- @param number + +local function outline_workarea(width, height) + local x, y = 0, 0 + local size = math.min(width, height) + + return {x=x+(width-size)/2, y=y+(height-size)/2, width=size, height=size} +end + +-- The child widget area +local function content_workarea(self, width, height) + local padding = self._private.paddings or {} + local border_width = self:get_border_width() or 0 + local wa = outline_workarea(width, height) + local thickness = math.max(border_width, self:get_thickness() or 5) + + wa.x = wa.x + (padding.left or 0) + thickness + 2*border_width + wa.y = wa.y + (padding.top or 0) + thickness + 2*border_width + wa.width = wa.width - (padding.left or 0) - (padding.right or 0) + - 2*thickness - 4*border_width + wa.height = wa.height - (padding.top or 0) - (padding.bottom or 0) + - 2*thickness - 4*border_width + + return wa +end + +-- Draw the radial outline and progress +function arcchart:after_draw_children(_, cr, width, height) + cr:restore() + + local values = self:get_values() or {} + local border_width = self:get_border_width() or 0 + local thickness = math.max(border_width, self:get_thickness() or 5) + + local offset = thickness + 2*border_width + + -- Draw a circular background + local bg = self:get_bg() + if bg then + cr:save() + cr:translate(offset/2, offset/2) + shape.circle( + cr, + width-offset, + height-offset + ) + cr:set_line_width(thickness+2*border_width) + cr:set_source(color(bg)) + cr:stroke() + cr:restore() + end + + if #values == 0 then + return + end + + local wa = outline_workarea(width, height) + cr:translate(wa.x+border_width/2, wa.y+border_width/2) + + + -- Get the min and max value + --local min_val = self:get_min_value() or 0 --TODO support min_values + local max_val = self:get_max_value() + local sum = 0 + + if not max_val then + for _, v in ipairs(values) do + sum = sum + v + end + max_val = sum + end + + max_val = math.max(max_val, sum) + + local use_rounded_edges = sum ~= max_val and self:get_rounded_edge() + + -- Fallback to the current foreground color + local colors = self:get_colors() or {} + + -- Draw the outline + local offset_angle = self:get_start_angle() or math.pi + local start_angle, end_angle = offset_angle, offset_angle + + for k, v in ipairs(values) do + end_angle = start_angle + (v*2*math.pi) / max_val + + if colors[k] then + cr:set_source(color(colors[k])) + end + + shape.arc(cr, wa.width-border_width, wa.height-border_width, + thickness+border_width, math.pi-end_angle, math.pi-start_angle, + (use_rounded_edges and k == 1), (use_rounded_edges and k == #values) + ) + + cr:fill() + start_angle = end_angle + end + + if border_width > 0 then + local border_color = self:get_border_color() + + cr:set_source(color(border_color)) + cr:set_line_width(border_width) + + shape.arc(cr, wa.width-border_width, wa.height-border_width, + thickness+border_width, math.pi-end_angle, math.pi-offset_angle, + use_rounded_edges, use_rounded_edges + ) + cr:stroke() + end + +end + +-- Set the clip +function arcchart:before_draw_children(_, cr, width, height) + cr:save() + local wa = content_workarea(self, width, height) + cr:translate(wa.x, wa.y) + shape.circle( + cr, + wa.width, + wa.height + ) + cr:clip() + cr:translate(-wa.x, -wa.y) +end + +-- Layout this layout +function arcchart:layout(_, width, height) + if self._private.widget then + local wa = content_workarea(self, width, height) + + return { base.place_widget_at( + self._private.widget, wa.x, wa.y, wa.width, wa.height + ) } + end +end + +-- Fit this layout into the given area +function arcchart:fit(_, width, height) + local size = math.min(width, height) + return size, size +end + +--- The widget to wrap in a radial proggressbar. +-- @property widget +-- @tparam widget widget The widget + +function arcchart:set_widget(widget) + if widget then + base.check_widget(widget) + end + self._private.widget = widget + self:emit_signal("widget::layout_changed") +end + +--- Get the children elements. +-- @treturn table The children +function arcchart:get_children() + return {self._private.widget} +end + +--- Replace the layout children +-- This layout only accept one children, all others will be ignored +-- @tparam table children A table composed of valid widgets +function arcchart:set_children(children) + self._private.widget = children and children[1] + self:emit_signal("widget::layout_changed") +end + +--- Reset this layout. The widget will be removed and the rotation reset. +function arcchart:reset() + self:set_widget(nil) +end + +for _,v in ipairs {"left", "right", "top", "bottom"} do + arcchart["set_"..v.."_padding"] = function(self, val) + self._private.paddings = self._private.paddings or {} + self._private.paddings[v] = val + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + end +end + +--- The padding between the outline and the progressbar. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_arcchart_paddings.svg) +-- +-- @property paddings +-- @tparam[opt=0] table|number paddings A number or a table +-- @tparam[opt=0] number paddings.top +-- @tparam[opt=0] number paddings.bottom +-- @tparam[opt=0] number paddings.left +-- @tparam[opt=0] number paddings.right + +--- The border background color. +-- +-- @property border_color + +--- The border foreground color. +-- +-- @property color + +--- The border width. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_arcchart_border_width.svg) +-- +-- @property border_width +-- @tparam[opt=3] number border_width + +--- The minimum value. +-- @property min_value + +--- The maximum value. +-- @property max_value + +--- The radial background. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_arcchart_bg.svg) +-- +-- @property bg +-- @param color +-- @see gears.color + +--- The value. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_arcchart_value.svg) +-- +-- @property value +-- @tparam number value Between min_value and max_value +-- @see values + +--- The values. +-- The arcchart is designed to display multiple values at once. Each will be +-- shown in table order. +-- +-- @property values +-- @tparam table values An ordered set if values. +-- @see value + +--- If the chart has rounded edges. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_arcchart_rounded_edge.svg) +-- +-- @property rounded_edge +-- @param[opt=false] boolean + +--- The arc thickness. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_arcchart_thickness.svg) +-- +-- @property thickness +-- @param number + +--- The (radiant) angle where the first value start. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_arcchart_start_angle.svg) +-- +-- @property start_angle +-- @param[opt=math.pi] number A number between 0 and 2*math.pi + +for _, prop in ipairs {"border_width", "border_color", "paddings", "colors", + "rounded_edge", "bg", "thickness", "values", "min_value", "max_value", + "start_angle" } do + arcchart["set_"..prop] = function(self, value) + self._private[prop] = value + self:emit_signal("property::"..prop) + self:emit_signal("widget::redraw_needed") + end + arcchart["get_"..prop] = function(self) + return self._private[prop] or beautiful["arcchart_"..prop] + end +end + +function arcchart:set_paddings(val) + self._private.paddings = type(val) == "number" and { + left = val, + right = val, + top = val, + bottom = val, + } or val or {} + self:emit_signal("property::paddings") + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") +end + +function arcchart:set_value(value) + self:set_values {value} +end + +--- Returns a new arcchart layout. +-- @param[opt] widget The widget to display. +-- @function wibox.container.arcchart +local function new(widget) + local ret = base.make_widget(nil, nil, { + enable_properties = true, + }) + + util.table.crush(ret, arcchart) + + ret:set_widget(widget) + + return ret +end + +function arcchart.mt:__call(...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(arcchart, arcchart.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/container/background.lua b/lib/wibox/container/background.lua new file mode 100644 index 0000000..2fb88c3 --- /dev/null +++ b/lib/wibox/container/background.lua @@ -0,0 +1,626 @@ +--------------------------------------------------------------------------- +-- A container capable of changing the background color, foreground color +-- widget shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_defaults_background.svg) +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.container.background +--------------------------------------------------------------------------- + +local base = require("wibox.widget.base") +local color = require("gears.color") +local surface = require("gears.surface") +local beautiful = require("beautiful") +local cairo = require("lgi").cairo +local util = require("awful.util") +local setmetatable = setmetatable +local type = type +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +local background = { mt = {} } + +-- Draw this widget +function background:draw(context, cr, width, height) + if not self._private.widget or not self._private.widget:get_visible() then + return + end + + -- Keep the shape path in case there is a border + self._private.path = nil + + if self._private.shape then + -- Only add the offset if there is something to draw + local offset = ((self._private.shape_border_width and self._private.shape_border_color) + and self._private.shape_border_width or 0) / 2 + + cr:translate(offset, offset) + self._private.shape(cr, width - 2*offset, height - 2*offset, unpack(self._private.shape_args or {})) + cr:translate(-offset, -offset) + self._private.path = cr:copy_path() + cr:clip() + end + + if self._private.background then + cr:set_source(self._private.background) + cr:paint() + end + if self._private.bgimage then + if type(self._private.bgimage) == "function" then + self._private.bgimage(context, cr, width, height,unpack(self._private.bgimage_args)) + else + local pattern = cairo.Pattern.create_for_surface(self._private.bgimage) + cr:set_source(pattern) + cr:paint() + end + end + +end + +-- Draw the border +function background:after_draw_children(_, cr) + -- Draw the border + if self._private.path and self._private.shape_border_width and self._private.shape_border_width > 0 then + cr:append_path(self._private.path) + cr:set_source(color(self._private.shape_border_color or self._private.foreground or beautiful.fg_normal)) + + cr:set_line_width(self._private.shape_border_width) + cr:stroke() + self._private.path = nil + end +end + +-- Prepare drawing the children of this widget +function background:before_draw_children(_, cr) + if self._private.foreground then + cr:set_source(self._private.foreground) + end + + -- Clip the shape + if self._private.path and self._private.shape_clip then + cr:append_path(self._private.path) + cr:clip() + end +end + +-- Layout this widget +function background:layout(_, width, height) + if self._private.widget then + return { base.place_widget_at(self._private.widget, 0, 0, width, height) } + end +end + +-- Fit this widget into the given area +function background:fit(context, width, height) + if not self._private.widget then + return 0, 0 + end + + return base.fit_widget(self, context, self._private.widget, width, height) +end + +--- The widget displayed in the background widget. +-- @property widget +-- @tparam widget widget The widget to be disaplayed inside of the background +-- area + +function background:set_widget(widget) + if widget then + base.check_widget(widget) + end + self._private.widget = widget + self:emit_signal("widget::layout_changed") +end + +function background:get_widget() + return self._private.widget +end + +-- Get children element +-- @treturn table The children +function background:get_children() + return {self._private.widget} +end + +-- Replace the layout children +-- This layout only accept one children, all others will be ignored +-- @tparam table children A table composed of valid widgets +function background:set_children(children) + self:set_widget(children[1]) +end + +--- The background color/pattern/gradient to use. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_background_bg.svg) +-- +-- @usage +--local text_widget = { +-- text = 'Hello world!', +-- widget = wibox.widget.textbox +--} +--parent : setup { +-- { +-- text_widget, +-- bg = '#ff0000', +-- widget = wibox.container.background +-- }, +-- { +-- text_widget, +-- bg = '#00ff00', +-- widget = wibox.container.background +-- }, +-- { +-- text_widget, +-- bg = '#0000ff', +-- widget = wibox.container.background +-- }, +-- spacing = 10, +-- layout = wibox.layout.fixed.vertical +--} +-- @property bg +-- @param bg A color string, pattern or gradient +-- @see gears.color + +function background:set_bg(bg) + if bg then + self._private.background = color(bg) + else + self._private.background = nil + end + self:emit_signal("widget::redraw_needed") +end + +function background:get_bg() + return self._private.background +end + +--- The foreground (text) color/pattern/gradient to use. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_background_fg.svg) +-- +-- @usage +--local text_widget = { +-- text = 'Hello world!', +-- widget = wibox.widget.textbox +--} +--parent : setup { +-- { +-- text_widget, +-- fg = '#ff0000', +-- widget = wibox.container.background +-- }, +-- { +-- text_widget, +-- fg = '#00ff00', +-- widget = wibox.container.background +-- }, +-- { +-- text_widget, +-- fg = '#0000ff', +-- widget = wibox.container.background +-- }, +-- spacing = 10, +-- layout = wibox.layout.fixed.vertical +--} +-- @property fg +-- @param fg A color string, pattern or gradient +-- @see gears.color + +function background:set_fg(fg) + if fg then + self._private.foreground = color(fg) + else + self._private.foreground = nil + end + self:emit_signal("widget::redraw_needed") +end + +function background:get_fg() + return self._private.foreground +end + +--- The background shap e. +-- +-- Use `set_shape` to set additional shape paramaters. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_background_shape.svg) +-- +-- @usage +--parent : setup { +-- { +-- -- Adding a shape without margin may result in cropped output +-- { +-- text = 'Hello world!', +-- widget = wibox.widget.textbox +-- }, +-- shape = gears.shape.hexagon, +-- bg = beautiful.bg_normal, +-- shape_border_color = beautiful.border_color, +-- shape_border_width = beautiful.border_width, +-- widget = wibox.container.background +-- }, +-- { +-- -- To solve this, use a margin +-- { +-- { +-- text = 'Hello world!', +-- widget = wibox.widget.textbox +-- }, +-- left = 10, +-- right = 10, +-- top = 3, +-- bottom = 3, +-- widget = wibox.container.margin +-- }, +-- shape = gears.shape.hexagon, +-- bg = beautiful.bg_normal, +-- shape_border_color = beautiful.border_color, +-- shape_border_width = beautiful.border_width, +-- widget = wibox.container.background +-- }, +-- spacing = 10, +-- layout = wibox.layout.fixed.vertical +--} +-- @property shape +-- @param shape A function taking a context, width and height as arguments +-- @see gears.shape +-- @see set_shape + +--- Set the background shape. +-- +-- Any other arguments will be passed to the shape function +-- @param shape A function taking a context, width and height as arguments +-- @see gears.shape +-- @see shape +function background:set_shape(shape, ...) + local args = {...} + + if shape == self._private.shape and #args == 0 then return end + + self._private.shape = shape + self._private.shape_args = {...} + self:emit_signal("widget::redraw_needed") +end + +function background:get_shape() + return self._private.shape +end + +--- When a `shape` is set, also draw a border. +-- +-- See `wibox.container.background.shape` for an usage example. +-- @property shape_border_width +-- @tparam number width The border width + +function background:set_shape_border_width(width) + if self._private.shape_border_width == width then return end + + self._private.shape_border_width = width + self:emit_signal("widget::redraw_needed") +end + +function background:get_shape_border_width() + return self._private.shape_border_width +end + +--- When a `shape` is set, also draw a border. +-- +-- See `wibox.container.background.shape` for an usage example. +-- @property shape_border_color +-- @param[opt=self._private.foreground] fg The border color, pattern or gradient +-- @see gears.color + +function background:set_shape_border_color(fg) + if self._private.shape_border_color == fg then return end + + self._private.shape_border_color = fg + self:emit_signal("widget::redraw_needed") +end + +function background:get_shape_border_color() + return self._private.shape_border_color +end + +--- When a `shape` is set, make sure nothing is drawn outside of it. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_background_clip.svg) +-- +-- @usage +--parent : setup { +-- { +-- -- Some content may be outside of the shape +-- { +-- text = 'Hello\nworld!', +-- widget = wibox.widget.textbox +-- }, +-- shape = gears.shape.circle, +-- bg = beautiful.bg_normal, +-- shape_border_color = beautiful.border_color, +-- widget = wibox.container.background +-- }, +-- { +-- -- To solve this, clip the content +-- { +-- text = 'Hello\nworld!', +-- widget = wibox.widget.textbox +-- }, +-- shape_clip = true, +-- shape = gears.shape.circle, +-- bg = beautiful.bg_normal, +-- shape_border_color = beautiful.border_color, +-- widget = wibox.container.background +-- }, +-- spacing = 10, +-- layout = wibox.layout.fixed.vertical +--} +-- @property shape_clip +-- @tparam boolean value If the shape clip is enable + +function background:set_shape_clip(value) + if self._private.shape_clip == value then return end + + self._private.shape_clip = value + self:emit_signal("widget::redraw_needed") +end + +function background:get_shape_clip() + return self._private.shape_clip or false +end + +--- The background image to use +-- If `image` is a function, it will be called with `(context, cr, width, height)` +-- as arguments. Any other arguments passed to this method will be appended. +-- @property bgimage +-- @param image A background image or a function +-- @see gears.surface + +function background:set_bgimage(image, ...) + self._private.bgimage = type(image) == "function" and image or surface.load(image) + self._private.bgimage_args = {...} + self:emit_signal("widget::redraw_needed") +end + +function background:get_bgimage() + return self._private.bgimage +end + +--- Returns a new background container. +-- +-- A background container applies a background and foreground color +-- to another widget. +-- @param[opt] widget The widget to display. +-- @param[opt] bg The background to use for that widget. +-- @param[opt] shape A `gears.shape` compatible shape function +-- @function wibox.container.background +local function new(widget, bg, shape) + local ret = base.make_widget(nil, nil, { + enable_properties = true, + }) + + util.table.crush(ret, background, true) + + ret._private.shape = shape + + ret:set_widget(widget) + ret:set_bg(bg) + + return ret +end + +function background.mt:__call(...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(background, background.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/container/constraint.lua b/lib/wibox/container/constraint.lua new file mode 100644 index 0000000..a210387 --- /dev/null +++ b/lib/wibox/container/constraint.lua @@ -0,0 +1,371 @@ +--------------------------------------------------------------------------- +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_defaults_constraint.svg) +-- +-- @author Lukáš Hrázký +-- @copyright 2012 Lukáš Hrázký +-- @classmod wibox.container.constraint +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local base = require("wibox.widget.base") +local util = require("awful.util") +local math = math + +local constraint = { mt = {} } + +-- Layout a constraint layout +function constraint:layout(_, width, height) + if self._private.widget then + return { base.place_widget_at(self._private.widget, 0, 0, width, height) } + end +end + +-- Fit a constraint layout into the given space +function constraint:fit(context, width, height) + local w, h + if self._private.widget then + w = self._private.strategy(width, self._private.width) + h = self._private.strategy(height, self._private.height) + + w, h = base.fit_widget(self, context, self._private.widget, w, h) + else + w, h = 0, 0 + end + + w = self._private.strategy(w, self._private.width) + h = self._private.strategy(h, self._private.height) + + return w, h +end + +--- The widget to be constrained. +-- @property widget +-- @tparam widget widget The widget + +function constraint:set_widget(widget) + self._private.widget = widget + self:emit_signal("widget::layout_changed") +end + +function constraint:get_widget() + return self._private.widget +end + +--- Get the number of children element +-- @treturn table The children +function constraint:get_children() + return {self._private.widget} +end + +--- Replace the layout children +-- This layout only accept one children, all others will be ignored +-- @tparam table children A table composed of valid widgets +function constraint:set_children(children) + self:set_widget(children[1]) +end + +--- Set the strategy to use for the constraining. Valid values are 'max', +-- 'min' or 'exact'. Throws an error on invalid values. +-- @property strategy + +function constraint:set_strategy(val) + local func = { + min = function(real_size, limit) + return limit and math.max(limit, real_size) or real_size + end, + max = function(real_size, limit) + return limit and math.min(limit, real_size) or real_size + end, + exact = function(real_size, limit) + return limit or real_size + end + } + + if not func[val] then + error("Invalid strategy for constraint layout: " .. tostring(val)) + end + + self._private.strategy = func[val] + self:emit_signal("widget::layout_changed") +end + +function constraint:get_strategy() + return self._private.strategy +end + +--- Set the maximum width to val. nil for no width limit. +-- @property height +-- @param number + +function constraint:set_width(val) + self._private.width = val + self:emit_signal("widget::layout_changed") +end + +function constraint:get_width() + return self._private.width +end + +--- Set the maximum height to val. nil for no height limit. +-- @property width +-- @param number + +function constraint:set_height(val) + self._private.height = val + self:emit_signal("widget::layout_changed") +end + +function constraint:get_height() + return self._private.height +end + +--- Reset this layout. The widget will be unreferenced, strategy set to "max" +-- and the constraints set to nil. +function constraint:reset() + self._private.width = nil + self._private.height = nil + self:set_strategy("max") + self:set_widget(nil) +end + +--- Returns a new constraint container. +-- This container will constraint the size of a +-- widget according to the strategy. Note that this will only work for layouts +-- that respect the widget's size, eg. fixed layout. In layouts that don't +-- (fully) respect widget's requested size, the inner widget still might get +-- drawn with a size that does not fit the constraint, eg. in flex layout. +-- @param[opt] widget A widget to use. +-- @param[opt] strategy How to constraint the size. 'max' (default), 'min' or +-- 'exact'. +-- @param[opt] width The maximum width of the widget. nil for no limit. +-- @param[opt] height The maximum height of the widget. nil for no limit. +-- @treturn table A new constraint container +-- @function wibox.container.constraint +local function new(widget, strategy, width, height) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, constraint, true) + + ret:set_strategy(strategy or "max") + ret:set_width(width) + ret:set_height(height) + + if widget then + ret:set_widget(widget) + end + + return ret +end + +function constraint.mt:__call(...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(constraint, constraint.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/container/init.lua b/lib/wibox/container/init.lua new file mode 100644 index 0000000..93158ab --- /dev/null +++ b/lib/wibox/container/init.lua @@ -0,0 +1,21 @@ +--------------------------------------------------------------------------- +--- Collection of containers that can be used in widget boxes +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.container +--------------------------------------------------------------------------- +local base = require("wibox.widget.base") + +return setmetatable({ + rotate = require("wibox.container.rotate"); + margin = require("wibox.container.margin"); + mirror = require("wibox.container.mirror"); + constraint = require("wibox.container.constraint"); + scroll = require("wibox.container.scroll"); + background = require("wibox.container.background"); + radialprogressbar = require("wibox.container.radialprogressbar"); + arcchart = require("wibox.container.arcchart"); +}, {__call = function(_, args) return base.make_widget_declarative(args) end}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/container/margin.lua b/lib/wibox/container/margin.lua new file mode 100644 index 0000000..edf9673 --- /dev/null +++ b/lib/wibox/container/margin.lua @@ -0,0 +1,419 @@ +--------------------------------------------------------------------------- +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_defaults_margin.svg) +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.container.margin +--------------------------------------------------------------------------- + +local pairs = pairs +local setmetatable = setmetatable +local base = require("wibox.widget.base") +local gcolor = require("gears.color") +local cairo = require("lgi").cairo +local util = require("awful.util") + +local margin = { mt = {} } + +-- Draw a margin layout +function margin:draw(_, cr, width, height) + local x = self._private.left + local y = self._private.top + local w = self._private.right + local h = self._private.bottom + local color = self._private.color + + if not self._private.widget or width <= x + w or height <= y + h then + return + end + + if color then + cr:set_source(color) + cr:rectangle(0, 0, width, height) + cr:rectangle(x, y, width - x - w, height - y - h) + cr:set_fill_rule(cairo.FillRule.EVEN_ODD) + cr:fill() + end +end + +-- Layout a margin layout +function margin:layout(_, width, height) + if self._private.widget then + local x = self._private.left + local y = self._private.top + local w = self._private.right + local h = self._private.bottom + + return { base.place_widget_at(self._private.widget, x, y, width - x - w, height - y - h) } + end +end + +-- Fit a margin layout into the given space +function margin:fit(context, width, height) + local extra_w = self._private.left + self._private.right + local extra_h = self._private.top + self._private.bottom + local w, h = 0, 0 + if self._private.widget then + w, h = base.fit_widget(self, context, self._private.widget, width - extra_w, height - extra_h) + end + + if self._private.draw_empty == false and (w == 0 or h == 0) then + return 0, 0 + end + + return w + extra_w, h + extra_h +end + +--- The widget to be wrapped the the margins. +-- @property widget +-- @tparam widget widget The widget + +function margin:set_widget(widget) + if widget then + base.check_widget(widget) + end + self._private.widget = widget + self:emit_signal("widget::layout_changed") +end + +function margin:get_widget() + return self._private.widget +end + +-- Get the number of children element +-- @treturn table The children +function margin:get_children() + return {self._private.widget} +end + +-- Replace the layout children +-- This layout only accept one children, all others will be ignored +-- @tparam table children A table composed of valid widgets +function margin:set_children(children) + self:set_widget(children[1]) +end + +--- Set all the margins to val. +-- @property margins +-- @tparam number val The margin value + +function margin:set_margins(val) + if self._private.left == val and + self._private.right == val and + self._private.top == val and + self._private.bottom == val then + return + end + + self._private.left = val + self._private.right = val + self._private.top = val + self._private.bottom = val + self:emit_signal("widget::layout_changed") +end + +--- Set the margins color to create a border. +-- @property color +-- @param color A color used to fill the margin. + +function margin:set_color(color) + self._private.color = color and gcolor(color) + self:emit_signal("widget::redraw_needed") +end + +function margin:get_color() + return self._private.color +end + +--- Draw the margin even if the content size is 0x0 (default: true) +-- @function draw_empty +-- @tparam boolean draw_empty Draw nothing is content is 0x0 or draw the margin anyway + +function margin:set_draw_empty(draw_empty) + self._private.draw_empty = draw_empty + self:emit_signal("widget::layout_changed") +end + +function margin:get_draw_empty() + return self._private.draw_empty +end + +--- Reset this layout. The widget will be unreferenced, the margins set to 0 +-- and the color erased +function margin:reset() + self:set_widget(nil) + self:set_margins(0) + self:set_color(nil) +end + +--- Set the left margin that this layout adds to its widget. +-- @param margin The new margin to use. +-- @property left + +--- Set the right margin that this layout adds to its widget. +-- @param margin The new margin to use. +-- @property right + +--- Set the top margin that this layout adds to its widget. +-- @param margin The new margin to use. +-- @property top + +--- Set the bottom margin that this layout adds to its widget. +-- @param margin The new margin to use. +-- @property bottom + +-- Create setters for each direction +for _, v in pairs({ "left", "right", "top", "bottom" }) do + margin["set_" .. v] = function(layout, val) + if layout._private[v] == val then return end + layout._private[v] = val + layout:emit_signal("widget::layout_changed") + end + + margin["get_" .. v] = function(layout) + return layout._private[v] + end +end + +--- Returns a new margin container. +-- @param[opt] widget A widget to use. +-- @param[opt] left A margin to use on the left side of the widget. +-- @param[opt] right A margin to use on the right side of the widget. +-- @param[opt] top A margin to use on the top side of the widget. +-- @param[opt] bottom A margin to use on the bottom side of the widget. +-- @param[opt] color A color for the margins. +-- @param[opt] draw_empty whether or not to draw the margin when the content is empty +-- @treturn table A new margin container +-- @function wibox.container.margin +local function new(widget, left, right, top, bottom, color, draw_empty) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, margin, true) + + ret:set_left(left or 0) + ret:set_right(right or 0) + ret:set_top(top or 0) + ret:set_bottom(bottom or 0) + ret:set_draw_empty(draw_empty) + + ret:set_color(color) + + if widget then + ret:set_widget(widget) + end + + return ret +end + +function margin.mt:__call(...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(margin, margin.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/container/mirror.lua b/lib/wibox/container/mirror.lua new file mode 100644 index 0000000..fc275cf --- /dev/null +++ b/lib/wibox/container/mirror.lua @@ -0,0 +1,340 @@ +--------------------------------------------------------------------------- +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_defaults_mirror.svg) +-- +-- @author dodo +-- @copyright 2012 dodo +-- @classmod wibox.container.mirror +--------------------------------------------------------------------------- + +local type = type +local error = error +local ipairs = ipairs +local setmetatable = setmetatable +local base = require("wibox.widget.base") +local matrix = require("gears.matrix") +local util = require("awful.util") + +local mirror = { mt = {} } + +-- Layout this layout +function mirror:layout(_, width, height) + if not self._private.widget then return end + + local m = matrix.identity + local t = { x = 0, y = 0 } -- translation + local s = { x = 1, y = 1 } -- scale + if self._private.horizontal then + t.x = width + s.x = -1 + end + if self._private.vertical then + t.y = height + s.y = -1 + end + m = m:translate(t.x, t.y) + m = m:scale(s.x, s.y) + + return { base.place_widget_via_matrix(self._private.widget, m, width, height) } +end + +-- Fit this layout into the given area +function mirror:fit(context, ...) + if not self._private.widget then + return 0, 0 + end + return base.fit_widget(self, context, self._private.widget, ...) +end + +--- The widget to be reflected. +-- @property widget +-- @tparam widget widget The widget + +function mirror:set_widget(widget) + if widget then + base.check_widget(widget) + end + self._private.widget = widget + self:emit_signal("widget::layout_changed") +end + +function mirror:get_widget() + return self._private.widget +end + +--- Get the number of children element +-- @treturn table The children +function mirror:get_children() + return {self._private.widget} +end + +--- Replace the layout children +-- This layout only accept one children, all others will be ignored +-- @tparam table children A table composed of valid widgets +function mirror:set_children(children) + self:set_widget(children[1]) +end + +--- Reset this layout. The widget will be removed and the axes reset. +function mirror:reset() + self._private.horizontal = false + self._private.vertical = false + self:set_widget(nil) +end + +function mirror:set_reflection(reflection) + if type(reflection) ~= 'table' then + error("Invalid type of reflection for mirror layout: " .. + type(reflection) .. " (should be a table)") + end + for _, ref in ipairs({"horizontal", "vertical"}) do + if reflection[ref] ~= nil then + self._private[ref] = reflection[ref] + end + end + self:emit_signal("widget::layout_changed") +end + +--- Get the reflection of this mirror layout. +-- @property reflection +-- @param table reflection A table of booleans with the keys "horizontal", "vertical". +-- @param boolean reflection.horizontal +-- @param boolean reflection.vertical + +function mirror:get_reflection() + return { horizontal = self._private.horizontal, vertical = self._private.vertical } +end + +--- Returns a new mirror container. +-- A mirror container mirrors a given widget. Use +-- `:set_widget()` to set the widget and +-- `:set_horizontal()` and `:set_vertical()` for the direction. +-- horizontal and vertical are by default false which doesn't change anything. +-- @param[opt] widget The widget to display. +-- @param[opt] reflection A table describing the reflection to apply. +-- @treturn table A new mirror container +-- @function wibox.container.mirror +local function new(widget, reflection) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + ret._private.horizontal = false + ret._private.vertical = false + + util.table.crush(ret, mirror, true) + + ret:set_widget(widget) + ret:set_reflection(reflection or {}) + + return ret +end + +function mirror.mt:__call(...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(mirror, mirror.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/container/radialprogressbar.lua b/lib/wibox/container/radialprogressbar.lua new file mode 100644 index 0000000..7c67906 --- /dev/null +++ b/lib/wibox/container/radialprogressbar.lua @@ -0,0 +1,481 @@ +--------------------------------------------------------------------------- +-- +-- A circular progressbar wrapper. +-- +-- If no child `widget` is set, then the radialprogressbar will take all the +-- available size. Use a `wibox.container.constraint` to prevent this. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_defaults_radialprogressbar.svg) +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2013 Emmanuel Lepage Vallee +-- @classmod wibox.container.radialprogressbar +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local base = require("wibox.widget.base") +local shape = require("gears.shape" ) +local util = require( "awful.util" ) +local color = require( "gears.color" ) +local beautiful = require("beautiful" ) + +local default_outline_width = 2 + +local radialprogressbar = { mt = {} } + +--- The progressbar border background color. +-- @beautiful beautiful.radialprogressbar_border_color + +--- The progressbar foreground color. +-- @beautiful beautiful.radialprogressbar_color + +--- The progressbar border width. +-- @beautiful beautiful.radialprogressbar_border_width + +--- The padding between the outline and the progressbar. +-- @beautiful beautiful.radialprogressbar_paddings +-- @tparam[opt=0] table|number paddings A number or a table +-- @tparam[opt=0] number paddings.top +-- @tparam[opt=0] number paddings.bottom +-- @tparam[opt=0] number paddings.left +-- @tparam[opt=0] number paddings.right + +local function outline_workarea(self, width, height) + local border_width = self._private.border_width or + beautiful.radialprogressbar_border_width or default_outline_width + + local x, y = 0, 0 + + -- Make sure the border fit in the clip area + local offset = border_width/2 + x, y = x + offset, y+offset + width, height = width-2*offset, height-2*offset + + return {x=x, y=y, width=width, height=height}, offset +end + +-- The child widget area +local function content_workarea(self, width, height) + local padding = self._private.paddings or {} + local wa = outline_workarea(self, width, height) + + wa.x = wa.x + (padding.left or 0) + wa.y = wa.y + (padding.top or 0) + wa.width = wa.width - (padding.left or 0) - (padding.right or 0) + wa.height = wa.height - (padding.top or 0) - (padding.bottom or 0) + + return wa +end + +-- Draw the radial outline and progress +function radialprogressbar:after_draw_children(_, cr, width, height) + cr:restore() + + local border_width = self._private.border_width or + beautiful.radialprogressbar_border_width or default_outline_width + + local wa = outline_workarea(self, width, height) + cr:translate(wa.x, wa.y) + + -- Draw the outline + shape.rounded_bar(cr, wa.width, wa.height) + cr:set_source(color(self:get_border_color() or "#0000ff")) + cr:set_line_width(border_width) + cr:stroke() + + -- Draw the progress + cr:set_source(color(self:get_color() or "#ff00ff")) + shape.radial_progress(cr, wa.width, wa.height, self._percent or 0) + cr:set_line_width(border_width) + cr:stroke() + +end + +-- Set the clip +function radialprogressbar:before_draw_children(_, cr, width, height) + cr:save() + local wa = content_workarea(self, width, height) + cr:translate(wa.x, wa.y) + shape.rounded_bar(cr, wa.width, wa.height) + cr:clip() + cr:translate(-wa.x, -wa.y) +end + +-- Layout this layout +function radialprogressbar:layout(_, width, height) + if self._private.widget then + local wa = content_workarea(self, width, height) + + return { base.place_widget_at( + self._private.widget, wa.x, wa.y, wa.width, wa.height + ) } + end +end + +-- Fit this layout into the given area +function radialprogressbar:fit(context, width, height) + if self._private.widget then + local wa = content_workarea(self, width, height) + local w, h = base.fit_widget(self, context, self._private.widget, wa.width, wa.height) + return wa.x + w, wa.y + h + end + + return width, height +end + +--- The widget to wrap in a radial proggressbar. +-- @property widget +-- @tparam widget widget The widget + +function radialprogressbar:set_widget(widget) + if widget then + base.check_widget(widget) + end + self._private.widget = widget + self:emit_signal("widget::layout_changed") +end + +--- Get the children elements +-- @treturn table The children +function radialprogressbar:get_children() + return {self._private.widget} +end + +--- Replace the layout children +-- This layout only accept one children, all others will be ignored +-- @tparam table children A table composed of valid widgets +function radialprogressbar:set_children(children) + self._private.widget = children and children[1] + self:emit_signal("widget::layout_changed") +end + +--- Reset this container. +function radialprogressbar:reset() + self:set_widget(nil) +end + +for _,v in ipairs {"left", "right", "top", "bottom"} do + radialprogressbar["set_"..v.."_padding"] = function(self, val) + self._private.paddings = self._private.paddings or {} + self._private.paddings[v] = val + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + end +end + +--- The padding between the outline and the progressbar. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_radialprogressbar_padding.svg) +-- +-- @property paddings +-- @tparam[opt=0] table|number paddings A number or a table +-- @tparam[opt=0] number paddings.top +-- @tparam[opt=0] number paddings.bottom +-- @tparam[opt=0] number paddings.left +-- @tparam[opt=0] number paddings.right + +--- The progressbar value. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_radialprogressbar_value.svg) +-- +-- @property value +-- @tparam number value Between min_value and max_value + +function radialprogressbar:set_value(val) + if not val then self._percent = 0; return end + + if val > self._private.max_value then + self:set_max_value(val) + elseif val < self._private.min_value then + self:set_min_value(val) + end + + local delta = self._private.max_value - self._private.min_value + + self._percent = val/delta + self:emit_signal("widget::redraw_needed") +end + +--- The border background color. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_radialprogressbar_border_color.svg) +-- +-- @property border_color + +--- The border foreground color. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_radialprogressbar_color.svg) +-- +-- @property color + +--- The border width. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_radialprogressbar_border_width.svg) +-- +-- @property border_width +-- @tparam[opt=3] number border_width + +--- The minimum value. +-- @property min_value + +--- The maximum value. +-- @property max_value + +for _, prop in ipairs {"max_value", "min_value", "border_color", "color", + "border_width", "paddings"} do + radialprogressbar["set_"..prop] = function(self, value) + self._private[prop] = value + self:emit_signal("property::"..prop) + self:emit_signal("widget::redraw_needed") + end + radialprogressbar["get_"..prop] = function(self) + return self._private[prop] or beautiful["radialprogressbar_"..prop] + end +end + +function radialprogressbar:set_paddings(val) + self._private.paddings = type(val) == "number" and { + left = val, + right = val, + top = val, + bottom = val, + } or val or {} + self:emit_signal("property::paddings") + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") +end + +--- Returns a new radialprogressbar layout. A radialprogressbar layout +-- radialprogressbars a given widget. Use `.widget` to set the widget. +-- @param[opt] widget The widget to display. +-- @function wibox.container.radialprogressbar +local function new(widget) + local ret = base.make_widget(nil, nil, { + enable_properties = true, + }) + + util.table.crush(ret, radialprogressbar) + ret._private.max_value = 1 + ret._private.min_value = 0 + + ret:set_widget(widget) + + return ret +end + +function radialprogressbar.mt:__call(_, ...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(radialprogressbar, radialprogressbar.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/container/rotate.lua b/lib/wibox/container/rotate.lua new file mode 100644 index 0000000..5fe4a8e --- /dev/null +++ b/lib/wibox/container/rotate.lua @@ -0,0 +1,384 @@ +--------------------------------------------------------------------------- +-- A container rotating the conained widget by 90 degrees. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_defaults_rotate.svg) +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.container.rotate +--------------------------------------------------------------------------- + +local error = error +local pi = math.pi +local setmetatable = setmetatable +local tostring = tostring +local base = require("wibox.widget.base") +local matrix = require("gears.matrix") +local util = require("awful.util") + +local rotate = { mt = {} } + +local function transform(layout, width, height) + local dir = layout:get_direction() + if dir == "east" or dir == "west" then + return height, width + end + return width, height +end + +-- Layout this layout +function rotate:layout(_, width, height) + if not self._private.widget or not self._private.widget._private.visible then + return + end + + local dir = self:get_direction() + + local m = matrix.identity + if dir == "west" then + m = m:rotate(pi / 2) + m = m:translate(0, -width) + elseif dir == "south" then + m = m:rotate(pi) + m = m:translate(-width, -height) + elseif dir == "east" then + m = m:rotate(3 * pi / 2) + m = m:translate(-height, 0) + end + + -- Since we rotated, we might have to swap width and height. + -- transform() does that for us. + return { base.place_widget_via_matrix(self._private.widget, m, transform(self, width, height)) } +end + +-- Fit this layout into the given area +function rotate:fit(context, width, height) + if not self._private.widget then + return 0, 0 + end + return transform(self, base.fit_widget(self, context, self._private.widget, transform(self, width, height))) +end + +--- The widget to be rotated. +-- @property widget +-- @tparam widget widget The widget + +function rotate:set_widget(widget) + if widget then + base.check_widget(widget) + end + self._private.widget = widget + self:emit_signal("widget::layout_changed") +end + +function rotate:get_widget() + return self._private.widget +end + +--- Get the number of children element +-- @treturn table The children +function rotate:get_children() + return {self._private.widget} +end + +--- Replace the layout children +-- This layout only accept one children, all others will be ignored +-- @tparam table children A table composed of valid widgets +function rotate:set_children(children) + self:set_widget(children[1]) +end + +--- Reset this layout. The widget will be removed and the rotation reset. +function rotate:reset() + self._private.direction = nil + self:set_widget(nil) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +--- The direction of this rotating container. +-- Valid values are: +-- +-- * *north* +-- * *east* +-- * *south* +-- * *north* +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_container_rotate_angle.svg) +-- +-- @usage +--local normal = create_arrow('Normal') +--local north = wibox.container { +-- create_arrow('North'), +-- direction = 'north', +-- widget = wibox.container.rotate +--} +--local south = wibox.container { +-- create_arrow('South'), +-- direction = 'south', +-- widget = wibox.container.rotate +--} +--local east = wibox.container { +-- create_arrow('East'), +-- direction = 'east', +-- widget = wibox.container.rotate +--} +--local west = wibox.container { +-- create_arrow('West'), +-- direction = 'west', +-- widget = wibox.container.rotate +--} +-- @property direction +-- @tparam string dir The direction + +function rotate:set_direction(dir) + local allowed = { + north = true, + east = true, + south = true, + west = true + } + + if not allowed[dir] then + error("Invalid direction for rotate layout: " .. tostring(dir)) + end + + self._private.direction = dir + self:emit_signal("widget::layout_changed") +end + +--- Get the direction of this rotating layout +function rotate:get_direction() + return self._private.direction or "north" +end + +--- Returns a new rotate container. +-- A rotate container rotates a given widget. Use +-- :set_widget() to set the widget and :set_direction() for the direction. +-- The default direction is "north" which doesn't change anything. +-- @param[opt] widget The widget to display. +-- @param[opt] dir The direction to rotate to. +-- @treturn table A new rotate container. +-- @function wibox.container.rotate +local function new(widget, dir) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, rotate, true) + + ret:set_widget(widget) + ret:set_direction(dir or "north") + + return ret +end + +function rotate.mt:__call(...) + return new(...) +end + +return setmetatable(rotate, rotate.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/container/scroll.lua b/lib/wibox/container/scroll.lua new file mode 100644 index 0000000..0fb2ad5 --- /dev/null +++ b/lib/wibox/container/scroll.lua @@ -0,0 +1,716 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter (based on ideas from Saleur Geoffrey) +-- @copyright 2015 Uli Schlachter +-- @classmod wibox.container.scroll +--------------------------------------------------------------------------- + +local cache = require("gears.cache") +local timer = require("gears.timer") +local hierarchy = require("wibox.hierarchy") +local base = require("wibox.widget.base") +local lgi = require("lgi") +local GLib = lgi.GLib + +local scroll = {} +local scroll_mt = { __index = scroll } +local _need_scroll_redraw + +-- "Strip" a context so that we can use it for our own drawing +local function cleanup_context(context) + local skip = { wibox = true, drawable = true, client = true, position = true } + local res = {} + for k, v in pairs(context) do + if not skip[k] then + res[k] = v + end + end + return res +end + +-- Create a hierarchy (and some more stuff) for drawing the given widget. This +-- allows "some stuff" to be re-used instead of re-created all the time. +local hierarchy_cache = cache.new(function(context, widget, width, height) + context = cleanup_context(context) + local layouts = setmetatable({}, { __mode = "k" }) + + -- Create a widget hierarchy and update when needed + local hier + local function do_pending_updates(layout) + layouts[layout] = true + hier:update(context, widget, width, height, nil) + end + local function emit(signal) + -- Make the scroll layouts redraw + for w in pairs(layouts) do + w:emit_signal(signal) + end + end + local function redraw_callback() + emit("widget::redraw_needed") + end + local function layout_callback() + emit("widget::redraw_needed") + emit("widget::layout_changed") + end + hier = hierarchy.new(context, widget, width, height, redraw_callback, layout_callback, nil) + + return hier, do_pending_updates, context +end) + +--- Calculate all the information needed for scrolling. +-- @param self The instance of the scrolling layout. +-- @param context A widget context under which we are fit/drawn. +-- @param width The available width +-- @param height The available height +-- @return A table with the following entries +-- @field fit_width The width that should be returned from :fit +-- @field fit_height The height that should be returned from :fit +-- @field surface_width The width for showing the child widget +-- @field surface_height The height for showing the child widget +-- @field first_x The x offset for drawing the child the first time +-- @field first_y The y offset for drawing the child the first time +-- @field[opt] second_x The x offset for drawing the child the second time +-- @field[opt] second_y The y offset for drawing the child the second time +-- @field hierarchy The wibox.hierarchy instance representing "everything" +-- @field context The widget context for drawing the hierarchy +local function calculate_info(self, context, width, height) + local result = {} + assert(self._private.widget) + + -- First, get the size of the widget (and the size of extra space) + local surface_width, surface_height = width, height + local extra_width, extra_height, extra = 0, 0, self._private.expand and self._private.extra_space or 0 + local w, h + if self._private.dir == "h" then + w, h = base.fit_widget(self, context, self._private.widget, self._private.space_for_scrolling, height) + surface_width = w + extra_width = extra + else + w, h = base.fit_widget(self, context, self._private.widget, width, self._private.space_for_scrolling) + surface_height = h + extra_height = extra + end + result.fit_width, result.fit_height = w, h + if self._private.dir == "h" then + if self._private.max_size then + result.fit_width = math.min(w, self._private.max_size) + end + else + if self._private.max_size then + result.fit_height = math.min(h, self._private.max_size) + end + end + if w > width or h > height then + -- There is less space available than we need, we have to scroll + _need_scroll_redraw(self) + + surface_width, surface_height = surface_width + extra_width, surface_height + extra_height + + local x, y = 0, 0 + local function get_scroll_offset(size, visible_size) + return self._private.step_function(self._private.timer:elapsed(), size, visible_size, self._private.speed, self._private.extra_space) + end + if self._private.dir == "h" then + x = -get_scroll_offset(surface_width - extra, width) + else + y = -get_scroll_offset(surface_height - extra, height) + end + result.first_x, result.first_y = x, y + -- Was the extra space already included elsewhere? + local extra_spacer = self._private.expand and 0 or self._private.extra_space + if self._private.dir == "h" then + x = x + surface_width + extra_spacer + else + y = y + surface_height + extra_spacer + end + result.second_x, result.second_y = x, y + else + result.first_x, result.first_y = 0, 0 + end + result.surface_width, result.surface_height = surface_width, surface_height + + -- Get the hierarchy and subscribe ourselves to updates + local hier, do_pending_updates, ctx = hierarchy_cache:get(context, + self._private.widget, surface_width, surface_height) + result.hierarchy = hier + result.context = ctx + do_pending_updates(self) + + return result +end + +-- Draw this scrolling layout. +-- @param context The context in which we are drawn. +-- @param cr The cairo context to draw to. +-- @param width The available width. +-- @param height The available height. +function scroll:draw(context, cr, width, height) + if not self._private.widget then + return + end + + local info = calculate_info(self, context, width, height) + + -- Draw the first instance of the child + cr:save() + cr:translate(info.first_x, info.first_y) + cr:rectangle(0, 0, info.surface_width, info.surface_height) + cr:clip() + info.hierarchy:draw(info.context, cr) + cr:restore() + + -- If there is one, draw the second instance (same code as above, minus the + -- clip) + if info.second_x and info.second_y then + cr:translate(info.second_x, info.second_y) + cr:rectangle(0, 0, info.surface_width, info.surface_height) + cr:clip() + info.hierarchy:draw(info.context, cr) + end +end + +-- Fit the scroll layout into the given space. +-- @param context The context in which we are fit. +-- @param width The available width. +-- @param height The available height. +function scroll:fit(context, width, height) + if not self._private.widget then + return 0, 0 + end + local info = calculate_info(self, context, width, height) + return info.fit_width, info.fit_height +end + +-- Internal function used for triggering redraws for scrolling. +-- The purpose is to start a timer for redrawing the widget for scrolling. +-- Redrawing works by simply emitting the `widget::redraw_needed` signal. +-- Pausing is implemented in this function: We just don't start a timer. +-- This function must be idempotent (calling it multiple times right after +-- another does not make a difference). +_need_scroll_redraw = function(self) + if not self._private.paused and not self._private.scroll_timer then + self._private.scroll_timer = timer.start_new(1 / self._private.fps, function() + self._private.scroll_timer = nil + self:emit_signal("widget::redraw_needed") + end) + end +end + +--- Pause the scrolling animation. +-- @see continue +function scroll:pause() + if self._private.paused then + return + end + self._private.paused = true + self._private.timer:stop() +end + +--- Continue the scrolling animation. +-- @see pause +function scroll:continue() + if not self._private.paused then + return + end + self._private.paused = false + self._private.timer:continue() + self:emit_signal("widget::redraw_needed") +end + +--- Reset the scrolling state to its initial condition. +-- For must scroll step functions, the effect of this function should be to +-- display the widget without any scrolling applied. +-- This function does not undo the effect of @{pause}. +function scroll:reset_scrolling() + self._private.timer:start() + if self._private.paused then + self._private.timer:stop() + end +end + +--- Set the direction in which this widget scroll. +-- @param dir Either "h" for horizontal scrolling or "v" for vertical scrolling +function scroll:set_direction(dir) + if dir == self._private.dir then + return + end + if dir ~= "h" and dir ~= "v" then + error("Invalid direction, can only be 'h' or 'v'") + end + self._private.dir = dir + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::redraw_needed") +end + +--- The widget to be scrolled. +-- @property widget +-- @tparam widget widget The widget + +function scroll:set_widget(widget) + if widget == self._private.widget then + return + end + if widget then + base.check_widget(widget) + end + self._private.widget = widget + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::redraw_needed") +end + +function scroll:get_widget() + return self._private.widget +end + +--- Get the number of children element +-- @treturn table The children +function scroll:get_children() + return {self._private.widget} +end + +--- Replace the layout children +-- This layout only accept one children, all others will be ignored +-- @tparam table children A table composed of valid widgets +function scroll:set_children(children) + self:set_widget(children[1]) +end + +--- Specify the expand mode that is used for extra space. +-- @tparam boolean expand If true, the widget is expanded to include the extra +-- space. If false, the extra space is simply left empty. +-- @see set_extra_space +function scroll:set_expand(expand) + if expand == self._private.expand then + return + end + self._private.expand = expand + self:emit_signal("widget::redraw_needed") +end + +--- Set the number of frames per second that this widget should draw. +-- @tparam number fps The number of frames per second +function scroll:set_fps(fps) + if fps == self._private.fps then + return + end + self._private.fps = fps + -- No signal needed: If we are scrolling, the next redraw will apply the new + -- FPS, else it obviously doesn't make a difference. +end + +--- Set the amount of extra space that should be included in the scrolling. This +-- extra space will likely be left empty between repetitions of the widgets. +-- @tparam number extra_space The amount of extra space +-- @see set_expand +function scroll:set_extra_space(extra_space) + if extra_space == self._private.extra_space then + return + end + self._private.extra_space = extra_space + self:emit_signal("widget::redraw_needed") +end + +--- Set the speed of the scrolling animation. The exact meaning depends on the +-- step function that is used, but for the simplest step functions, this will be +-- in pixels per second. +-- @tparam number speed The speed for the animation +function scroll:set_speed(speed) + if speed == self._private.speed then + return + end + self._private.speed = speed + self:emit_signal("widget::redraw_needed") +end + +--- Set the maximum size of this widget in the direction set by +-- @{set_direction}. If the child widget is smaller than this size, no scrolling +-- is done. If the child widget is larger, then only this size will be visible +-- and the rest is made visible via scrolling. +-- @tparam number max_size The maximum size of this widget or nil for unlimited. +function scroll:set_max_size(max_size) + if max_size == self._private.max_size then + return + end + self._private.max_size = max_size + self:emit_signal("widget::layout_changed") +end + +--- Set the step function that determines the exact behaviour of the scrolling +-- animation. +-- The step function is called with five arguments: +-- +-- * The time in seconds since the state of the animation +-- * The size of the child widget +-- * The size of the visible part of the widget +-- * The speed of the animation. This should have a linear effect on this +-- function's behaviour. +-- * The extra space configured by @{set_extra_space}. This was not yet added to +-- the size of the child widget, but should likely be added to it in most +-- cases. +-- +-- The step function should return a single number. This number is the offset at +-- which the widget is drawn and should be between 0 and `size+extra_space`. +-- @tparam function step_function A step function. +-- @see step_functions +function scroll:set_step_function(step_function) + -- Call the step functions once to see if it works + step_function(0, 42, 10, 10, 5) + if step_function == self._private.step_function then + return + end + self._private.step_function = step_function + self:emit_signal("widget::redraw_needed") +end + +--- Set an upper limit for the space for scrolling. +-- This restricts the child widget's maximal size. +-- @tparam number space_for_scrolling The space for scrolling +function scroll:set_space_for_scrolling(space_for_scrolling) + if space_for_scrolling == self._private.space_for_scrolling then + return + end + self._private.space_for_scrolling = space_for_scrolling + self:emit_signal("widget::layout_changed") +end + +local function get_layout(dir, widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + ret._private.paused = false + ret._private.timer = GLib.Timer() + ret._private.scroll_timer = nil + + setmetatable(ret, scroll_mt) + + ret:set_direction(dir) + ret:set_widget(widget) + ret:set_fps(fps or 20) + ret:set_speed(speed or 10) + ret:set_extra_space(extra_space or 0) + ret:set_expand(expand) + ret:set_max_size(max_size) + ret:set_step_function(step_function or scroll.step_functions.linear_increase) + ret:set_space_for_scrolling(space_for_scrolling or 2^1024) + + return ret +end + +--- Get a new horizontal scrolling container. +-- @param[opt] widget The widget that should be scrolled +-- @param[opt=20] fps The number of frames per second +-- @param[opt=10] speed The speed of the animation +-- @param[opt=0] extra_space The amount of extra space to include +-- @tparam[opt=false] boolean expand Should the widget be expanded to include the +-- extra space? +-- @param[opt] max_size The maximum size of the child widget +-- @param[opt=step_functions.linear_increase] step_function The step function to be used +-- @param[opt=2^1024] space_for_scrolling The space for scrolling +function scroll.horizontal(widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling) + return get_layout("h", widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling) +end + +--- Get a new vertical scrolling container. +-- @param[opt] widget The widget that should be scrolled +-- @param[opt=20] fps The number of frames per second +-- @param[opt=10] speed The speed of the animation +-- @param[opt=0] extra_space The amount of extra space to include +-- @tparam[opt=false] boolean expand Should the widget be expanded to include the +-- extra space? +-- @param[opt] max_size The maximum size of the child widget +-- @param[opt=step_functions.linear_increase] step_function The step function to be used +-- @param[opt=2^1024] space_for_scrolling The space for scrolling +function scroll.vertical(widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling) + return get_layout("v", widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling) +end + +--- A selection of step functions +-- @see set_step_function +scroll.step_functions = {} + +--- A step function that scrolls the widget in an increasing direction with +-- constant speed. +function scroll.step_functions.linear_increase(elapsed, size, _, speed, extra_space) + return (elapsed * speed) % (size + extra_space) +end + +--- A step function that scrolls the widget in an decreasing direction with +-- constant speed. +function scroll.step_functions.linear_decrease(elapsed, size, _, speed, extra_space) + return (-elapsed * speed) % (size + extra_space) +end + +--- A step function that scrolls the widget to its end and back to its +-- beginning, then back to its end, etc. The speed is constant. +function scroll.step_functions.linear_back_and_forth(elapsed, size, visible_size, speed) + local state = ((elapsed * speed) % (2 * size)) / size + state = state <= 1 and state or 2 - state + return (size - visible_size) * state +end + +--- A step function that scrolls the widget to its end and back to its +-- beginning, then back to its end, etc. The speed is null at the ends and +-- maximal in the middle. +function scroll.step_functions.nonlinear_back_and_forth(elapsed, size, visible_size, speed) + local state = ((elapsed * speed) % (2 * size)) / size + local negate = false + if state > 1 then + negate = true + state = state - 1 + end + if state < 1/3 then + -- In the first 1/3rd of time, do a quadratic increase in speed + state = 2 * state * state + elseif state < 2/3 then + -- In the center, do a linear increase. That means we need: + -- If state is 1/3, result is 2/9 = 2 * 1/3 * 1/3 + -- If state is 2/3, result is 7/9 = 1 - 2 * (1 - 2/3) * (1 - 2/3) + state = 5/3*state - 3/9 + else + -- In the last 1/3rd of time, do a quadratic decrease in speed + state = 1 - 2 * (1 - state) * (1 - state) + end + if negate then + state = 1 - state + end + return (size - visible_size) * state +end + +--- A step function that scrolls the widget to its end and back to its +-- beginning, then back to its end, etc. The speed is null at the ends and +-- maximal in the middle. At both ends the widget stands still for a moment. +function scroll.step_functions.waiting_nonlinear_back_and_forth(elapsed, size, visible_size, speed) + local state = ((elapsed * speed) % (2 * size)) / size + local negate = false + if state > 1 then + negate = true + state = state - 1 + end + if state < 1/5 or state > 4/5 then + -- One fifth of time, nothing moves + state = state < 1/5 and 0 or 1 + else + state = (state - 1/5) * 5/3 + if state < 1/3 then + -- In the first 1/3rd of time, do a quadratic increase in speed + state = 2 * state * state + elseif state < 2/3 then + -- In the center, do a linear increase. That means we need: + -- If state is 1/3, result is 2/9 = 2 * 1/3 * 1/3 + -- If state is 2/3, result is 7/9 = 1 - 2 * (1 - 2/3) * (1 - 2/3) + state = 5/3*state - 3/9 + else + -- In the last 1/3rd of time, do a quadratic decrease in speed + state = 1 - 2 * (1 - state) * (1 - state) + end + end + if negate then + state = 1 - state + end + return (size - visible_size) * state +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return scroll + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/drawable.lua b/lib/wibox/drawable.lua new file mode 100644 index 0000000..330edc3 --- /dev/null +++ b/lib/wibox/drawable.lua @@ -0,0 +1,489 @@ +--------------------------------------------------------------------------- +--- Handling of drawables. A drawable is something that can be drawn to. +-- +-- @author Uli Schlachter +-- @copyright 2012 Uli Schlachter +-- @classmod wibox.drawable +--------------------------------------------------------------------------- + +local drawable = {} +local capi = { + awesome = awesome, + root = root, + screen = screen +} +local beautiful = require("beautiful") +local cairo = require("lgi").cairo +local color = require("gears.color") +local object = require("gears.object") +local surface = require("gears.surface") +local timer = require("gears.timer") +local grect = require("gears.geometry").rectangle +local matrix = require("gears.matrix") +local hierarchy = require("wibox.hierarchy") +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +local visible_drawables = {} + +-- Get the widget context. This should always return the same table (if +-- possible), so that our draw and fit caches can work efficiently. +local function get_widget_context(self) + local geom = self.drawable:geometry() + + local s = self._forced_screen + if not s then + local sgeos = {} + + for scr in capi.screen do + sgeos[scr] = scr.geometry + end + + s = grect.get_by_coord(sgeos, geom.x, geom.y) or capi.screen.primary + end + + local context = self._widget_context + local dpi = beautiful.xresources.get_dpi(s) + if (not context) or context.screen ~= s or context.dpi ~= dpi then + context = { + screen = s, + dpi = dpi, + drawable = self, + } + for k, v in pairs(self._widget_context_skeleton) do + context[k] = v + end + self._widget_context = context + + -- Give widgets a chance to react to the new context + self._need_complete_repaint = true + end + return context +end + +local function do_redraw(self) + if not self.drawable.valid then return end + if self._forced_screen and not self._forced_screen.valid then return end + + local surf = surface.load_silently(self.drawable.surface, false) + -- The surface can be nil if the drawable's parent was already finalized + if not surf then return end + local cr = cairo.Context(surf) + local geom = self.drawable:geometry(); + local x, y, width, height = geom.x, geom.y, geom.width, geom.height + local context = get_widget_context(self) + + -- Relayout + if self._need_relayout or self._need_complete_repaint then + self._need_relayout = false + if self._widget_hierarchy and self.widget then + self._widget_hierarchy:update(context, + self.widget, width, height, self._dirty_area) + else + self._need_complete_repaint = true + if self.widget then + self._widget_hierarchy_callback_arg = {} + self._widget_hierarchy = hierarchy.new(context, self.widget, width, height, + self._redraw_callback, self._layout_callback, self._widget_hierarchy_callback_arg) + else + self._widget_hierarchy = nil + end + end + + if self._need_complete_repaint then + self._need_complete_repaint = false + self._dirty_area:union_rectangle(cairo.RectangleInt{ + x = 0, y = 0, width = width, height = height + }) + end + end + + -- Clip to the dirty area + if self._dirty_area:is_empty() then + return + end + for i = 0, self._dirty_area:num_rectangles() - 1 do + local rect = self._dirty_area:get_rectangle(i) + cr:rectangle(rect.x, rect.y, rect.width, rect.height) + end + self._dirty_area = cairo.Region.create() + cr:clip() + + -- Draw the background + cr:save() + + if not capi.awesome.composite_manager_running then + -- This is pseudo-transparency: We draw the wallpaper in the background + local wallpaper = surface.load_silently(capi.root.wallpaper(), false) + if wallpaper then + cr.operator = cairo.Operator.SOURCE + cr:set_source_surface(wallpaper, -x, -y) + cr:paint() + end + cr.operator = cairo.Operator.OVER + else + -- This is true transparency: We draw a translucent background + cr.operator = cairo.Operator.SOURCE + end + + cr:set_source(self.background_color) + cr:paint() + + cr:restore() + + -- Paint the background image + if self.background_image then + cr:save() + if type(self.background_image) == "function" then + self.background_image(context, cr, width, height, unpack(self.background_image_args)) + else + local pattern = cairo.Pattern.create_for_surface(self.background_image) + cr:set_source(pattern) + cr:paint() + end + cr:restore() + end + + -- Draw the widget + if self._widget_hierarchy then + cr:set_source(self.foreground_color) + self._widget_hierarchy:draw(context, cr) + end + + self.drawable:refresh() + + assert(cr.status == "SUCCESS", "Cairo context entered error state: " .. cr.status) +end + +local function find_widgets(_drawable, result, _hierarchy, x, y) + local m = _hierarchy:get_matrix_from_device() + + -- Is (x,y) inside of this hierarchy or any child (aka the draw extents) + local x1, y1 = m:transform_point(x, y) + local x2, y2, w2, h2 = _hierarchy:get_draw_extents() + if x1 < x2 or x1 >= x2 + w2 then + return + end + if y1 < y2 or y1 >= y2 + h2 then + return + end + + -- Is (x,y) inside of this widget? + local width, height = _hierarchy:get_size() + if x1 >= 0 and y1 >= 0 and x1 <= width and y1 <= height then + -- Get the extents of this widget in the device space + local x3, y3, w3, h3 = matrix.transform_rectangle(_hierarchy:get_matrix_to_device(), + 0, 0, width, height) + table.insert(result, { + x = x3, y = y3, width = w3, height = h3, + widget_width = width, + widget_height = height, + drawable = _drawable, + widget = _hierarchy:get_widget(), + hierarchy = _hierarchy + }) + end + for _, child in ipairs(_hierarchy:get_children()) do + find_widgets(_drawable, result, child, x, y) + end +end + +--- Find a widget by a point. +-- The drawable must have drawn itself at least once for this to work. +-- @param x X coordinate of the point +-- @param y Y coordinate of the point +-- @treturn table A table containing a description of all the widgets that +-- contain the given point. Each entry is a table containing this drawable as +-- its `.drawable` entry, the widget under `.widget` and the instance of +-- `wibox.hierarchy` describing the size and position of the widget under +-- `.hierarchy`. For convenience, `.x`, `.y`, `.width` and `.height` contain an +-- approximation of the widget's extents on the surface. `widget_width` and +-- `widget_height` contain the exact size of the widget in its own, local +-- coordinate system (which may e.g. be rotated and scaled). +function drawable:find_widgets(x, y) + local result = {} + if self._widget_hierarchy then + find_widgets(self, result, self._widget_hierarchy, x, y) + end + return result +end + + +--- Set the widget that the drawable displays +function drawable:set_widget(widget) + self.widget = widget + + -- Make sure the widget gets drawn + self._need_relayout = true + self.draw() +end + +--- Set the background of the drawable +-- @param c The background to use. This must either be a cairo pattern object, +-- nil or a string that gears.color() understands. +-- @see gears.color +function drawable:set_bg(c) + c = c or "#000000" + local t = type(c) + + if t == "string" or t == "table" then + c = color(c) + end + + -- If the background is completely opaque, we don't need to redraw when + -- the drawable is moved + -- XXX: This isn't needed when awesome.composite_manager_running is true, + -- but a compositing manager could stop/start and we'd have to properly + -- handle this. So for now we choose the lazy approach. + local redraw_on_move = not color.create_opaque_pattern(c) + if self._redraw_on_move ~= redraw_on_move then + self._redraw_on_move = redraw_on_move + if redraw_on_move then + self.drawable:connect_signal("property::x", self._do_complete_repaint) + self.drawable:connect_signal("property::y", self._do_complete_repaint) + else + self.drawable:disconnect_signal("property::x", self._do_complete_repaint) + self.drawable:disconnect_signal("property::y", self._do_complete_repaint) + end + end + + self.background_color = c + self._do_complete_repaint() +end + +--- Set the background image of the drawable +-- If `image` is a function, it will be called with `(context, cr, width, height)` +-- as arguments. Any other arguments passed to this method will be appended. +-- @param image A background image or a function +function drawable:set_bgimage(image, ...) + if type(image) ~= "function" then + image = surface(image) + end + + self.background_image = image + self.background_image_args = {...} + + self._do_complete_repaint() +end + +--- Set the foreground of the drawable +-- @param c The foreground to use. This must either be a cairo pattern object, +-- nil or a string that gears.color() understands. +-- @see gears.color +function drawable:set_fg(c) + c = c or "#FFFFFF" + if type(c) == "string" or type(c) == "table" then + c = color(c) + end + self.foreground_color = c + self._do_complete_repaint() +end + +function drawable:_force_screen(s) + self._forced_screen = s +end + +function drawable:_inform_visible(visible) + self._visible = visible + if visible then + visible_drawables[self] = true + -- The wallpaper or widgets might have changed + self:_do_complete_repaint() + else + visible_drawables[self] = nil + end +end + +local function emit_difference(name, list, skip) + local function in_table(table, val) + for _, v in pairs(table) do + if v.widget == val.widget then + return true + end + end + return false + end + + for _, v in pairs(list) do + if not in_table(skip, v) then + v.widget:emit_signal(name,v) + end + end +end + +local function handle_leave(_drawable) + emit_difference("mouse::leave", _drawable._widgets_under_mouse, {}) + _drawable._widgets_under_mouse = {} +end + +local function handle_motion(_drawable, x, y) + if x < 0 or y < 0 or x > _drawable.drawable:geometry().width or y > _drawable.drawable:geometry().height then + return handle_leave(_drawable) + end + + -- Build a plain list of all widgets on that point + local widgets_list = _drawable:find_widgets(x, y) + + -- First, "leave" all widgets that were left + emit_difference("mouse::leave", _drawable._widgets_under_mouse, widgets_list) + -- Then enter some widgets + emit_difference("mouse::enter", widgets_list, _drawable._widgets_under_mouse) + + _drawable._widgets_under_mouse = widgets_list +end + +local function setup_signals(_drawable) + local d = _drawable.drawable + + local function clone_signal(name) + -- When "name" is emitted on wibox.drawin, also emit it on wibox + d:connect_signal(name, function(_, ...) + _drawable:emit_signal(name, ...) + end) + end + clone_signal("button::press") + clone_signal("button::release") + clone_signal("mouse::enter") + clone_signal("mouse::leave") + clone_signal("mouse::move") + clone_signal("property::surface") + clone_signal("property::width") + clone_signal("property::height") + clone_signal("property::x") + clone_signal("property::y") +end + +function drawable.new(d, widget_context_skeleton, drawable_name) + local ret = object() + ret.drawable = d + ret._widget_context_skeleton = widget_context_skeleton + ret._need_complete_repaint = true + ret._need_relayout = true + ret._dirty_area = cairo.Region.create() + setup_signals(ret) + + for k, v in pairs(drawable) do + if type(v) == "function" then + ret[k] = v + end + end + + -- Only redraw a drawable once, even when we get told to do so multiple times. + ret._redraw_pending = false + ret._do_redraw = function() + ret._redraw_pending = false + do_redraw(ret) + end + + -- Connect our signal when we need a redraw + ret.draw = function() + if not ret._redraw_pending then + timer.delayed_call(ret._do_redraw) + ret._redraw_pending = true + end + end + ret._do_complete_repaint = function() + ret._need_complete_repaint = true + ret:draw() + end + + -- Do a full redraw if the surface changes (the new surface has no content yet) + d:connect_signal("property::surface", ret._do_complete_repaint) + + -- Do a normal redraw when the drawable moves. This will likely do nothing + -- in most cases, but it makes us do a complete repaint when we are moved to + -- a different screen. + d:connect_signal("property::x", ret.draw) + d:connect_signal("property::y", ret.draw) + + -- Currently we aren't redrawing on move (signals not connected). + -- :set_bg() will later recompute this. + ret._redraw_on_move = false + + -- Set the default background + ret:set_bg(beautiful.bg_normal) + ret:set_fg(beautiful.fg_normal) + + -- Initialize internals + ret._widgets_under_mouse = {} + + local function button_signal(name) + d:connect_signal(name, function(_, x, y, button, modifiers) + local widgets = ret:find_widgets(x, y) + for _, v in pairs(widgets) do + -- Calculate x/y inside of the widget + local lx, ly = v.hierarchy:get_matrix_from_device():transform_point(x, y) + v.widget:emit_signal(name, lx, ly, button, modifiers,v) + end + end) + end + button_signal("button::press") + button_signal("button::release") + + d:connect_signal("mouse::move", function(_, x, y) handle_motion(ret, x, y) end) + d:connect_signal("mouse::leave", function() handle_leave(ret) end) + + -- Set up our callbacks for repaints + ret._redraw_callback = function(hierar, arg) + -- Avoid crashes when a drawable was partly finalized and dirty_area is broken. + if not ret._visible then + return + end + if ret._widget_hierarchy_callback_arg ~= arg then + return + end + local m = hierar:get_matrix_to_device() + local x, y, width, height = matrix.transform_rectangle(m, hierar:get_draw_extents()) + local x1, y1 = math.floor(x), math.floor(y) + local x2, y2 = math.ceil(x + width), math.ceil(y + height) + ret._dirty_area:union_rectangle(cairo.RectangleInt{ + x = x1, y = y1, width = x2 - x1, height = y2 - y1 + }) + ret:draw() + end + ret._layout_callback = function(_, arg) + if ret._widget_hierarchy_callback_arg ~= arg then + return + end + ret._need_relayout = true + -- When not visible, we will be redrawn when we become visible. In the + -- mean-time, the layout does not matter much. + if ret._visible then + ret:draw() + end + end + + -- Add __tostring method to metatable. + ret.drawable_name = drawable_name or object.modulename(3) + local mt = {} + local orig_string = tostring(ret) + mt.__tostring = function() + return string.format("%s (%s)", ret.drawable_name, orig_string) + end + ret = setmetatable(ret, mt) + + -- Make sure the drawable is drawn at least once + ret._do_complete_repaint() + + return ret +end + +-- Redraw all drawables when the wallpaper changes +capi.awesome.connect_signal("wallpaper_changed", function() + for d in pairs(visible_drawables) do + d:_do_complete_repaint() + end +end) + +-- Give drawables a chance to react to screen changes +local function draw_all() + for d in pairs(visible_drawables) do + d:draw() + end +end +screen.connect_signal("property::geometry", draw_all) +screen.connect_signal("added", draw_all) +screen.connect_signal("removed", draw_all) + +return setmetatable(drawable, { __call = function(_, ...) return drawable.new(...) end }) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/hierarchy.lua b/lib/wibox/hierarchy.lua new file mode 100644 index 0000000..6bf2167 --- /dev/null +++ b/lib/wibox/hierarchy.lua @@ -0,0 +1,333 @@ +--------------------------------------------------------------------------- +-- Management of widget hierarchies. Each widget hierarchy object has a widget +-- for which it saves e.g. size and transformation in its parent. Also, each +-- widget has a number of children. +-- +-- @author Uli Schlachter +-- @copyright 2015 Uli Schlachter +-- @module wibox.hierarchy +--------------------------------------------------------------------------- + +local matrix = require("gears.matrix") +local protected_call = require("gears.protected_call") +local cairo = require("lgi").cairo +local base = require("wibox.widget.base") +local no_parent = base.no_parent_I_know_what_I_am_doing + +local hierarchy = {} + +local function hierarchy_new(redraw_callback, layout_callback, callback_arg) + local result = { + _matrix = matrix.identity, + _matrix_to_device = matrix.identity, + _need_update = true, + _widget = nil, + _context = nil, + _redraw_callback = redraw_callback, + _layout_callback = layout_callback, + _callback_arg = callback_arg, + _size = { + width = nil, + height = nil + }, + _draw_extents = { + x = 0, + y = 0, + width = 0, + height = 0 + }, + _parent = nil, + _children = {} + } + + function result._redraw() + redraw_callback(result, callback_arg) + end + function result._layout() + local h = result + while h do + h._need_update = true + h = h._parent + end + layout_callback(result, callback_arg) + end + function result._emit_recursive(widget, name, ...) + local cur = result + assert(widget == cur._widget) + while cur do + if cur._widget then + cur._widget:emit_signal(name, ...) + end + cur = cur._parent + end + end + + for k, f in pairs(hierarchy) do + if type(f) == "function" then + result[k] = f + end + end + return result +end + +local hierarchy_update +function hierarchy_update(self, context, widget, width, height, region, matrix_to_parent, matrix_to_device) + if (not self._need_update) and self._widget == widget and + self._context == context and + self._size.width == width and self._size.height == height and + matrix.equals(self._matrix, matrix_to_parent) and + matrix.equals(self._matrix_to_device, matrix_to_device) then + -- Nothing changed + return + end + + self._need_update = false + + local old_x, old_y, old_width, old_height + local old_widget = self._widget + if self._size.width and self._size.height then + local x, y, w, h = matrix.transform_rectangle(self._matrix_to_device, 0, 0, self._size.width, self._size.height) + old_x, old_y = math.floor(x), math.floor(y) + old_width, old_height = math.ceil(x + w) - old_x, math.ceil(y + h) - old_y + else + old_x, old_y, old_width, old_height = 0, 0, 0, 0 + end + + -- Disconnect old signals + if old_widget and old_widget ~= widget then + self._widget:disconnect_signal("widget::redraw_needed", self._redraw) + self._widget:disconnect_signal("widget::layout_changed", self._layout) + self._widget:disconnect_signal("widget::emit_recursive", self._emit_recursive) + end + + -- Save the arguments we need to save + self._widget = widget + self._context = context + self._size.width = width + self._size.height = height + self._matrix = matrix_to_parent + self._matrix_to_device = matrix_to_device + + -- Connect signals + if old_widget ~= widget then + widget:weak_connect_signal("widget::redraw_needed", self._redraw) + widget:weak_connect_signal("widget::layout_changed", self._layout) + widget:weak_connect_signal("widget::emit_recursive", self._emit_recursive) + end + + -- Update children + local old_children = self._children + local layout_result = base.layout_widget(no_parent, context, widget, width, height) + self._children = {} + for _, w in ipairs(layout_result or {}) do + local r = table.remove(old_children, 1) + if not r then + r = hierarchy_new(self._redraw_callback, self._layout_callback, self._callback_arg) + r._parent = self + end + hierarchy_update(r, context, w._widget, w._width, w._height, region, w._matrix, w._matrix * matrix_to_device) + table.insert(self._children, r) + end + + -- Calculate the draw extents + local x1, y1, x2, y2 = 0, 0, width, height + for _, h in ipairs(self._children) do + local px, py, pwidth, pheight = matrix.transform_rectangle(h._matrix, h:get_draw_extents()) + x1 = math.min(x1, px) + y1 = math.min(y1, py) + x2 = math.max(x2, px + pwidth) + y2 = math.max(y2, py + pheight) + end + self._draw_extents = { + x = x1, y = y1, + width = x2 - x1, + height = y2 - y1 + } + + -- Check which part needs to be redrawn + + -- Are there any children which were removed? Their area needs a redraw. + for _, child in ipairs(old_children) do + local x, y, w, h = matrix.transform_rectangle(child._matrix_to_device, child:get_draw_extents()) + region:union_rectangle(cairo.RectangleInt{ + x = x, y = y, width = w, height = h + }) + child._parent = nil + end + + -- Did we change and need to be redrawn? + local x, y, w, h = matrix.transform_rectangle(self._matrix_to_device, 0, 0, self._size.width, self._size.height) + local new_x, new_y = math.floor(x), math.floor(y) + local new_width, new_height = math.ceil(x + w) - new_x, math.ceil(y + h) - new_y + if new_x ~= old_x or new_y ~= old_y or new_width ~= old_width or new_height ~= old_height or + widget ~= old_widget then + region:union_rectangle(cairo.RectangleInt{ + x = old_x, y = old_y, width = old_width, height = old_height + }) + region:union_rectangle(cairo.RectangleInt{ + x = new_x, y = new_y, width = new_width, height = new_height + }) + end +end + +--- Create a new widget hierarchy that has no parent. +-- @param context The context in which we are laid out. +-- @param widget The widget that is at the base of the hierarchy. +-- @param width The available width for this hierarchy. +-- @param height The available height for this hierarchy. +-- @param redraw_callback Callback that is called with the corresponding widget +-- hierarchy on widget::redraw_needed on some widget. +-- @param layout_callback Callback that is called with the corresponding widget +-- hierarchy on widget::layout_changed on some widget. +-- @param callback_arg A second argument that is given to the above callbacks. +-- @return A new widget hierarchy +function hierarchy.new(context, widget, width, height, redraw_callback, layout_callback, callback_arg) + local result = hierarchy_new(redraw_callback, layout_callback, callback_arg) + result:update(context, widget, width, height) + return result +end + +--- Update a widget hierarchy with some new state. +-- @param context The context in which we are laid out. +-- @param widget The widget that is at the base of the hierarchy. +-- @param width The available width for this hierarchy. +-- @param height The available height for this hierarchy. +-- @param[opt] region A region to use for accumulating changed parts +-- @return A cairo region describing the changed parts (either the `region` +-- argument or a new, internally created region). +function hierarchy:update(context, widget, width, height, region) + region = region or cairo.Region.create() + hierarchy_update(self, context, widget, width, height, region, self._matrix, self._matrix_to_device) + return region +end + +--- Get the widget that this hierarchy manages. +function hierarchy:get_widget() + return self._widget +end + +--- Get a matrix that transforms to the parent's coordinate space from this +-- hierarchy's coordinate system. +-- @return A matrix describing the transformation. +function hierarchy:get_matrix_to_parent() + return self._matrix +end + +--- Get a matrix that transforms to the base of this hierarchy's coordinate +-- system (aka the coordinate system of the device that this +-- hierarchy is applied upon) from this hierarchy's coordinate system. +-- @return A matrix describing the transformation. +function hierarchy:get_matrix_to_device() + return self._matrix_to_device +end + +--- Get a matrix that transforms from the parent's coordinate space into this +-- hierarchy's coordinate system. +-- @return A matrix describing the transformation. +function hierarchy:get_matrix_from_parent() + local m = self:get_matrix_to_parent() + return m:invert() +end + +--- Get a matrix that transforms from the base of this hierarchy's coordinate +-- system (aka the coordinate system of the device that this +-- hierarchy is applied upon) into this hierarchy's coordinate system. +-- @return A matrix describing the transformation. +function hierarchy:get_matrix_from_device() + local m = self:get_matrix_to_device() + return m:invert() +end + +--- Get the extents that this hierarchy possibly draws to (in the current coordinate space). +-- This includes the size of this element plus the size of all children +-- (after applying the corresponding transformation). +-- @return x, y, width, height +function hierarchy:get_draw_extents() + local ext = self._draw_extents + return ext.x, ext.y, ext.width, ext.height +end + +--- Get the size that this hierarchy logically covers (in the current coordinate space). +-- @return width, height +function hierarchy:get_size() + local ext = self._size + return ext.width, ext.height +end + +--- Get a list of all children. +-- @return List of all children hierarchies. +function hierarchy:get_children() + return self._children +end + +--- Does the given cairo context have an empty clip (aka "no drawing possible")? +local function empty_clip(cr) + local _, _, width, height = cr:clip_extents() + return width == 0 or height == 0 +end + +--- Draw a hierarchy to some cairo context. +-- This function draws the widgets in this widget hierarchy to the given cairo +-- context. The context's clip is used to skip parts that aren't visible. +-- @param context The context in which widgets are drawn. +-- @param cr The cairo context that is used for drawing. +function hierarchy:draw(context, cr) + local widget = self:get_widget() + if not widget._private.visible then + return + end + + cr:save() + cr:transform(self:get_matrix_to_parent():to_cairo_matrix()) + + -- Clip to the draw extents + cr:rectangle(self:get_draw_extents()) + cr:clip() + + -- Draw if needed + if not empty_clip(cr) then + local opacity = widget:get_opacity() + local function call(func, extra_arg1, extra_arg2) + if not func then return end + if not extra_arg2 then + protected_call(func, widget, context, cr, self:get_size()) + else + protected_call(func, widget, context, extra_arg1, extra_arg2, cr, self:get_size()) + end + end + + -- Prepare opacity handling + if opacity ~= 1 then + cr:push_group() + end + + -- Draw the widget + cr:save() + cr:rectangle(0, 0, self:get_size()) + cr:clip() + call(widget.draw) + cr:restore() + + -- Draw its children (We already clipped to the draw extents above) + call(widget.before_draw_children) + for i, wi in ipairs(self:get_children()) do + call(widget.before_draw_child, i, wi:get_widget()) + wi:draw(context, cr) + call(widget.after_draw_child, i, wi:get_widget()) + end + call(widget.after_draw_children) + + -- Apply opacity + if opacity ~= 1 then + cr:pop_group_to_source() + cr.operator = cairo.Operator.OVER + cr:paint_with_alpha(opacity) + end + end + + cr:restore() +end + +return hierarchy + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/init.lua b/lib/wibox/init.lua new file mode 100644 index 0000000..3fc89ce --- /dev/null +++ b/lib/wibox/init.lua @@ -0,0 +1,479 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox +--------------------------------------------------------------------------- + +local capi = { + drawin = drawin, + root = root, + awesome = awesome, + screen = screen +} +local setmetatable = setmetatable +local pairs = pairs +local type = type +local object = require("gears.object") +local grect = require("gears.geometry").rectangle +local beautiful = require("beautiful") +local base = require("wibox.widget.base") + +--- This provides widget box windows. Every wibox can also be used as if it were +-- a drawin. All drawin functions and properties are also available on wiboxes! +-- wibox +local wibox = { mt = {}, object = {} } +wibox.layout = require("wibox.layout") +wibox.container = require("wibox.container") +wibox.widget = require("wibox.widget") +wibox.drawable = require("wibox.drawable") +wibox.hierarchy = require("wibox.hierarchy") + +local force_forward = { + shape_bounding = true, + shape_clip = true, +} + +--Imported documentation + +--- Border width. +-- +-- **Signal:** +-- +-- * *property::border_width* +-- +-- @property border_width +-- @param integer + +--- Border color. +-- +-- Please note that this property only support string based 24 bit or 32 bit +-- colors: +-- +-- Red Blue +-- _| _| +-- #FF00FF +-- T‾ +-- Green +-- +-- +-- Red Blue +-- _| _| +-- #FF00FF00 +-- T‾ ‾T +-- Green Alpha +-- +-- **Signal:** +-- +-- * *property::border_color* +-- +-- @property border_color +-- @param string + +--- On top of other windows. +-- +-- **Signal:** +-- +-- * *property::ontop* +-- +-- @property ontop +-- @param boolean + +--- The mouse cursor. +-- +-- **Signal:** +-- +-- * *property::cursor* +-- +-- @property cursor +-- @param string +-- @see mouse + +--- Visibility. +-- +-- **Signal:** +-- +-- * *property::visible* +-- +-- @property visible +-- @param boolean + +--- The opacity of the wibox, between 0 and 1. +-- +-- **Signal:** +-- +-- * *property::opacity* +-- +-- @property opacity +-- @tparam number opacity (between 0 and 1) + +--- The window type (desktop, normal, dock, ...). +-- +-- **Signal:** +-- +-- * *property::type* +-- +-- @property type +-- @param string +-- @see client.type + +--- The x coordinates. +-- +-- **Signal:** +-- +-- * *property::x* +-- +-- @property x +-- @param integer + +--- The y coordinates. +-- +-- **Signal:** +-- +-- * *property::y* +-- +-- @property y +-- @param integer + +--- The width of the wibox. +-- +-- **Signal:** +-- +-- * *property::width* +-- +-- @property width +-- @param width + +--- The height of the wibox. +-- +-- **Signal:** +-- +-- * *property::height* +-- +-- @property height +-- @param height + +--- The wibox screen. +-- +-- @property screen +-- @param screen + +--- The wibox's `drawable`. +-- +-- **Signal:** +-- +-- * *property::drawable* +-- +-- @property drawable +-- @tparam drawable drawable + +--- The widget that the `wibox` displays. +-- @property widget +-- @param widget + +--- The X window id. +-- +-- **Signal:** +-- +-- * *property::window* +-- +-- @property window +-- @param string +-- @see client.window + +--- The wibox's bounding shape as a (native) cairo surface. +-- +-- **Signal:** +-- +-- * *property::shape_bounding* +-- +-- @property shape_bounding +-- @param surface._native + +--- The wibox's clip shape as a (native) cairo surface. +-- +-- **Signal:** +-- +-- * *property::shape_clip* +-- +-- @property shape_clip +-- @param surface._native + +--- Get or set mouse buttons bindings to a wibox. +-- +-- @param buttons_table A table of buttons objects, or nothing. +-- @function buttons + +--- Get or set wibox geometry. That's the same as accessing or setting the x, +-- y, width or height properties of a wibox. +-- +-- @param A table with coordinates to modify. +-- @return A table with wibox coordinates and geometry. +-- @function geometry + +--- Get or set wibox struts. +-- +-- @param strut A table with new strut, or nothing +-- @return The wibox strut in a table. +-- @function struts +-- @see client.struts + +--- The default background color. +-- @beautiful beautiful.bg_normal +-- @see bg + +--- The default foreground (text) color. +-- @beautiful beautiful.fg_normal +-- @see fg + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @name setup +-- @class function + +--- The background of the wibox. +-- @param c The background to use. This must either be a cairo pattern object, +-- nil or a string that gears.color() understands. +-- @property bg +-- @see gears.color + +--- The background image of the drawable. +-- If `image` is a function, it will be called with `(context, cr, width, height)` +-- as arguments. Any other arguments passed to this method will be appended. +-- @param image A background image or a function +-- @property bgimage +-- @see gears.surface + +--- The foreground (text) of the wibox. +-- @param c The foreground to use. This must either be a cairo pattern object, +-- nil or a string that gears.color() understands. +-- @property fg +-- @see gears.color + +--- Find a widget by a point. +-- The wibox must have drawn itself at least once for this to work. +-- @tparam number x X coordinate of the point +-- @tparam number y Y coordinate of the point +-- @treturn table A sorted table of widgets positions. The first element is the biggest +-- container while the last is the topmost widget. The table contains *x*, *y*, +-- *width*, *height* and *widget*. +-- @name find_widgets +-- @class function + + +function wibox:set_widget(widget) + self._drawable:set_widget(widget) +end + +function wibox:get_widget() + return self._drawable.widget +end + +wibox.setup = base.widget.setup + +function wibox:set_bg(c) + self._drawable:set_bg(c) +end + +function wibox:set_bgimage(image, ...) + self._drawable:set_bgimage(image, ...) +end + +function wibox:set_fg(c) + self._drawable:set_fg(c) +end + +function wibox:find_widgets(x, y) + return self._drawable:find_widgets(x, y) +end + +function wibox:get_screen() + if self.screen_assigned and self.screen_assigned.valid then + return self.screen_assigned + else + self.screen_assigned = nil + end + local sgeos = {} + + for s in capi.screen do + sgeos[s] = s.geometry + end + + return grect.get_closest_by_coord(sgeos, self.x, self.y) +end + +function wibox:set_screen(s) + s = capi.screen[s or 1] + if s ~= self:get_screen() then + self.x = s.geometry.x + self.y = s.geometry.y + end + + -- Remember this screen so things work correctly if screens overlap and + -- (x,y) is not enough to figure out the correct screen. + self.screen_assigned = s + self._drawable:_force_screen(s) +end + +for _, k in pairs{ "buttons", "struts", "geometry", "get_xproperty", "set_xproperty" } do + wibox[k] = function(self, ...) + return self.drawin[k](self.drawin, ...) + end +end + +local function setup_signals(_wibox) + local obj + local function clone_signal(name) + -- When "name" is emitted on wibox.drawin, also emit it on wibox + obj:connect_signal(name, function(_, ...) + _wibox:emit_signal(name, ...) + end) + end + + obj = _wibox.drawin + clone_signal("property::border_color") + clone_signal("property::border_width") + clone_signal("property::buttons") + clone_signal("property::cursor") + clone_signal("property::height") + clone_signal("property::ontop") + clone_signal("property::opacity") + clone_signal("property::struts") + clone_signal("property::visible") + clone_signal("property::width") + clone_signal("property::x") + clone_signal("property::y") + clone_signal("property::geometry") + clone_signal("property::shape_bounding") + clone_signal("property::shape_clip") + + obj = _wibox._drawable + clone_signal("button::press") + clone_signal("button::release") + clone_signal("mouse::enter") + clone_signal("mouse::leave") + clone_signal("mouse::move") + clone_signal("property::surface") +end + +--- Create a wibox. +-- @tparam[opt=nil] table args +-- @tparam integer args.border_width Border width. +-- @tparam string args.border_color Border color. +-- @tparam boolean args.ontop On top of other windows. +-- @tparam string args.cursor The mouse cursor. +-- @tparam boolean args.visible Visibility. +-- @tparam number args.opacity The opacity of the wibox, between 0 and 1. +-- @tparam string args.type The window type (desktop, normal, dock, …). +-- @tparam integer args.x The x coordinates. +-- @tparam integer args.y The y coordinates. +-- @tparam integer args.width The width of the wibox. +-- @tparam integer args.height The height of the wibox. +-- @tparam screen args.screen The wibox screen. +-- @tparam wibox.widget args.widget The widget that the wibox displays. +-- @param args.shape_bounding The wibox’s bounding shape as a (native) cairo surface. +-- @param args.shape_clip The wibox’s clip shape as a (native) cairo surface. +-- @tparam color args.bg The background of the wibox. +-- @tparam surface args.bgimage The background image of the drawable. +-- @tparam color args.fg The foreground (text) of the wibox. +-- @treturn wibox The new wibox +-- @function .wibox + +local function new(args) + args = args or {} + local ret = object() + local w = capi.drawin(args) + + function w.get_wibox() + return ret + end + + ret.drawin = w + ret._drawable = wibox.drawable(w.drawable, { wibox = ret }, + "wibox drawable (" .. object.modulename(3) .. ")") + + ret._drawable:_inform_visible(w.visible) + w:connect_signal("property::visible", function() + ret._drawable:_inform_visible(w.visible) + end) + + for k, v in pairs(wibox) do + if type(v) == "function" then + ret[k] = v + end + end + + setup_signals(ret) + ret.draw = ret._drawable.draw + + -- Set the default background + ret:set_bg(args.bg or beautiful.bg_normal) + ret:set_fg(args.fg or beautiful.fg_normal) + + -- Add __tostring method to metatable. + local mt = {} + local orig_string = tostring(ret) + mt.__tostring = function() + return string.format("wibox: %s (%s)", + tostring(ret._drawable), orig_string) + end + ret = setmetatable(ret, mt) + + -- Make sure the wibox is drawn at least once + ret.draw() + + -- If a value is not found, look in the drawin + setmetatable(ret, { + __index = function(self, k) + if rawget(self, "get_"..k) then + return self["get_"..k](self) + else + return w[k] + end + end, + __newindex = function(self, k,v) + if rawget(self, "set_"..k) then + self["set_"..k](self, v) + elseif w[k] ~= nil or force_forward[k] then + w[k] = v + else + rawset(self, k, v) + end + end + }) + + -- Set other wibox specific arguments + if args.bgimage then + ret:set_bgimage( args.bgimage ) + end + + if args.widget then + ret:set_widget ( args.widget ) + end + + if args.screen then + ret:set_screen ( args.screen ) + end + + return ret +end + +--- Redraw a wibox. You should never have to call this explicitely because it is +-- automatically called when needed. +-- @param wibox +-- @function draw + +function wibox.mt:__call(...) + return new(...) +end + +-- Extend the luaobject +object.properties(capi.drawin, { + getter_class = wibox.object, + setter_class = wibox.object, + auto_emit = true, +}) + +return setmetatable(wibox, wibox.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/align.lua b/lib/wibox/layout/align.lua new file mode 100644 index 0000000..92a5582 --- /dev/null +++ b/lib/wibox/layout/align.lua @@ -0,0 +1,526 @@ +--------------------------------------------------------------------------- +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_layout_defaults_align.svg) +-- +-- @usage +--wibox.widget { +-- generic_widget( 'first' ), +-- generic_widget( 'second' ), +-- generic_widget( 'third' ), +-- layout = wibox.layout.align.horizontal +--} +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.layout.align +--------------------------------------------------------------------------- + +local table = table +local pairs = pairs +local type = type +local floor = math.floor +local util = require("awful.util") +local base = require("wibox.widget.base") + +local align = {} + +-- Calculate the layout of an align layout. +-- @param context The context in which we are drawn. +-- @param width The available width. +-- @param height The available height. +function align:layout(context, width, height) + local result = {} + + -- Draw will have to deal with all three align modes and should work in a + -- way that makes sense if one or two of the widgets are missing (if they + -- are all missing, it won't draw anything.) It should also handle the case + -- where the fit something that isn't set to expand (for instance the + -- outside widgets when the expand mode is "inside" or any of the widgets + -- when the expand mode is "none" wants to take up more space than is + -- allowed. + local size_first = 0 + -- start with all the space given by the parent, subtract as we go along + local size_remains = self._private.dir == "y" and height or width + -- This is only set & used if expand ~= "inside" and we have second width. + -- It contains the size allocated to the second widget. + local size_second + + -- we will prioritize the middle widget unless the expand mode is "inside" + -- if it is, we prioritize the first widget by not doing this block also, + -- if the second widget doesn't exist, we will prioritise the first one + -- instead + if self._private.expand ~= "inside" and self._private.second then + local w, h = base.fit_widget(self, context, self._private.second, width, height) + size_second = self._private.dir == "y" and h or w + -- if all the space is taken, skip the rest, and draw just the middle + -- widget + if size_second >= size_remains then + return { base.place_widget_at(self._private.second, 0, 0, width, height) } + else + -- the middle widget is sized first, the outside widgets are given + -- the remaining space if available we will draw later + size_remains = floor((size_remains - size_second) / 2) + end + end + if self._private.first then + local w, h, _ = width, height, nil + -- we use the fit function for the "inside" and "none" modes, but + -- ignore it for the "outside" mode, which will force it to expand + -- into the remaining space + if self._private.expand ~= "outside" then + if self._private.dir == "y" then + _, h = base.fit_widget(self, context, self._private.first, width, size_remains) + size_first = h + -- for "inside", the third widget will get a chance to use the + -- remaining space, then the middle widget. For "none" we give + -- the third widget the remaining space if there was no second + -- widget to take up any space (as the first if block is skipped + -- if this is the case) + if self._private.expand == "inside" or not self._private.second then + size_remains = size_remains - h + end + else + w, _ = base.fit_widget(self, context, self._private.first, size_remains, height) + size_first = w + if self._private.expand == "inside" or not self._private.second then + size_remains = size_remains - w + end + end + else + if self._private.dir == "y" then + h = size_remains + else + w = size_remains + end + end + table.insert(result, base.place_widget_at(self._private.first, 0, 0, w, h)) + end + -- size_remains will be <= 0 if first used all the space + if self._private.third and size_remains > 0 then + local w, h, _ = width, height, nil + if self._private.expand ~= "outside" then + if self._private.dir == "y" then + _, h = base.fit_widget(self, context, self._private.third, width, size_remains) + -- give the middle widget the rest of the space for "inside" mode + if self._private.expand == "inside" then + size_remains = size_remains - h + end + else + w, _ = base.fit_widget(self, context, self._private.third, size_remains, height) + if self._private.expand == "inside" then + size_remains = size_remains - w + end + end + else + if self._private.dir == "y" then + h = size_remains + else + w = size_remains + end + end + local x, y = width - w, height - h + table.insert(result, base.place_widget_at(self._private.third, x, y, w, h)) + end + -- here we either draw the second widget in the space set aside for it + -- in the beginning, or in the remaining space, if it is "inside" + if self._private.second and size_remains > 0 then + local x, y, w, h = 0, 0, width, height + if self._private.expand == "inside" then + if self._private.dir == "y" then + h = size_remains + x, y = 0, size_first + else + w = size_remains + x, y = size_first, 0 + end + else + local _ + if self._private.dir == "y" then + _, h = base.fit_widget(self, context, self._private.second, width, size_second) + y = floor( (height - h)/2 ) + else + w, _ = base.fit_widget(self, context, self._private.second, size_second, height) + x = floor( (width -w)/2 ) + end + end + table.insert(result, base.place_widget_at(self._private.second, x, y, w, h)) + end + return result +end + +--- Set the layout's first widget. +-- This is the widget that is at the left/top +-- @property first + +function align:set_first(widget) + if self._private.first == widget then + return + end + self._private.first = widget + self:emit_signal("widget::layout_changed") +end + +--- Set the layout's second widget. This is the centered one. +-- @property second + +function align:set_second(widget) + if self._private.second == widget then + return + end + self._private.second = widget + self:emit_signal("widget::layout_changed") +end + +--- Set the layout's third widget. +-- This is the widget that is at the right/bottom +-- @property third + +function align:set_third(widget) + if self._private.third == widget then + return + end + self._private.third = widget + self:emit_signal("widget::layout_changed") +end + +for _, prop in ipairs {"first", "second", "third", "expand" } do + align["get_"..prop] = function(self) + return self._private[prop] + end +end + +--- All direct children of this layout. +-- This can be used to replace all 3 widgets at once. +-- @treturn table a list of all widgets +-- @property children + +function align:get_children() + return util.from_sparse {self._private.first, self._private.second, self._private.third} +end + +function align:set_children(children) + self:set_first(children[1]) + self:set_second(children[2]) + self:set_third(children[3]) +end + +-- Fit the align layout into the given space. The align layout will +-- ask for the sum of the sizes of its sub-widgets in its direction +-- and the largest sized sub widget in the other direction. +-- @param context The context in which we are fit. +-- @param orig_width The available width. +-- @param orig_height The available height. +function align:fit(context, orig_width, orig_height) + local used_in_dir = 0 + local used_in_other = 0 + + for _, v in pairs{self._private.first, self._private.second, self._private.third} do + local w, h = base.fit_widget(self, context, v, orig_width, orig_height) + + local max = self._private.dir == "y" and w or h + if max > used_in_other then + used_in_other = max + end + + used_in_dir = used_in_dir + (self._private.dir == "y" and h or w) + end + + if self._private.dir == "y" then + return used_in_other, used_in_dir + end + return used_in_dir, used_in_other +end + +--- Set the expand mode which determines how sub widgets expand to take up +-- unused space. +-- +-- @tparam[opt=inside] string mode How to use unused space. +-- +-- * "inside" - Default option. Size of outside widgets is determined using +-- their fit function. Second, middle, or center widget expands to fill +-- remaining space. +-- * "outside" - Center widget is sized using its fit function and placed in +-- the center of the allowed space. Outside widgets expand (or contract) to +-- fill remaining space on their side. +-- * "none" - All widgets are sized using their fit function, drawn to only the +-- returned space, or remaining space, whichever is smaller. Center widget +-- gets priority. +-- @property expand + +function align:set_expand(mode) + if mode == "none" or mode == "outside" then + self._private.expand = mode + else + self._private.expand = "inside" + end + self:emit_signal("widget::layout_changed") +end + +function align:reset() + for _, v in pairs({ "first", "second", "third" }) do + self[v] = nil + end + self:emit_signal("widget::layout_changed") +end + +local function get_layout(dir, first, second, third) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + ret._private.dir = dir + + for k, v in pairs(align) do + if type(v) == "function" then + rawset(ret, k, v) + end + end + + ret:set_expand("inside") + ret:set_first(first) + ret:set_second(second) + ret:set_third(third) + + -- An align layout allow set_children to have empty entries + ret.allow_empty_widget = true + + return ret +end + +--- Returns a new horizontal align layout. An align layout can display up to +-- three widgets. The widget set via :set_left() is left-aligned. :set_right() +-- sets a widget which will be right-aligned. The remaining space between those +-- two will be given to the widget set via :set_middle(). +-- @tparam[opt] widget left Widget to be put to the left. +-- @tparam[opt] widget middle Widget to be put to the middle. +-- @tparam[opt] widget right Widget to be put to the right. +function align.horizontal(left, middle, right) + local ret = get_layout("x", left, middle, right) + + rawset(ret, "set_left" , ret.set_first ) + rawset(ret, "set_middle", ret.set_second ) + rawset(ret, "set_right" , ret.set_third ) + + return ret +end + +--- Returns a new vertical align layout. An align layout can display up to +-- three widgets. The widget set via :set_top() is top-aligned. :set_bottom() +-- sets a widget which will be bottom-aligned. The remaining space between those +-- two will be given to the widget set via :set_middle(). +-- @tparam[opt] widget top Widget to be put to the top. +-- @tparam[opt] widget middle Widget to be put to the middle. +-- @tparam[opt] widget bottom Widget to be put to the right. +function align.vertical(top, middle, bottom) + local ret = get_layout("y", top, middle, bottom) + + rawset(ret, "set_top" , ret.set_first ) + rawset(ret, "set_middle", ret.set_second ) + rawset(ret, "set_bottom", ret.set_third ) + + return ret +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return align + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/constraint.lua b/lib/wibox/layout/constraint.lua new file mode 100644 index 0000000..5333b38 --- /dev/null +++ b/lib/wibox/layout/constraint.lua @@ -0,0 +1,17 @@ +--------------------------------------------------------------------------- +-- This class has been moved to `wibox.container.` +-- +-- @author Lukáš Hrázký +-- @copyright 2012 Lukáš Hrázký +-- @classmod wibox.layout.constraint +--------------------------------------------------------------------------- + +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.container.constraint"), + "wibox.layout.constraint", + "wibox.container.constraint" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/fixed.lua b/lib/wibox/layout/fixed.lua new file mode 100644 index 0000000..7258438 --- /dev/null +++ b/lib/wibox/layout/fixed.lua @@ -0,0 +1,585 @@ +--------------------------------------------------------------------------- +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_layout_defaults_fixed.svg) +-- +-- @usage +--wibox.widget { +-- generic_widget( 'first' ), +-- generic_widget( 'second' ), +-- generic_widget( 'third' ), +-- layout = wibox.layout.fixed.horizontal +--} +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.layout.fixed +--------------------------------------------------------------------------- + +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local base = require("wibox.widget.base") +local table = table +local pairs = pairs +local util = require("awful.util") + +local fixed = {} + +--Imported documentation + +--- Set a widget at a specific index, replace the current one. +-- **Signal:** widget::replaced The argument is the new widget and the old one +-- and the index. +-- @tparam number index A widget or a widget index +-- @param widget2 The widget to take the place of the first one +-- @treturn boolean If the operation is successful +-- @name set +-- @class function + +--- Replace the first instance of `widget` in the layout with `widget2`. +-- **Signal:** widget::replaced The argument is the new widget and the old one +-- and the index. +-- @param widget The widget to replace +-- @param widget2 The widget to replace `widget` with +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name replace_widget +-- @class function + +--- Swap 2 widgets in a layout. +-- **Signal:** widget::swapped The arguments are both widgets and both (new) indexes. +-- @tparam number index1 The first widget index +-- @tparam number index2 The second widget index +-- @treturn boolean If the operation is successful +-- @name swap +-- @class function + +--- Swap 2 widgets in a layout. +-- If widget1 is present multiple time, only the first instance is swapped +-- **Signal:** widget::swapped The arguments are both widgets and both (new) indexes. +-- if the layouts not the same, then only `widget::replaced` will be emitted. +-- @param widget1 The first widget +-- @param widget2 The second widget +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name swap_widgets +-- @class function + +--- Get all direct children of this layout. +-- @param layout The layout you are modifying. +-- @property children + +--- Reset a ratio layout. This removes all widgets from the layout. +-- **Signal:** widget::reset +-- @param layout The layout you are modifying. +-- @name reset +-- @class function + + +-- Layout a fixed layout. Each widget gets just the space it asks for. +-- @param context The context in which we are drawn. +-- @param width The available width. +-- @param height The available height. +function fixed:layout(context, width, height) + local result = {} + local pos,spacing = 0, self._private.spacing + + for k, v in pairs(self._private.widgets) do + local x, y, w, h, _ + if self._private.dir == "y" then + x, y = 0, pos + w, h = width, height - pos + if k ~= #self._private.widgets or not self._private.fill_space then + _, h = base.fit_widget(self, context, v, w, h); + end + pos = pos + h + spacing + else + x, y = pos, 0 + w, h = width - pos, height + if k ~= #self._private.widgets or not self._private.fill_space then + w, _ = base.fit_widget(self, context, v, w, h); + end + pos = pos + w + spacing + end + + if (self._private.dir == "y" and pos-spacing > height) or + (self._private.dir ~= "y" and pos-spacing > width) then + break + end + table.insert(result, base.place_widget_at(v, x, y, w, h)) + end + return result +end + +--- Add some widgets to the given fixed layout +-- @param ... Widgets that should be added (must at least be one) +function fixed:add(...) + -- No table.pack in Lua 5.1 :-( + local args = { n=select('#', ...), ... } + assert(args.n > 0, "need at least one widget to add") + for i=1, args.n do + base.check_widget(args[i]) + table.insert(self._private.widgets, args[i]) + end + self:emit_signal("widget::layout_changed") +end + + +--- Remove a widget from the layout +-- @tparam number index The widget index to remove +-- @treturn boolean index If the operation is successful +function fixed:remove(index) + if not index or index < 1 or index > #self._private.widgets then return false end + + table.remove(self._private.widgets, index) + + self:emit_signal("widget::layout_changed") + + return true +end + +--- Remove one or more widgets from the layout +-- The last parameter can be a boolean, forcing a recursive seach of the +-- widget(s) to remove. +-- @param widget ... Widgets that should be removed (must at least be one) +-- @treturn boolean If the operation is successful +function fixed:remove_widgets(...) + local args = { ... } + + local recursive = type(args[#args]) == "boolean" and args[#args] + + local ret = true + for k, rem_widget in ipairs(args) do + if recursive and k == #args then break end + + local idx, l = self:index(rem_widget, recursive) + + if idx and l and l.remove then + l:remove(idx, false) + else + ret = false + end + + end + + return #args > (recursive and 1 or 0) and ret +end + +function fixed:get_children() + return self._private.widgets +end + +function fixed:set_children(children) + self:reset() + if #children > 0 then + self:add(unpack(children)) + end +end + +--- Replace the first instance of `widget` in the layout with `widget2` +-- @param widget The widget to replace +-- @param widget2 The widget to replace `widget` with +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +function fixed:replace_widget(widget, widget2, recursive) + local idx, l = self:index(widget, recursive) + + if idx and l then + l:set(idx, widget2) + return true + end + + return false +end + +function fixed:swap(index1, index2) + if not index1 or not index2 or index1 > #self._private.widgets + or index2 > #self._private.widgets then + return false + end + + local widget1, widget2 = self._private.widgets[index1], self._private.widgets[index2] + + self:set(index1, widget2) + self:set(index2, widget1) + + self:emit_signal("widget::swapped", widget1, widget2, index2, index1) + + return true +end + +function fixed:swap_widgets(widget1, widget2, recursive) + base.check_widget(widget1) + base.check_widget(widget2) + + local idx1, l1 = self:index(widget1, recursive) + local idx2, l2 = self:index(widget2, recursive) + + if idx1 and l1 and idx2 and l2 and (l1.set or l1.set_widget) and (l2.set or l2.set_widget) then + if l1.set then + l1:set(idx1, widget2) + if l1 == self then + self:emit_signal("widget::swapped", widget1, widget2, idx2, idx1) + end + elseif l1.set_widget then + l1:set_widget(widget2) + end + if l2.set then + l2:set(idx2, widget1) + if l2 == self then + self:emit_signal("widget::swapped", widget1, widget2, idx2, idx1) + end + elseif l2.set_widget then + l2:set_widget(widget1) + end + + return true + end + + return false +end + +function fixed:set(index, widget2) + if (not widget2) or (not self._private.widgets[index]) then return false end + + base.check_widget(widget2) + + local w = self._private.widgets[index] + + self._private.widgets[index] = widget2 + + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::replaced", widget2, w, index) + + return true +end + +--- Insert a new widget in the layout at position `index` +-- **Signal:** widget::inserted The arguments are the widget and the index +-- @tparam number index The position +-- @param widget The widget +-- @treturn boolean If the operation is successful +function fixed:insert(index, widget) + if not index or index < 1 or index > #self._private.widgets + 1 then return false end + + base.check_widget(widget) + table.insert(self._private.widgets, index, widget) + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::inserted", widget, #self._private.widgets) + + return true +end + +-- Fit the fixed layout into the given space +-- @param context The context in which we are fit. +-- @param orig_width The available width. +-- @param orig_height The available height. +function fixed:fit(context, orig_width, orig_height) + local width, height = orig_width, orig_height + local used_in_dir, used_max = 0, 0 + + for _, v in pairs(self._private.widgets) do + local w, h = base.fit_widget(self, context, v, width, height) + local in_dir, max + if self._private.dir == "y" then + max, in_dir = w, h + height = height - in_dir + else + in_dir, max = w, h + width = width - in_dir + end + if max > used_max then + used_max = max + end + used_in_dir = used_in_dir + in_dir + + if width <= 0 or height <= 0 then + if self._private.dir == "y" then + used_in_dir = orig_height + else + used_in_dir = orig_width + end + break + end + end + + local spacing = self._private.spacing * (#self._private.widgets-1) + + if self._private.dir == "y" then + return used_max, used_in_dir + spacing + end + return used_in_dir + spacing, used_max +end + +function fixed:reset() + self._private.widgets = {} + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::reseted") +end + +--- Set the layout's fill_space property. If this property is true, the last +-- widget will get all the space that is left. If this is false, the last widget +-- won't be handled specially and there can be space left unused. +-- @property fill_space + +function fixed:fill_space(val) + if self._private.fill_space ~= val then + self._private.fill_space = not not val + self:emit_signal("widget::layout_changed") + end +end + +local function get_layout(dir, widget1, ...) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, fixed, true) + + ret._private.dir = dir + ret._private.widgets = {} + ret:set_spacing(0) + ret:fill_space(false) + + if widget1 then + ret:add(widget1, ...) + end + + return ret +end + +--- Returns a new horizontal fixed layout. Each widget will get as much space as it +-- asks for and each widget will be drawn next to its neighboring widget. +-- Widgets can be added via :add() or as arguments to this function. +-- @tparam widget ... Widgets that should be added to the layout. +-- @function wibox.layout.fixed.horizontal +function fixed.horizontal(...) + return get_layout("x", ...) +end + +--- Returns a new vertical fixed layout. Each widget will get as much space as it +-- asks for and each widget will be drawn next to its neighboring widget. +-- Widgets can be added via :add() or as arguments to this function. +-- @tparam widget ... Widgets that should be added to the layout. +-- @function wibox.layout.fixed.vertical +function fixed.vertical(...) + return get_layout("y", ...) +end + +--- Add spacing between each layout widgets +-- @property spacing +-- @tparam number spacing Spacing between widgets. + +function fixed:set_spacing(spacing) + if self._private.spacing ~= spacing then + self._private.spacing = spacing + self:emit_signal("widget::layout_changed") + end +end + +function fixed:get_spacing() + return self._private.spacing or 0 +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return fixed + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/flex.lua b/lib/wibox/layout/flex.lua new file mode 100644 index 0000000..1326f31 --- /dev/null +++ b/lib/wibox/layout/flex.lua @@ -0,0 +1,429 @@ +--------------------------------------------------------------------------- +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_layout_defaults_flex.svg) +-- +-- @usage +--wibox.widget { +-- generic_widget( 'first' ), +-- generic_widget( 'second' ), +-- generic_widget( 'third' ), +-- layout = wibox.layout.flex.horizontal +--} +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.layout.flex +--------------------------------------------------------------------------- + +local base = require("wibox.widget.base") +local fixed = require("wibox.layout.fixed") +local table = table +local pairs = pairs +local floor = math.floor +local util = require("awful.util") + +local flex = {} + +--Imported documentation + +--- Set a widget at a specific index, replace the current one. +-- **Signal:** widget::replaced The argument is the new widget and the old one +-- and the index. +-- @tparam number index A widget or a widget index +-- @param widget2 The widget to take the place of the first one +-- @treturn boolean If the operation is successful +-- @name set +-- @class function + +--- Replace the first instance of `widget` in the layout with `widget2`. +-- **Signal:** widget::replaced The argument is the new widget and the old one +-- and the index. +-- @param widget The widget to replace +-- @param widget2 The widget to replace `widget` with +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name replace_widget +-- @class function + +--- Swap 2 widgets in a layout. +-- **Signal:** widget::swapped The arguments are both widgets and both (new) indexes. +-- @tparam number index1 The first widget index +-- @tparam number index2 The second widget index +-- @treturn boolean If the operation is successful +-- @name swap +-- @class function + +--- Swap 2 widgets in a layout. +-- If widget1 is present multiple time, only the first instance is swapped +-- **Signal:** widget::swapped The arguments are both widgets and both (new) indexes. +-- if the layouts not the same, then only `widget::replaced` will be emitted. +-- @param widget1 The first widget +-- @param widget2 The second widget +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name swap_widgets +-- @class function + +--- Get all direct children of this layout. +-- @param layout The layout you are modifying. +-- @property children + +--- Reset a ratio layout. This removes all widgets from the layout. +-- **Signal:** widget::reset +-- @param layout The layout you are modifying. +-- @name reset +-- @class function + + +--- Replace the layout children +-- @tparam table children A table composed of valid widgets +-- @name set_children +-- @class function + +--- Add some widgets to the given fixed layout +-- @param layout The layout you are modifying. +-- @tparam widget ... Widgets that should be added (must at least be one) +-- @name add +-- @class function + +--- Remove a widget from the layout +-- @tparam index The widget index to remove +-- @treturn boolean index If the operation is successful +-- @name remove +-- @class function + +--- Remove one or more widgets from the layout +-- The last parameter can be a boolean, forcing a recursive seach of the +-- widget(s) to remove. +-- @param widget ... Widgets that should be removed (must at least be one) +-- @treturn boolean If the operation is successful +-- @name remove_widgets +-- @class function + +--- Insert a new widget in the layout at position `index` +-- @tparam number index The position +-- @param widget The widget +-- @treturn boolean If the operation is successful +-- @name insert +-- @class function + +function flex:layout(_, width, height) + local result = {} + local pos,spacing = 0, self._private.spacing + local num = #self._private.widgets + local total_spacing = (spacing*(num-1)) + + local space_per_item + if self._private.dir == "y" then + space_per_item = height / num - total_spacing/num + else + space_per_item = width / num - total_spacing/num + end + + if self._private.max_widget_size then + space_per_item = math.min(space_per_item, self._private.max_widget_size) + end + + for _, v in pairs(self._private.widgets) do + local x, y, w, h + if self._private.dir == "y" then + x, y = 0, util.round(pos) + w, h = width, floor(space_per_item) + else + x, y = util.round(pos), 0 + w, h = floor(space_per_item), height + end + + table.insert(result, base.place_widget_at(v, x, y, w, h)) + + pos = pos + space_per_item + spacing + + if (self._private.dir == "y" and pos-spacing >= height) or + (self._private.dir ~= "y" and pos-spacing >= width) then + break + end + end + + return result +end + +-- Fit the flex layout into the given space. +-- @param context The context in which we are fit. +-- @param orig_width The available width. +-- @param orig_height The available height. +function flex:fit(context, orig_width, orig_height) + local used_in_dir = 0 + local used_in_other = 0 + + -- Figure out the maximum size we can give out to sub-widgets + local sub_height = self._private.dir == "x" and orig_height or orig_height / #self._private.widgets + local sub_width = self._private.dir == "y" and orig_width or orig_width / #self._private.widgets + + for _, v in pairs(self._private.widgets) do + local w, h = base.fit_widget(self, context, v, sub_width, sub_height) + + local max = self._private.dir == "y" and w or h + if max > used_in_other then + used_in_other = max + end + + used_in_dir = used_in_dir + (self._private.dir == "y" and h or w) + end + + if self._private.max_widget_size then + used_in_dir = math.min(used_in_dir, + #self._private.widgets * self._private.max_widget_size) + end + + local spacing = self._private.spacing * (#self._private.widgets-1) + + if self._private.dir == "y" then + return used_in_other, used_in_dir + spacing + end + return used_in_dir + spacing, used_in_other +end + +--- Set the maximum size the widgets in this layout will take. +--That is, maximum width for horizontal and maximum height for vertical. +-- @property max_widget_size +-- @param number + +function flex:set_max_widget_size(val) + if self._private.max_widget_size ~= val then + self._private.max_widget_size = val + self:emit_signal("widget::layout_changed") + end +end + +local function get_layout(dir, widget1, ...) + local ret = fixed[dir](widget1, ...) + + util.table.crush(ret, flex, true) + + ret._private.fill_space = nil + + return ret +end + +--- Returns a new horizontal flex layout. A flex layout shares the available space +-- equally among all widgets. Widgets can be added via :add(widget). +-- @tparam widget ... Widgets that should be added to the layout. +-- @function wibox.layout.flex.horizontal +function flex.horizontal(...) + return get_layout("horizontal", ...) +end + +--- Returns a new vertical flex layout. A flex layout shares the available space +-- equally among all widgets. Widgets can be added via :add(widget). +-- @tparam widget ... Widgets that should be added to the layout. +-- @function wibox.layout.flex.vertical +function flex.vertical(...) + return get_layout("vertical", ...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return flex + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/init.lua b/lib/wibox/layout/init.lua new file mode 100644 index 0000000..4a9b006 --- /dev/null +++ b/lib/wibox/layout/init.lua @@ -0,0 +1,23 @@ +--------------------------------------------------------------------------- +--- Collection of layouts that can be used in widget boxes +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.layout +--------------------------------------------------------------------------- +local base = require("wibox.widget.base") + +return setmetatable({ + fixed = require("wibox.layout.fixed"); + align = require("wibox.layout.align"); + flex = require("wibox.layout.flex"); + rotate = require("wibox.layout.rotate"); + margin = require("wibox.layout.margin"); + mirror = require("wibox.layout.mirror"); + constraint = require("wibox.layout.constraint"); + scroll = require("wibox.layout.scroll"); + ratio = require("wibox.layout.ratio"); + stack = require("wibox.layout.stack"); +}, {__call = function(_, args) return base.make_widget_declarative(args) end}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/margin.lua b/lib/wibox/layout/margin.lua new file mode 100644 index 0000000..8d2eba2 --- /dev/null +++ b/lib/wibox/layout/margin.lua @@ -0,0 +1,17 @@ +--------------------------------------------------------------------------- +-- This class has been moved to `wibox.container.margin` +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.layout.margin +--------------------------------------------------------------------------- + +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.container.margin"), + "wibox.layout.margin", + "wibox.container.margin" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/mirror.lua b/lib/wibox/layout/mirror.lua new file mode 100644 index 0000000..16be754 --- /dev/null +++ b/lib/wibox/layout/mirror.lua @@ -0,0 +1,17 @@ +--------------------------------------------------------------------------- +-- This class has been moved to `wibox.container.mirror` +-- +-- @author dodo +-- @copyright 2012 dodo +-- @classmod wibox.layout.mirror +--------------------------------------------------------------------------- + +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.container.mirror"), + "wibox.layout.mirror", + "wibox.container.mirror" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/ratio.lua b/lib/wibox/layout/ratio.lua new file mode 100644 index 0000000..bcae443 --- /dev/null +++ b/lib/wibox/layout/ratio.lua @@ -0,0 +1,583 @@ +--------------------------------------------------------------------------- +--- A layout filling all the available space. Each widget is assigned a +-- ratio (percentage) of the total space. Multiple methods are available to +-- ajust this ratio. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_layout_defaults_ratio.svg) +-- +-- @usage +--local w = wibox.widget { +-- generic_widget( 'first' ), +-- generic_widget( 'second' ), +-- generic_widget( 'third' ), +-- layout = wibox.layout.ratio.horizontal +--} +--w:ajust_ratio(2, 0.44, 0.33, 0.22) +-- @author Emmanuel Lepage Vallee +-- @copyright 2016 Emmanuel Lepage Vallee +-- @classmod wibox.layout.ratio +--------------------------------------------------------------------------- + +local base = require("wibox.widget.base" ) +local flex = require("wibox.layout.flex" ) +local table = table +local pairs = pairs +local floor = math.floor +local util = require("awful.util") + +local ratio = {} + +--Imported documentation + +--- Set a widget at a specific index, replace the current one. +-- **Signal:** widget::replaced The argument is the new widget and the old one +-- and the index. +-- @tparam number index A widget or a widget index +-- @param widget2 The widget to take the place of the first one +-- @treturn boolean If the operation is successful +-- @name set +-- @class function + +--- Replace the first instance of `widget` in the layout with `widget2`. +-- **Signal:** widget::replaced The argument is the new widget and the old one +-- and the index. +-- @param widget The widget to replace +-- @param widget2 The widget to replace `widget` with +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name replace_widget +-- @class function + +--- Swap 2 widgets in a layout. +-- **Signal:** widget::swapped The arguments are both widgets and both (new) indexes. +-- @tparam number index1 The first widget index +-- @tparam number index2 The second widget index +-- @treturn boolean If the operation is successful +-- @name swap +-- @class function + +--- Swap 2 widgets in a layout. +-- If widget1 is present multiple time, only the first instance is swapped +-- **Signal:** widget::swapped The arguments are both widgets and both (new) indexes. +-- if the layouts not the same, then only `widget::replaced` will be emitted. +-- @param widget1 The first widget +-- @param widget2 The second widget +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name swap_widgets +-- @class function + +--- Get all direct children of this layout. +-- @param layout The layout you are modifying. +-- @property children + +--- Reset a ratio layout. This removes all widgets from the layout. +-- **Signal:** widget::reset +-- @param layout The layout you are modifying. +-- @name reset +-- @class function + + +-- Compute the sum of all ratio (ideally, it should be 1) +local function gen_sum(self, i_s, i_e) + local sum, new_w = 0,0 + + for i = i_s or 1, i_e or #self._private.widgets do + if self._private.ratios[i] then + sum = sum + self._private.ratios[i] + else + new_w = new_w + 1 + end + end + + return sum, new_w +end + +-- The ratios are expressed as percentages. For this to work, the sum of all +-- ratio must be 1. This function attempt to ajust them. Space can be taken +-- from or added to a ratio when widgets are being added or removed. If a +-- specific ratio must be enforced for a widget, it has to be done with the +-- `ajust_ratio` method after each insertion or deletion +local function normalize(self) + local count = #self._private.widgets + if count == 0 then return end + + -- Instead of adding "if" everywhere, just handle this common case + if count == 1 then + self._private.ratios = { 1 } + return + end + + local sum, new_w = gen_sum(self) + local old_count = #self._private.widgets - new_w + + local to_add = (sum == 0) and 1 or (sum / old_count) + + -- Make sure all widgets have a ratio + for i=1, #self._private.widgets do + if not self._private.ratios[i] then + self._private.ratios[i] = to_add + end + end + + sum = sum + to_add*new_w + + local delta, new_sum = (1 - sum) / count,0 + + -- Increase or decrease each ratio so it the sum become 1 + for i=1, #self._private.widgets do + self._private.ratios[i] = self._private.ratios[i] + delta + new_sum = new_sum + self._private.ratios[i] + end + + -- Floating points is not an exact science, but it should still be close + -- to 1.00. + assert(new_sum > 0.99 and new_sum < 1.01) +end + +function ratio:layout(_, width, height) + local result = {} + local pos,spacing = 0, self._private.spacing + + for k, v in ipairs(self._private.widgets) do + local space + local x, y, w, h + + if self._private.dir == "y" then + space = height * self._private.ratios[k] + x, y = 0, util.round(pos) + w, h = width, floor(space) + else + space = width * self._private.ratios[k] + x, y = util.round(pos), 0 + w, h = floor(space), height + end + + table.insert(result, base.place_widget_at(v, x, y, w, h)) + + pos = pos + space + spacing + + -- Make sure all widgets fit in the layout, if they aren't, something + -- went wrong + if (self._private.dir == "y" and util.round(pos) >= height) or + (self._private.dir ~= "y" and util.round(pos) >= width) then + break + end + end + + return result +end + +--- Increase the ratio of "widget" +-- If the increment produce an invalid ratio (not between 0 and 1), the method +-- do nothing. +-- @tparam number index The widget index to change +-- @tparam number increment An floating point value between -1 and 1 where the +-- end result is within 0 and 1 +function ratio:inc_ratio(index, increment) + if #self._private.widgets == 1 or (not index) or (not self._private.ratios[index]) + or increment < -1 or increment > 1 then + return + end + + assert(self._private.ratios[index]) + + self:set_ratio(index, self._private.ratios[index] + increment) +end + +--- Increment the ratio of the first instance of `widget` +-- If the increment produce an invalid ratio (not between 0 and 1), the method +-- do nothing. +-- @param widget The widget to ajust +-- @tparam number increment An floating point value between -1 and 1 where the +-- end result is within 0 and 1 +function ratio:inc_widget_ratio(widget, increment) + if not widget or not increment then return end + + local index = self:index(widget) + + self:inc_ratio(index, increment) +end + +--- Set the ratio of the widget at position `index` +-- @tparam number index The index of the widget to change +-- @tparam number percent An floating point value between 0 and 1 +function ratio:set_ratio(index, percent) + if not percent or #self._private.widgets == 1 or not index or not self._private.widgets[index] + or percent < 0 or percent > 1 then + return + end + + local old = self._private.ratios[index] + + -- Remove what has to be cleared from all widget + local delta = ( (percent-old) / (#self._private.widgets-1) ) + + for k in pairs(self._private.widgets) do + self._private.ratios[k] = self._private.ratios[k] - delta + end + + -- Set the new ratio + self._private.ratios[index] = percent + + -- As some widgets may now have a slightly negative ratio, normalize again + normalize(self) + + self:emit_signal("widget::layout_changed") +end + +--- Get the ratio at `index`. +-- @tparam number index The widget index to query +-- @treturn number The index (between 0 and 1) +function ratio:get_ratio(index) + if not index then return end + return self._private.ratios[index] +end + +--- Set the ratio of `widget` to `percent`. +-- @tparam widget widget The widget to ajust. +-- @tparam number percent A floating point value between 0 and 1. +function ratio:set_widget_ratio(widget, percent) + local index = self:index(widget) + + self:set_ratio(index, percent) +end + +--- Update all widgets to match a set of a ratio. +-- The sum of before, itself and after must be 1 or nothing will be done +-- @tparam number index The index of the widget to change +-- @tparam number before The sum of the ratio before the widget +-- @tparam number itself The ratio for "widget" +-- @tparam number after The sum of the ratio after the widget +function ratio:ajust_ratio(index, before, itself, after) + if not self._private.widgets[index] or not before or not itself or not after then + return + end + + local sum = before + itself + after + + -- As documented, it is the caller job to come up with valid numbers + if math.min(before, itself, after) < 0 then return end + if sum > 1.01 or sum < -0.99 then return end + + -- Compute the before and after offset to be applied to each widgets + local before_count, after_count = index-1, #self._private.widgets - index + + local b, a = gen_sum(self, 1, index-1), gen_sum(self, index+1) + + local db, da = (before - b)/before_count, (after - a)/after_count + + -- Apply the new ratio + self._private.ratios[index] = itself + + -- Equality split the delta among widgets before and after + for i = 1, index -1 do + self._private.ratios[i] = self._private.ratios[i] + db + end + for i = index+1, #self._private.widgets do + self._private.ratios[i] = self._private.ratios[i] + da + end + + -- Remove potential negative ratio + normalize(self) + + self:emit_signal("widget::layout_changed") +end + +--- Update all widgets to match a set of a ratio +-- @param widget The widget to ajust +-- @tparam number before The sum of the ratio before the widget +-- @tparam number itself The ratio for "widget" +-- @tparam number after The sum of the ratio after the widget +function ratio:ajust_widget_ratio(widget, before, itself, after) + local index = self:index(widget) + self:ajust_ratio(index, before, itself, after) +end + +--- Add some widgets to the given fixed layout +-- **Signal:** widget::added The argument are the widgets +-- @tparam widget ... Widgets that should be added (must at least be one) +function ratio:add(...) + -- No table.pack in Lua 5.1 :-( + local args = { n=select('#', ...), ... } + assert(args.n > 0, "need at least one widget to add") + for i=1, args.n do + base.check_widget(args[i]) + table.insert(self._private.widgets, args[i]) + end + + normalize(self) + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::added", ...) +end + +--- Remove a widget from the layout +-- **Signal:** widget::removed The arguments are the widget and the index +-- @tparam number index The widget index to remove +-- @treturn boolean index If the operation is successful +function ratio:remove(index) + if not index or not self._private.widgets[index] then return false end + + local w = self._private.widgets[index] + + table.remove(self._private.ratios, index) + table.remove(self._private.widgets, index) + + normalize(self) + + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::removed", w, index) + + return true +end + +--- Insert a new widget in the layout at position `index` +-- **Signal:** widget::inserted The arguments are the widget and the index +-- @tparam number index The position +-- @param widget The widget +function ratio:insert(index, widget) + if not index or index < 1 or index > #self._private.widgets + 1 then return false end + + base.check_widget(widget) + + table.insert(self._private.widgets, index, widget) + + normalize(self) + + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::inserted", widget, #self._private.widgets) +end + +local function get_layout(dir, widget1, ...) + local ret = flex[dir](widget1, ...) + + util.table.crush(ret, ratio, true) + + ret._private.fill_space = nil + + ret._private.ratios = {} + + return ret +end + +--- Returns a new horizontal ratio layout. A ratio layout shares the available space +-- equally among all widgets. Widgets can be added via :add(widget). +-- @tparam widget ... Widgets that should be added to the layout. +function ratio.horizontal(...) + return get_layout("horizontal", ...) +end + +--- Returns a new vertical ratio layout. A ratio layout shares the available space +-- equally among all widgets. Widgets can be added via :add(widget). +-- @tparam widget ... Widgets that should be added to the layout. +function ratio.vertical(...) + return get_layout("vertical", ...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return ratio + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/rotate.lua b/lib/wibox/layout/rotate.lua new file mode 100644 index 0000000..a6b5d7a --- /dev/null +++ b/lib/wibox/layout/rotate.lua @@ -0,0 +1,17 @@ +--------------------------------------------------------------------------- +-- This class has been moved to `wibox.container.rotate` +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.layout.rotate +--------------------------------------------------------------------------- + +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.container.rotate"), + "wibox.layout.rotate", + "wibox.container.rotate" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/scroll.lua b/lib/wibox/layout/scroll.lua new file mode 100644 index 0000000..e0be79f --- /dev/null +++ b/lib/wibox/layout/scroll.lua @@ -0,0 +1,16 @@ +--------------------------------------------------------------------------- +-- This class has been moved to `wibox.container.scroll` +-- +-- @author Uli Schlachter (based on ideas from Saleur Geoffrey) +-- @copyright 2015 Uli Schlachter +-- @classmod wibox.layout.scroll +--------------------------------------------------------------------------- +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.container.scroll"), + "wibox.layout.scroll", + "wibox.container.scroll" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/stack.lua b/lib/wibox/layout/stack.lua new file mode 100644 index 0000000..bd28703 --- /dev/null +++ b/lib/wibox/layout/stack.lua @@ -0,0 +1,402 @@ +--------------------------------------------------------------------------- +-- A stacked layout. +-- +-- This layout display widgets on top of each other. It can be used to overlay +-- a `wibox.widget.textbox` on top of a `awful.widget.progressbar` or manage +-- "pages" where only one is visible at any given moment. +-- +-- The indices are going from 1 (the bottom of the stack) up to the top of +-- the stack. The order can be changed either using `:swap` or `:raise`. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_layout_defaults_stack.svg) +-- +-- @usage +--wibox.widget { +-- generic_widget( 'first' ), +-- generic_widget( 'second' ), +-- generic_widget( 'third' ), +-- layout = wibox.layout.stack +--} +-- @author Emmanuel Lepage Vallee +-- @copyright 2016 Emmanuel Lepage Vallee +-- @classmod wibox.layout.stack +--------------------------------------------------------------------------- + +local base = require("wibox.widget.base" ) +local fixed = require("wibox.layout.fixed") +local table = table +local pairs = pairs +local util = require("awful.util") + +local stack = {mt={}} + +--Imported documentation + +--- Set a widget at a specific index, replace the current one. +-- **Signal:** widget::replaced The argument is the new widget and the old one +-- and the index. +-- @tparam number index A widget or a widget index +-- @param widget2 The widget to take the place of the first one +-- @treturn boolean If the operation is successful +-- @name set +-- @class function + +--- Replace the first instance of `widget` in the layout with `widget2`. +-- **Signal:** widget::replaced The argument is the new widget and the old one +-- and the index. +-- @param widget The widget to replace +-- @param widget2 The widget to replace `widget` with +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name replace_widget +-- @class function + +--- Swap 2 widgets in a layout. +-- **Signal:** widget::swapped The arguments are both widgets and both (new) indexes. +-- @tparam number index1 The first widget index +-- @tparam number index2 The second widget index +-- @treturn boolean If the operation is successful +-- @name swap +-- @class function + +--- Swap 2 widgets in a layout. +-- If widget1 is present multiple time, only the first instance is swapped +-- **Signal:** widget::swapped The arguments are both widgets and both (new) indexes. +-- if the layouts not the same, then only `widget::replaced` will be emitted. +-- @param widget1 The first widget +-- @param widget2 The second widget +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name swap_widgets +-- @class function + +--- Get all direct children of this layout. +-- @param layout The layout you are modifying. +-- @property children + +--- Reset a ratio layout. This removes all widgets from the layout. +-- **Signal:** widget::reset +-- @param layout The layout you are modifying. +-- @name reset +-- @class function + + +--- Add some widgets to the given stack layout +-- @param layout The layout you are modifying. +-- @tparam widget ... Widgets that should be added (must at least be one) +-- @name add +-- @class function + +--- Remove a widget from the layout +-- @tparam index The widget index to remove +-- @treturn boolean index If the operation is successful +-- @name remove +-- @class function + +--- Insert a new widget in the layout at position `index` +-- @tparam number index The position +-- @param widget The widget +-- @treturn boolean If the operation is successful +-- @name insert +-- @class function + +--- Remove one or more widgets from the layout +-- The last parameter can be a boolean, forcing a recursive seach of the +-- widget(s) to remove. +-- @param widget ... Widgets that should be removed (must at least be one) +-- @treturn boolean If the operation is successful +-- @name remove_widgets +-- @class function + +--- Add spacing between each layout widgets +-- @property spacing +-- @tparam number spacing Spacing between widgets. + +function stack:layout(_, width, height) + local result = {} + local spacing = self._private.spacing + + for _, v in pairs(self._private.widgets) do + table.insert(result, base.place_widget_at(v, spacing, spacing, width - 2*spacing, height - 2*spacing)) + if self._private.top_only then break end + end + + return result +end + +function stack:fit(context, orig_width, orig_height) + local max_w, max_h = 0,0 + local spacing = self._private.spacing + + for _, v in pairs(self._private.widgets) do + local w, h = base.fit_widget(self, context, v, orig_width, orig_height) + max_w, max_h = math.max(max_w, w+2*spacing), math.max(max_h, h+2*spacing) + end + + return math.min(max_w, orig_width), math.min(max_h, orig_height) +end + +--- If only the first stack widget is drawn +-- @property top_only + +function stack:get_top_only() + return self._private.top_only +end + +function stack:set_top_only(top_only) + self._private.top_only = top_only +end + +--- Raise a widget at `index` to the top of the stack +-- @tparam number index the widget index to raise +function stack:raise(index) + if (not index) or self._private.widgets[index] then return end + + local w = self._private.widgets[index] + table.remove(self._private.widgets, index) + table.insert(self._private.widgets, w) + + self:emit_signal("widget::layout_changed") +end + +--- Raise the first instance of `widget` +-- @param widget The widget to raise +-- @tparam[opt=false] boolean recursive Also look deeper in the hierarchy to +-- find the widget +function stack:raise_widget(widget, recursive) + local idx, layout = self:index(widget, recursive) + + if not idx or not layout then return end + + -- Bubble up in the stack until the right index is found + while layout and layout ~= self do + idx, layout = self:index(layout, recursive) + end + + if layout == self and idx ~= 1 then + self:raise(idx) + end +end + +--- Create a new stack layout. +-- @function wibox.layout.stack +-- @treturn widget A new stack layout + +local function new(...) + local ret = fixed.horizontal(...) + + util.table.crush(ret, stack, true) + + return ret +end + +function stack.mt:__call(_, ...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(stack, stack.mt) +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/background.lua b/lib/wibox/widget/background.lua new file mode 100644 index 0000000..3c35a0b --- /dev/null +++ b/lib/wibox/widget/background.lua @@ -0,0 +1,16 @@ +--------------------------------------------------------------------------- +-- This class has been moved to `wibox.container.background` +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.widget.background +--------------------------------------------------------------------------- +local util = require("awful.util") + +return util.deprecate_class( + require("wibox.container.background"), + "wibox.widget.background", + "wibox.container.background" +) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/base.lua b/lib/wibox/widget/base.lua new file mode 100644 index 0000000..dd80ec7 --- /dev/null +++ b/lib/wibox/widget/base.lua @@ -0,0 +1,694 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.widget.base +--------------------------------------------------------------------------- + +local object = require("gears.object") +local cache = require("gears.cache") +local matrix = require("gears.matrix") +local protected_call = require("gears.protected_call") +local util = require("awful.util") +local setmetatable = setmetatable +local pairs = pairs +local type = type +local table = table + +local base = {} + +-- {{{ Functions on widgets + +--- Functions available on all widgets. +base.widget = {} + +--- Set/get a widget's buttons. +-- @tab _buttons The table of buttons that is bound to the widget. +-- @function buttons +function base.widget:buttons(_buttons) + if _buttons then + self._private.widget_buttons = _buttons + end + return self._private.widget_buttons +end + +--- Set a widget's visibility. +-- @tparam boolean b Whether the widget is visible. +-- @function set_visible +function base.widget:set_visible(b) + if b ~= self._private.visible then + self._private.visible = b + self:emit_signal("widget::layout_changed") + -- In case something ignored fit and drew the widget anyway. + self:emit_signal("widget::redraw_needed") + end +end + +--- Is the widget visible? +-- @treturn boolean +-- @function get_visible +function base.widget:get_visible() + return self._private.visible or false +end + +--- Set a widget's opacity. +-- @tparam number o The opacity to use (a number from 0 (transparent) to 1 +-- (opaque)). +-- @function set_opacity +function base.widget:set_opacity(o) + if o ~= self._private.opacity then + self._private.opacity = o + self:emit_signal("widget::redraw") + end +end + +--- Get the widget's opacity. +-- @treturn number The opacity (between 0 (transparent) and 1 (opaque)). +-- @function get_opacity +function base.widget:get_opacity() + return self._private.opacity +end + +--- Set the widget's forced width. +-- @tparam[opt] number width With `nil` the default mechanism of calling the +-- `:fit` method is used. +-- @see fit_widget +-- @function set_forced_width +function base.widget:set_forced_width(width) + if width ~= self._private.forced_width then + self._private.forced_width = width + self:emit_signal("widget::layout_changed") + end +end + +--- Get the widget's forced width. +-- +-- Note that widget instances can be used in different places simultaneously, +-- and therefore can have multiple dimensions. +-- If there is no forced width/height, then the only way to get the widget's +-- actual size is during a `mouse::enter`, `mouse::leave` or button event. +-- @treturn[opt] number The forced width (nil if automatic). +-- @see fit_widget +-- @function get_forced_width +function base.widget:get_forced_width() + return self._private.forced_width +end + +--- Set the widget's forced height. +-- @tparam[opt] number height With `nil` the default mechanism of calling the +-- `:fit` method is used. +-- @see fit_widget +-- @function set_height +function base.widget:set_forced_height(height) + if height ~= self._private.forced_height then + self._private.forced_height = height + self:emit_signal("widget::layout_changed") + end +end + +--- Get the widget's forced height. +-- +-- Note that widget instances can be used in different places simultaneously, +-- and therefore can have multiple dimensions. +-- If there is no forced width/height, then the only way to get the widget's +-- actual size is during a `mouse::enter`, `mouse::leave` or button event. +-- @treturn[opt] number The forced height (nil if automatic). +-- @function get_forced_height +function base.widget:get_forced_height() + return self._private.forced_height +end + +--- Get the widget's direct children widgets. +-- +-- This method should be re-implemented by the relevant widgets. +-- @treturn table The children +-- @function get_children +function base.widget:get_children() + return {} +end + +--- Replace the layout children. +-- +-- The default implementation does nothing, this must be re-implemented by +-- all layout and container widgets. +-- @tab children A table composed of valid widgets. +-- @function set_children +function base.widget:set_children(children) -- luacheck: no unused + -- Nothing on purpose +end + +-- It could have been merged into `get_all_children`, but it's not necessary. +local function digg_children(ret, tlw) + for _, w in ipairs(tlw:get_children()) do + table.insert(ret, w) + digg_children(ret, w) + end +end + +--- Get all direct and indirect children widgets. +-- +-- This will scan all containers recursively to find widgets. +-- +-- *Warning*: This method it prone to stack overflow if the widget, or any of +-- its children, contains (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children +function base.widget:get_all_children() + local ret = {} + digg_children(ret, self) + return ret +end + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- +-- Note that this function has two flaws: +-- +-- 1. The signal is only forwarded once the widget tree has been built. This +-- happens after all currently scheduled functions have been executed. +-- Therefore, it will not start to work right away. +-- 2. In case the widget is present multiple times in a single widget tree, +-- this function will also forward the signal multiple time (one per upward +-- tree path). +-- +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive +function base.widget:emit_signal_recursive(signal_name, ...) + -- This is a convenience wrapper, the real implementation is in the + -- hierarchy. + + self:emit_signal("widget::emit_recursive", signal_name, ...) +end + +--- Get the index of a widget. +-- @tparam widget widget The widget to look for. +-- @tparam[opt] boolean recursive Also check sub-widgets? +-- @tparam[opt] widget ... Additional widgets to add at the end of the "path" +-- @treturn number The index. +-- @treturn widget The parent widget. +-- @treturn table The path between "self" and "widget". +-- @function index +function base.widget:index(widget, recursive, ...) + local widgets = self:get_children() + for idx, w in ipairs(widgets) do + if w == widget then + return idx, self, {...} + elseif recursive then + local child_idx, l, path = w:index(widget, true, self, ...) + if child_idx and l then + return child_idx, l, path + end + end + end + return nil, self, {} +end +-- }}} + +-- {{{ Caches + +-- Indexes are widgets, allow them to be garbage-collected. +local widget_dependencies = setmetatable({}, { __mode = "kv" }) + +-- Get the cache of the given kind for this widget. This returns a gears.cache +-- that calls the callback of kind `kind` on the widget. +local function get_cache(widget, kind) + if not widget._private.widget_caches[kind] then + widget._private.widget_caches[kind] = cache.new(function(...) + return protected_call(widget[kind], widget, ...) + end) + end + return widget._private.widget_caches[kind] +end + +-- Special value to skip the dependency recording that is normally done by +-- base.fit_widget() and base.layout_widget(). The caller must ensure that no +-- caches depend on the result of the call and/or must handle the children's +-- widget::layout_changed signal correctly when using this. +base.no_parent_I_know_what_I_am_doing = {} + +-- Record a dependency from parent to child: The layout of `parent` depends on +-- the layout of `child`. +local function record_dependency(parent, child) + if parent == base.no_parent_I_know_what_I_am_doing then + return + end + + base.check_widget(parent) + base.check_widget(child) + + local deps = widget_dependencies[child] or {} + deps[parent] = true + widget_dependencies[child] = deps +end + +-- Clear the caches for `widget` and all widgets that depend on it. +local clear_caches +function clear_caches(widget) + local deps = widget_dependencies[widget] or {} + widget_dependencies[widget] = {} + widget._private.widget_caches = {} + for w in pairs(deps) do + clear_caches(w) + end +end + +-- }}} + +--- Figure out the geometry in the device coordinate space. +-- +-- This gives only tight bounds if no rotations by non-multiples of 90° are +-- used. +-- @function wibox.widget.base.rect_to_device_geometry +function base.rect_to_device_geometry(cr, x, y, width, height) + return matrix.transform_rectangle(cr.matrix, x, y, width, height) +end + +--- Fit a widget for the given available width and height. +-- +-- This calls the widget's `:fit` callback and caches the result for later use. +-- Never call `:fit` directly, but always through this function! +-- @tparam widget parent The parent widget which requests this information. +-- @tab context The context in which we are fit. +-- @tparam widget widget The widget to fit (this uses +-- `widget:fit(context, width, height)`). +-- @tparam number width The available width for the widget. +-- @tparam number height The available height for the widget. +-- @treturn number The width that the widget wants to use. +-- @treturn number The height that the widget wants to use. +-- @function wibox.widget.base.fit_widget +function base.fit_widget(parent, context, widget, width, height) + record_dependency(parent, widget) + + if not widget._private.visible then + return 0, 0 + end + + -- Sanitize the input. This also filters out e.g. NaN. + width = math.max(0, width) + height = math.max(0, height) + + local w, h = 0, 0 + if widget.fit then + w, h = get_cache(widget, "fit"):get(context, width, height) + else + -- If it has no fit method, calculate based on the size of children + local children = base.layout_widget(parent, context, widget, width, height) + for _, info in ipairs(children or {}) do + local x, y, w2, h2 = matrix.transform_rectangle(info._matrix, + 0, 0, info._width, info._height) + w, h = math.max(w, x + w2), math.max(h, y + h2) + end + end + + -- Apply forced size and handle nil's + w = widget._private.forced_width or w or 0 + h = widget._private.forced_height or h or 0 + + -- Also sanitize the output. + w = math.max(0, math.min(w, width)) + h = math.max(0, math.min(h, height)) + return w, h +end + +--- Lay out a widget for the given available width and height. +-- +-- This calls the widget's `:layout` callback and caches the result for later +-- use. Never call `:layout` directly, but always through this function! +-- However, normally there shouldn't be any reason why you need to use this +-- function. +-- @tparam widget parent The parent widget which requests this information. +-- @tab context The context in which we are laid out. +-- @tparam widget widget The widget to layout (this uses +-- `widget:layout(context, width, height)`). +-- @tparam number width The available width for the widget. +-- @tparam number height The available height for the widget. +-- @treturn[opt] table The result from the widget's `:layout` callback. +-- @function wibox.widget.base.layout_widget +function base.layout_widget(parent, context, widget, width, height) + record_dependency(parent, widget) + + if not widget._private.visible then + return + end + + -- Sanitize the input. This also filters out e.g. NaN. + width = math.max(0, width) + height = math.max(0, height) + + if widget.layout then + return get_cache(widget, "layout"):get(context, width, height) + end +end + +--- Handle a button event on a widget. +-- +-- This is used internally and should not be called directly. +-- @function wibox.widget.base.handle_button +function base.handle_button(event, widget, x, y, button, modifiers, geometry) + x = x or y -- luacheck: no unused + local function is_any(mod) + return #mod == 1 and mod[1] == "Any" + end + + local function tables_equal(a, b) + if #a ~= #b then + return false + end + for k, v in pairs(b) do + if a[k] ~= v then + return false + end + end + return true + end + + -- Find all matching button objects. + local matches = {} + for _, v in pairs(widget._private.widget_buttons) do + local match = true + -- Is it the right button? + if v.button ~= 0 and v.button ~= button then match = false end + -- Are the correct modifiers pressed? + if (not is_any(v.modifiers)) and (not tables_equal(v.modifiers, modifiers)) then match = false end + if match then + table.insert(matches, v) + end + end + + -- Emit the signals. + for _, v in pairs(matches) do + v:emit_signal(event,geometry) + end +end + +--- Create widget placement information. This should be used in a widget's +-- `:layout()` callback. +-- @tparam widget widget The widget that should be placed. +-- @param mat A matrix transforming from the parent widget's coordinate +-- system. For example, use matrix.create_translate(1, 2) to draw a +-- widget at position (1, 2) relative to the parent widget. +-- @tparam number width The width of the widget in its own coordinate system. +-- That is, after applying the transformation matrix. +-- @tparam number height The height of the widget in its own coordinate system. +-- That is, after applying the transformation matrix. +-- @treturn table An opaque object that can be returned from `:layout()`. +-- @function wibox.widget.base.place_widget_via_matrix +function base.place_widget_via_matrix(widget, mat, width, height) + return { + _widget = widget, + _width = width, + _height = height, + _matrix = mat + } +end + +--- Create widget placement information. This should be used for a widget's +-- `:layout()` callback. +-- @tparam widget widget The widget that should be placed. +-- @tparam number x The x coordinate for the widget. +-- @tparam number y The y coordinate for the widget. +-- @tparam number width The width of the widget in its own coordinate system. +-- That is, after applying the transformation matrix. +-- @tparam number height The height of the widget in its own coordinate system. +-- That is, after applying the transformation matrix. +-- @treturn table An opaque object that can be returned from `:layout()`. +-- @function wibox.widget.base.place_widget_at +function base.place_widget_at(widget, x, y, width, height) + return base.place_widget_via_matrix(widget, matrix.create_translate(x, y), width, height) +end + +-- Read the table, separate attributes from widgets. +local function parse_table(t, leave_empty) + local max = 0 + local attributes, widgets = {}, {} + for k,v in pairs(t) do + if type(k) == "number" then + if v then + -- Since `ipairs` doesn't always work on sparse tables, update + -- the maximum. + if k > max then + max = k + end + + widgets[k] = v + end + else + attributes[k] = v + end + end + + -- Pack the sparse table, if the container doesn't support sparse tables. + if not leave_empty then + widgets = util.table.from_sparse(widgets) + max = #widgets + end + + return max, attributes, widgets +end + +-- Recursively build a container from a declarative table. +local function drill(ids, content) + if not content then return end + + -- Alias `widget` to `layout` as they are handled the same way. + content.layout = content.layout or content.widget + + -- Make sure the layout is not indexed on a function. + local layout = type(content.layout) == "function" and content.layout() or content.layout + + -- Create layouts based on metatable's __call. + local l = layout.is_widget and layout or layout() + + -- Get the number of children widgets (including nil widgets). + local max, attributes, widgets = parse_table(content, l.allow_empty_widget) + + -- Get the optional identifier to create a virtual widget tree to place + -- in an "access table" to be able to retrieve the widget. + local id = attributes.id + + -- Clear the internal attributes. + attributes.id, attributes.layout, attributes.widget = nil, nil, nil + + -- Set layout attributes. + -- This has to be done before the widgets are added because it might affect + -- the output. + for attr, val in pairs(attributes) do + if l["set_"..attr] then + l["set_"..attr](l, val) + elseif type(l[attr]) == "function" then + l[attr](l, val) + else + l[attr] = val + end + end + + -- Add all widgets. + for k = 1, max do + -- ipairs cannot be used on sparse tables. + local v, id2, e = widgets[k], id, nil + if v then + -- It is another declarative container, parse it. + if not v.is_widget then + e, id2 = drill(ids, v) + widgets[k] = e + end + base.check_widget(widgets[k]) + + -- Place the widget in the access table. + if id2 then + l [id2] = e + ids[id2] = ids[id2] or {} + table.insert(ids[id2], e) + end + end + end + -- Replace all children (if any) with the new ones. + l:set_children(widgets) + return l, id +end + +-- Only available when the declarative system is used. +local function get_children_by_id(self, name) + if rawget(self, "_private") then + return self._private.by_id[name] or {} + else + return rawget(self, "_by_id")[name] or {} + end +end + +--- Set a declarative widget hierarchy description. +-- +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html). +-- @tab args A table containing the widget's disposition. +-- @function setup +function base.widget:setup(args) + local f,ids = self.set_widget or self.add or self.set_first,{} + local w, id = drill(ids, args) + f(self,w) + if id then + -- Avoid being dropped by wibox metatable -> drawin + rawset(self, id, w) + ids[id] = ids[id] or {} + table.insert(ids[id], 1, w) + end + + if rawget(self, "_private") then + self._private.by_id = ids + else + rawset(self, "_by_id", ids) + end + + rawset(self, "get_children_by_id", get_children_by_id) +end + +--- Create a widget from a declarative description. +-- +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html). +-- @tab args A table containing the widgets disposition. +-- @function wibox.widget.base.make_widget_declarative +function base.make_widget_declarative(args) + local ids = {} + + if (not args.layout) and (not args.widget) then + args.widget = base.make_widget(nil, args.id) + end + + local w, id = drill(ids, args) + + local mt = getmetatable(w) or {} + local orig_string = tostring(w) + + -- Add the main id (if any) + if id then + ids[id] = ids[id] or {} + table.insert(ids[id], 1, w) + end + + if rawget(w, "_private") then + w._private.by_id = ids + else + rawset(w, "_by_id", ids) + end + + rawset(w, "get_children_by_id", get_children_by_id) + + mt.__tostring = function() + return string.format("%s (%s)", id or w.widget_name or "N/A", orig_string) + end + + return setmetatable(w, mt) +end + +--- Create an empty widget skeleton. +-- +-- See [Creating new widgets](../documentation/04-new-widget.md.html). +-- @tparam[opt] widget proxy If this is set, the returned widget will be a +-- proxy for this widget. It will be equivalent to this widget. +-- This means it looks the same on the screen. +-- @tparam[opt] string widget_name Name of the widget. If not set, it will be +-- set automatically via @{gears.object.modulename}. +-- @tparam[opt={}] table args Widget settings +-- @tparam[opt=false] boolean args.enable_properties Enable automatic getter +-- and setter methods. +-- @tparam[opt=nil] table args.class The widget class +-- @see fit_widget +-- @function wibox.widget.base.make_widget +function base.make_widget(proxy, widget_name, args) + args = args or {} + local ret = object { + enable_properties = args.enable_properties, + class = args.class, + } + + -- Backwards compatibility. + -- TODO: Remove this + ret:connect_signal("widget::updated", function() + ret:emit_signal("widget::layout_changed") + ret:emit_signal("widget::redraw_needed") + end) + + -- Create a table used to store the widgets internal data. + rawset(ret, "_private", {}) + + -- No buttons yet. + ret._private.widget_buttons = {} + + -- Widget is visible. + ret._private.visible = true + + -- Widget is fully opaque. + ret._private.opacity = 1 + + -- Differentiate tables from widgets. + rawset(ret, "is_widget", true) + + -- Size is not restricted/forced. + ret._private.forced_width = nil + ret._private.forced_height = nil + + -- Make buttons work. + ret:connect_signal("button::press", function(...) + return base.handle_button("press", ...) + end) + ret:connect_signal("button::release", function(...) + return base.handle_button("release", ...) + end) + + if proxy then + rawset(ret, "fit", function(_, context, width, height) + return base.fit_widget(ret, context, proxy, width, height) + end) + rawset(ret, "layout", function(_, _, width, height) + return { base.place_widget_at(proxy, 0, 0, width, height) } + end) + proxy:connect_signal("widget::layout_changed", function() + ret:emit_signal("widget::layout_changed") + end) + proxy:connect_signal("widget::redraw_needed", function() + ret:emit_signal("widget::redraw_needed") + end) + end + + -- Set up caches. + clear_caches(ret) + ret:connect_signal("widget::layout_changed", function() + clear_caches(ret) + end) + + -- Add functions. + for k, v in pairs(base.widget) do + rawset(ret, k, v) + end + + -- Add __tostring method to metatable. + rawset(ret, "widget_name", widget_name or object.modulename(3)) + local mt = getmetatable(ret) or {} + local orig_string = tostring(ret) + mt.__tostring = function() + return string.format("%s (%s)", ret.widget_name, orig_string) + end + return setmetatable(ret, mt) +end + +--- Generate an empty widget which takes no space and displays nothing. +-- @function wibox.widget.base.empty_widget +function base.empty_widget() + return base.make_widget() +end + +--- Do some sanity checking on a widget. +-- +-- This function raises an error if the widget is not valid. +-- @function wibox.widget.base.check_widget +function base.check_widget(widget) + assert(type(widget) == "table", "Type should be table, but is " .. tostring(type(widget))) + assert(widget.is_widget, "Argument is not a widget!") + for _, func in pairs({ "connect_signal", "disconnect_signal" }) do + assert(type(widget[func]) == "function", func .. " is not a function") + end +end + +return base + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/checkbox.lua b/lib/wibox/widget/checkbox.lua new file mode 100644 index 0000000..57bdaa3 --- /dev/null +++ b/lib/wibox/widget/checkbox.lua @@ -0,0 +1,530 @@ +--------------------------------------------------------------------------- +-- A boolean display widget. +-- +-- If necessary, themes can implement custom shape: +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_checkbox_custom.svg) +-- +-- +-- wibox.widget { +-- checked = true, +-- color = beautiful.bg_normal, +-- paddings = 2, +-- check_shape = function(cr, width, height) +-- local rs = math.min(width, height) +-- cr:move_to( 0 , 0 ) +-- cr:line_to( rs , 0 ) +-- cr:move_to( 0 , 0 ) +-- cr:line_to( 0 , rs ) +-- cr:move_to( 0 , rs ) +-- cr:line_to( rs , rs ) +-- cr:move_to( rs , 0 ) +-- cr:line_to( rs , rs ) +-- cr:move_to( 0 , 0 ) +-- cr:line_to( rs , rs ) +-- cr:move_to( 0 , rs ) +-- cr:line_to( rs , 0 ) +-- end, +-- check_border_color = '#ff0000', +-- check_color = '#00000000', +-- check_border_width = 1, +-- widget = wibox.widget.checkbox +-- } +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_defaults_checkbox.svg) +-- +-- @usage +--wibox.widget { +-- checked = true, +-- color = beautiful.bg_normal, +-- paddings = 2, +-- shape = gears.shape.circle, +-- widget = wibox.widget.checkbox +--} +-- @author Emmanuel Lepage Valle +-- @copyright 2010 Emmanuel Lepage Vallee +-- @classmod wibox.widget.checkbox +--------------------------------------------------------------------------- + +local color = require( "gears.color" ) +local base = require( "wibox.widget.base" ) +local beautiful = require( "beautiful" ) +local shape = require( "gears.shape" ) +local util = require( "awful.util" ) + +local checkbox = {} + +--- The outer (unchecked area) border width. +-- @beautiful beautiful.checkbox_border_width + +--- The outer (unchecked area) background color, pattern or gradient. +-- @beautiful beautiful.checkbox_bg + +--- The outer (unchecked area) border color. +-- @beautiful beautiful.checkbox_border_color + +--- The checked part border color. +-- @beautiful beautiful.checkbox_check_border_color + +--- The checked part border width. +-- @beautiful beautiful.checkbox_check_border_width + +--- The checked part filling color. +-- @beautiful beautiful.checkbox_check_color + +--- The outer (unchecked area) shape. +-- @beautiful beautiful.checkbox_shape +-- @see gears.shape + +--- The checked part shape. +-- If none is set, then the `shape` property will be used. +-- @beautiful beautiful.checkbox_check_shape +-- @see gears.shape + +--- The padding between the outline and the progressbar. +-- @beautiful beautiful.checkbox_paddings +-- @tparam[opt=0] table|number paddings A number or a table +-- @tparam[opt=0] number paddings.top +-- @tparam[opt=0] number paddings.bottom +-- @tparam[opt=0] number paddings.left +-- @tparam[opt=0] number paddings.right + +--- The checkbox color. +-- This will be used for the unchecked part border color and the checked part +-- filling color. Note that `check_color` and `border_color` have priority +-- over this property. +-- @beautiful beautiful.checkbox_color + +--- The outer (unchecked area) border width. +-- @property border_width + +--- The outer (unchecked area) background color, pattern or gradient. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_checkbox_bg.svg) +-- +-- @usage +--wibox.widget { +-- checked = true, +-- color = beautiful.bg_normal, +-- bg = '#ff00ff', +-- border_width = 3, +-- paddings = 4, +-- border_color = '#0000ff', +-- check_color = '#ff0000', +-- check_border_color = '#ffff00', +-- check_border_width = 1, +-- widget = wibox.widget.checkbox +--} +-- @property bg + +--- The outer (unchecked area) border color. +-- @property border_color + +--- The checked part border color. +-- @property check_border_color + +--- The checked part border width. +-- @property check_border_width + +--- The checked part filling color. +-- @property check_color + +--- The outer (unchecked area) shape. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_checkbox_shape.svg) +-- +-- @usage +--for _, s in ipairs {'rectangle', 'circle', 'losange', 'octogon'} do +-- wibox.widget { +-- checked = true, +-- color = beautiful.bg_normal, +-- paddings = 2, +-- shape = gears.shape[s], +-- widget = wibox.widget.checkbox +-- } +--end +-- @property shape +-- @see gears.shape + +--- The checked part shape. +-- If none is set, then the `shape` property will be used. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_checkbox_check_shape.svg) +-- +-- @usage +--for _, s in ipairs {'rectangle', 'circle', 'losange', 'octogon'} do +-- wibox.widget { +-- checked = true, +-- color = beautiful.bg_normal, +-- paddings = 2, +-- check_shape = gears.shape[s], +-- widget = wibox.widget.checkbox +-- } +--end +-- @property check_shape +-- @see gears.shape + +--- The padding between the outline and the progressbar. +-- @property paddings +-- @tparam[opt=0] table|number paddings A number or a table +-- @tparam[opt=0] number paddings.top +-- @tparam[opt=0] number paddings.bottom +-- @tparam[opt=0] number paddings.left +-- @tparam[opt=0] number paddings.right + +--- The checkbox color. +-- This will be used for the unchecked part border color and the checked part +-- filling color. Note that `check_color` and `border_color` have priority +-- over this property. +-- @property color + +local function outline_workarea(self, width, height) + local offset = (self._private.border_width or + beautiful.checkbox_border_width or 1)/2 + + return { + x = offset, + y = offset, + width = width-2*offset, + height = height-2*offset + } +end + +-- The child widget area +local function content_workarea(self, width, height) + local padding = self._private.paddings or {} + local offset = self:get_check_border_width() or 0 + local wa = outline_workarea(self, width, height) + + wa.x = offset + wa.x + (padding.left or 1) + wa.y = offset + wa.y + (padding.top or 1) + wa.width = wa.width - (padding.left or 1) - (padding.right or 1) - 2*offset + wa.height = wa.height - (padding.top or 1) - (padding.bottom or 1) - 2*offset + + return wa +end + +local function draw(self, _, cr, width, height) + local size = math.min(width, height) + + local background_shape = self:get_shape() or shape.rectangle + local border_width = self:get_border_width() or 1 + + local main_color = self:get_color() + local bg = self:get_bg() + local border_color = self:get_border_color() + + -- If no color is set, it will fallback to the default one + if border_color or main_color then + cr:set_source(color(border_color or main_color)) + end + + local wa = outline_workarea(self, size, size) + cr:translate(wa.x, wa.y) + background_shape(cr, wa.width, wa.height) + cr:set_line_width(border_width) + + if bg then + cr:save() + cr:set_source(color(bg)) + cr:fill_preserve() + cr:restore() + end + + cr:stroke() + + cr:translate(-wa.x, -wa.y) + + -- Draw the checked part + if self._private.checked then + local col = self:get_check_color() or main_color + border_color = self:get_check_border_color() + border_width = self:get_check_border_width() or 0 + local check_shape = self:get_check_shape() or background_shape + + wa = content_workarea(self, size, size) + cr:translate(wa.x, wa.y) + + check_shape(cr, wa.width, wa.height) + + if col then + cr:set_source(color(col)) + end + + if border_width > 0 then + cr:fill_preserve() + cr:set_line_width(border_width) + cr:set_source(color(border_color)) + cr:stroke() + else + cr:fill() + end + end +end + +local function fit(_, _, w, h) + local size = math.min(w, h) + return size, size +end + +--- If the checkbox is checked. +-- @property checked +-- @param boolean + +for _, prop in ipairs {"border_width", "bg", "border_color", "check_border_color", + "check_border_width", "check_color", "shape", "check_shape", "paddings", + "checked", "color" } do + checkbox["set_"..prop] = function(self, value) + self._private[prop] = value + self:emit_signal("property::"..prop) + self:emit_signal("widget::redraw_needed") + end + checkbox["get_"..prop] = function(self) + return self._private[prop] or beautiful["checkbox_"..prop] + end +end + +--- The checkbox color. +-- @property color + +function checkbox:set_paddings(val) + self._private.paddings = type(val) == "number" and { + left = val, + right = val, + top = val, + bottom = val, + } or val or {} + self:emit_signal("property::paddings") + self:emit_signal("widget::redraw_needed") +end + +local function new(checked, args) + checked, args = checked or false, args or {} + + local ret = base.make_widget(nil, nil, { + enable_properties = true, + }) + + util.table.crush(ret, checkbox) + + ret._private.checked = checked + ret._private.color = args.color and color(args.color) or nil + + rawset(ret, "fit" , fit ) + rawset(ret, "draw", draw) + + return ret +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable({}, { __call = function(_, ...) return new(...) end}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/graph.lua b/lib/wibox/widget/graph.lua new file mode 100644 index 0000000..c03c726 --- /dev/null +++ b/lib/wibox/widget/graph.lua @@ -0,0 +1,575 @@ +--------------------------------------------------------------------------- +--- A graph widget. +-- +-- The graph goes from left to right. To change this to right to left, use +-- a `wibox.container.mirror` widget. This can also be used to have data +-- shown from top to bottom. +-- +-- To add text on top of the graph, use a `wibox.layout.stack` and a +-- `wibox.container.align` widgets. +-- +-- To display the graph vertically, use a `wibox.container.rotate` widget. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_defaults_graph.svg) +-- +-- @usage +--wibox.widget { +-- max_value = 29, +-- widget = wibox.widget.graph +--} +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @classmod wibox.widget.graph +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local ipairs = ipairs +local math = math +local table = table +local type = type +local color = require("gears.color") +local base = require("wibox.widget.base") +local beautiful = require("beautiful") + +local graph = { mt = {} } + +--- Set the graph border color. +-- If the value is nil, no border will be drawn. +-- +-- @property border_color +-- @tparam gears.color border_color The border color to set. +-- @see gears.color + +--- Set the graph foreground color. +-- +-- @property color +-- @tparam color color The graph color. +-- @see gears.color + +--- Set the graph background color. +-- +-- @property background_color +-- @tparam gears.color background_color The graph background color. +-- @see gears.color + +--- Set the maximum value the graph should handle. +-- If "scale" is also set, the graph never scales up below this value, but it +-- automatically scales down to make all data fit. +-- +-- @property max_value +-- @param number + +--- The minimum value. +-- Note that the min_value is not supported when used along with the stack +-- property. +-- @property min_value +-- @param number + +--- Set the graph to automatically scale its values. Default is false. +-- +-- @property scale +-- @param boolean + +--- Set the width or the individual steps. +-- +-- Note that it isn't supported when used along with stacked graphs. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_graph_step.svg) +-- +-- @usage +--wibox.widget { +-- max_value = 29, +-- step_width = 3, +-- step_spacing = 1, +-- step_shape = function(cr, width, height) +-- gears.shape.rounded_rect(cr, width, height, 2) +-- end, +-- widget = wibox.widget.graph +--} +-- +-- @property step_width +-- @param[opt=1] number + +--- Set the spacing between the steps. +-- +-- Note that it isn't supported when used along with stacked graphs. +-- +-- @property step_spacing +-- @param[opt=0] number + +--- The step shape. +-- @property step_shape +-- @param[opt=rectangle] shape +-- @see gears.shape + +--- Set the graph to draw stacks. Default is false. +-- +-- @property stack +-- @param boolean + +--- Set the graph stacking colors. Order matters. +-- +-- @property stack_colors +-- @param stack_colors A table with stacking colors. + +--- The graph background color. +-- @beautiful beautiful.graph_bg + +--- The graph foreground color. +-- @beautiful beautiful.graph_fg + +--- The graph border color. +-- @beautiful beautiful.graph_border_color + +local properties = { "width", "height", "border_color", "stack", + "stack_colors", "color", "background_color", + "max_value", "scale", "min_value", "step_shape", + "step_spacing", "step_width" } + +function graph.draw(_graph, _, cr, width, height) + local max_value = _graph._private.max_value + local min_value = _graph._private.min_value or ( + _graph._private.scale and math.huge or 0) + local values = _graph._private.values + + local step_shape = _graph._private.step_shape + local step_spacing = _graph._private.step_spacing or 0 + local step_width = _graph._private.step_width or 1 + + cr:set_line_width(1) + + -- Draw the background first + cr:set_source(color(_graph._private.background_color or beautiful.graph_bg or "#000000aa")) + cr:paint() + + -- Account for the border width + cr:save() + if _graph._private.border_color then + cr:translate(1, 1) + width, height = width - 2, height - 2 + end + + -- Draw a stacked graph + if _graph._private.stack then + + if _graph._private.scale then + for _, v in ipairs(values) do + for _, sv in ipairs(v) do + if sv > max_value then + max_value = sv + end + if min_value > sv then + min_value = sv + end + end + end + end + + for i = 0, width do + local rel_i = 0 + local rel_x = i + 0.5 + + if _graph._private.stack_colors then + for idx, col in ipairs(_graph._private.stack_colors) do + local stack_values = values[idx] + if stack_values and i < #stack_values then + local value = stack_values[#stack_values - i] + rel_i + cr:move_to(rel_x, height * (1 - (rel_i / max_value))) + cr:line_to(rel_x, height * (1 - (value / max_value))) + cr:set_source(color(col or beautiful.graph_fg or "#ff0000")) + cr:stroke() + rel_i = value + end + end + end + end + else + if _graph._private.scale then + for _, v in ipairs(values) do + if v > max_value then + max_value = v + end + if min_value > v then + min_value = v + end + end + end + + -- Draw the background on no value + if #values ~= 0 then + -- Draw reverse + for i = 0, #values - 1 do + local value = values[#values - i] + if value >= 0 then + local x = i*step_width + ((i-1)*step_spacing) + 0.5 + value = (value - min_value) / max_value + cr:move_to(x, height * (1 - value)) + + if step_shape then + cr:translate(step_width + (i>1 and step_spacing or 0), height * (1 - value)) + step_shape(cr, step_width, height) + cr:translate(0, -(height * (1 - value))) + elseif step_width > 1 then + cr:rectangle(x, height * (1 - value), step_width, height) + else + cr:line_to(x, height) + end + end + end + cr:set_source(color(_graph._private.color or beautiful.graph_fg or "#ff0000")) + + if step_shape or step_width > 1 then + cr:fill() + else + cr:stroke() + end + end + + end + + -- Undo the cr:translate() for the border and step shapes + cr:restore() + + -- Draw the border last so that it overlaps already drawn values + if _graph._private.border_color then + -- We decremented these by two above + width, height = width + 2, height + 2 + + -- Draw the border + cr:rectangle(0.5, 0.5, width - 1, height - 1) + cr:set_source(color(_graph._private.border_color or beautiful.graph_border_color or "#ffffff")) + cr:stroke() + end +end + +function graph.fit(_graph) + return _graph._private.width, _graph._private.height +end + +--- Add a value to the graph +-- +-- @param value The value to be added to the graph +-- @param group The stack color group index. +function graph:add_value(value, group) + value = value or 0 + local values = self._private.values + local max_value = self._private.max_value + value = math.max(0, value) + if not self._private.scale then + value = math.min(max_value, value) + end + + if self._private.stack and group then + if not self._private.values[group] + or type(self._private.values[group]) ~= "table" + then + self._private.values[group] = {} + end + values = self._private.values[group] + end + table.insert(values, value) + + local border_width = 0 + if self._private.border_color then border_width = 2 end + + -- Ensure we never have more data than we can draw + while #values > self._private.width - border_width do + table.remove(values, 1) + end + + self:emit_signal("widget::redraw_needed") + return self +end + +--- Clear the graph. +function graph:clear() + self._private.values = {} + self:emit_signal("widget::redraw_needed") + return self +end + +--- Set the graph height. +-- @param height The height to set. +function graph:set_height(height) + if height >= 5 then + self._private.height = height + self:emit_signal("widget::layout_changed") + end + return self +end + +--- Set the graph width. +-- @param width The width to set. +function graph:set_width(width) + if width >= 5 then + self._private.width = width + self:emit_signal("widget::layout_changed") + end + return self +end + +-- Build properties function +for _, prop in ipairs(properties) do + if not graph["set_" .. prop] then + graph["set_" .. prop] = function(_graph, value) + if _graph._private[prop] ~= value then + _graph._private[prop] = value + _graph:emit_signal("widget::redraw_needed") + end + return _graph + end + end + if not graph["get_" .. prop] then + graph["get_" .. prop] = function(_graph) + return _graph._private[prop] + end + end +end + +--- Create a graph widget. +-- @param args Standard widget() arguments. You should add width and height +-- key to set graph geometry. +-- @return A new graph widget. +-- @function wibox.widget.graph +function graph.new(args) + args = args or {} + + local width = args.width or 100 + local height = args.height or 20 + + if width < 5 or height < 5 then return end + + local _graph = base.make_widget(nil, nil, {enable_properties = true}) + + _graph._private.width = width + _graph._private.height = height + _graph._private.values = {} + _graph._private.max_value = 1 + + -- Set methods + _graph.add_value = graph["add_value"] + _graph.clear = graph["clear"] + _graph.draw = graph.draw + _graph.fit = graph.fit + + for _, prop in ipairs(properties) do + _graph["set_" .. prop] = graph["set_" .. prop] + _graph["get_" .. prop] = graph["get_" .. prop] + end + + return _graph +end + +function graph.mt:__call(...) + return graph.new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(graph, graph.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/imagebox.lua b/lib/wibox/widget/imagebox.lua new file mode 100644 index 0000000..bdfa499 --- /dev/null +++ b/lib/wibox/widget/imagebox.lua @@ -0,0 +1,395 @@ +--------------------------------------------------------------------------- +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_defaults_imagebox.svg) +-- +-- @usage +--wibox.widget { +-- image = beautiful.awesome_icon, +-- resize = false, +-- widget = wibox.widget.imagebox +--} +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.widget.imagebox +--------------------------------------------------------------------------- + +local base = require("wibox.widget.base") +local surface = require("gears.surface") +local util = require("awful.util") +local setmetatable = setmetatable +local type = type +local print = print +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +local imagebox = { mt = {} } + +-- Draw an imagebox with the given cairo context in the given geometry. +function imagebox:draw(_, cr, width, height) + if not self._private.image then return end + if width == 0 or height == 0 then return end + + if not self._private.resize_forbidden then + -- Let's scale the image so that it fits into (width, height) + local w = self._private.image:get_width() + local h = self._private.image:get_height() + local aspect = width / w + local aspect_h = height / h + if aspect > aspect_h then aspect = aspect_h end + + cr:scale(aspect, aspect) + end + + -- Set the clip + if self._private.clip_shape then + cr:clip(self._private.clip_shape(cr, width, height, unpack(self._private.clip_args))) + end + + cr:set_source_surface(self._private.image, 0, 0) + cr:paint() +end + +-- Fit the imagebox into the given geometry +function imagebox:fit(_, width, height) + if not self._private.image then + return 0, 0 + end + + local w = self._private.image:get_width() + local h = self._private.image:get_height() + + if w > width then + h = h * width / w + w = width + end + if h > height then + w = w * height / h + h = height + end + + if h == 0 or w == 0 then + return 0, 0 + end + + if not self._private.resize_forbidden then + local aspect = width / w + local aspect_h = height / h + + -- Use the smaller one of the two aspect ratios. + if aspect > aspect_h then aspect = aspect_h end + + w, h = w * aspect, h * aspect + end + + return w, h +end + +--- Set an imagebox' image +-- @property image +-- @param image Either a string or a cairo image surface. A string is +-- interpreted as the path to a png image file. +-- @return true on success, false if the image cannot be used + +function imagebox:set_image(image) + if type(image) == "string" then + image = surface.load(image) + if not image then + print(debug.traceback()) + return false + end + end + + image = surface.load(image) + + if image then + local w = image.width + local h = image.height + if w <= 0 or h <= 0 then + return false + end + end + + if self._private.image == image then + -- The image could have been modified, so better redraw + self:emit_signal("widget::redraw_needed") + return + end + + self._private.image = image + + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + return true +end + +--- Set a clip shape for this imagebox +-- A clip shape define an area where the content is displayed and one where it +-- is trimmed. +-- +-- @property clip_shape +-- @param clip_shape A `gears_shape` compatible shape function +-- @see gears.shape +-- @see set_clip_shape + +--- Set a clip shape for this imagebox +-- A clip shape define an area where the content is displayed and one where it +-- is trimmed. +-- +-- Any other parameters will be passed to the clip shape function +-- +-- @param clip_shape A `gears_shape` compatible shape function +-- @see gears.shape +-- @see clip_shape +function imagebox:set_clip_shape(clip_shape, ...) + self._private.clip_shape = clip_shape + self._private.clip_args = {...} + self:emit_signal("widget::redraw_needed") +end + +--- Should the image be resized to fit into the available space? +-- @property resize +-- @param allowed If false, the image will be clipped, else it will be resized +-- to fit into the available space. + +function imagebox:set_resize(allowed) + self._private.resize_forbidden = not allowed + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") +end + +--- Returns a new imagebox. +-- Any other arguments will be passed to the clip shape function +-- @param image the image to display, may be nil +-- @param resize_allowed If false, the image will be clipped, else it will be resized +-- to fit into the available space. +-- @param clip_shape A `gears.shape` compatible function +-- @treturn table A new `imagebox` +-- @function wibox.widget.imagebox +local function new(image, resize_allowed, clip_shape) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, imagebox, true) + + if image then + ret:set_image(image) + end + if resize_allowed ~= nil then + ret:set_resize(resize_allowed) + end + + ret._private.clip_shape = clip_shape + ret._private.clip_args = {} + + return ret +end + +function imagebox.mt:__call(...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(imagebox, imagebox.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/init.lua b/lib/wibox/widget/init.lua new file mode 100644 index 0000000..43c6fc0 --- /dev/null +++ b/lib/wibox/widget/init.lua @@ -0,0 +1,22 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.widget +--------------------------------------------------------------------------- +local base = require("wibox.widget.base") + +return setmetatable({ + base = base; + textbox = require("wibox.widget.textbox"); + imagebox = require("wibox.widget.imagebox"); + background = require("wibox.widget.background"); + systray = require("wibox.widget.systray"); + textclock = require("wibox.widget.textclock"); + progressbar = require("wibox.widget.progressbar"); + graph = require("wibox.widget.graph"); + checkbox = require("wibox.widget.checkbox"); + piechart = require("wibox.widget.piechart"); + slider = require("wibox.widget.slider"); +}, {__call = function(_, args) return base.make_widget_declarative(args) end}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/piechart.lua b/lib/wibox/widget/piechart.lua new file mode 100644 index 0000000..5a1c185 --- /dev/null +++ b/lib/wibox/widget/piechart.lua @@ -0,0 +1,467 @@ +--------------------------------------------------------------------------- +-- Display percentage in a circle. +-- +-- Note that this widget makes no attempts to prevent overlapping labels or +-- labels drawn outside of the widget boundaries. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_defaults_piechart.svg) +-- +-- @usage +--wibox.widget { +-- data_list = { +-- { 'L1', 100 }, +-- { 'L2', 200 }, +-- { 'L3', 300 }, +-- }, +-- border_width = 1, +-- colors = { +-- beautiful.bg_normal, +-- beautiful.bg_highlight, +-- beautiful.border_color, +-- }, +-- widget = wibox.widget.piechart +--} +-- @author Emmanuel Lepage Valle +-- @copyright 2012 Emmanuel Lepage Vallee +-- @classmod wibox.widget.piechart +--------------------------------------------------------------------------- + +local color = require( "gears.color" ) +local base = require( "wibox.widget.base" ) +local beautiful = require( "beautiful" ) +local util = require( "awful.util" ) +local pie = require( "gears.shape" ).pie +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +local module = {} + +local piechart = {} + +local function draw_label(cr,angle,radius,center_x,center_y,text) + local edge_x = center_x+(radius/2)*math.cos(angle) + local edge_y = center_y+(radius/2)*math.sin(angle) + + cr:move_to(edge_x, edge_y) + + cr:rel_line_to(radius*math.cos(angle), radius*math.sin(angle)) + + local x,y = cr:get_current_point() + + cr:rel_line_to(x > center_x and radius/2 or -radius/2, 0) + + local ext = cr:text_extents(text) + + cr:rel_move_to( + (x>center_x and radius/2.5 or (-radius/2.5 - ext.width)), + ext.height/2 + ) + + cr:show_text(text) --TODO eventually port away from the toy API + cr:stroke() + + cr:arc(edge_x, edge_y,2,0,2*math.pi) + cr:arc(x+(x>center_x and radius/2 or -radius/2),y,2,0,2*math.pi) + + cr:fill() +end + +local function compute_sum(data) + local ret = 0 + for _, entry in ipairs(data) do + ret = ret + entry[2] + end + + return ret +end + +local function draw(self, _, cr, width, height) + if not self._private.data_list then return end + + local radius = (height > width and width or height) / 4 + local sum, start, count = compute_sum(self._private.data_list),0,0 + local has_label = self._private.display_labels ~= false + + -- Labels need to be drawn later so the original source is kept + -- use get_source() wont work are the reference cannot be set from Lua(?) + local labels = {} + + local border_width = self:get_border_width() or 1 + local border_color = self:get_border_color() + border_color = border_color and color(border_color) + + -- Draw the pies + cr:save() + cr:set_line_width(border_width) + + -- Alternate from a given sets or colors + local colors = self:get_colors() + local col_count = colors and #colors or 0 + + for _, entry in ipairs(self._private.data_list) do + local k, v = entry[1], entry[2] + local end_angle = start + 2*math.pi*(v/sum) + + local col = colors and color(colors[math.fmod(count,col_count)+1]) or nil + + pie(cr, width, height, start, end_angle, radius) + + if col then + cr:save() + cr:set_source(color(col)) + end + + if border_width > 0 then + if col then + cr:fill_preserve() + cr:restore() + end + + -- By default, it uses the fg color + if border_color then + cr:set_source(border_color) + end + cr:stroke() + elseif col then + cr:fill() + cr:restore() + end + + -- Store the label position for later + if has_label then + table.insert(labels, { + --[[angle ]] start+(end_angle-start)/2, + --[[radius ]] radius, + --[[center_x]] width/2, + --[[center_y]] height/2, + --[[text ]] k, + }) + end + start,count = end_angle,count+1 + end + cr:restore() + + -- Draw the labels + if has_label then + for _, v in ipairs(labels) do + draw_label(cr, unpack(v)) + end + end +end + +local function fit(_, _, width, height) + return width, height +end + +--- The pie chart data list. +-- @property data_list +-- @tparam table data_list Sorted table where each entry has a label as its +-- first value and a number as its second value. + +--- The pie chart data. +-- @property data +-- @tparam table data Labels as keys and number as value. + +--- The border color. +-- If none is set, it will use current foreground (text) color. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_piechart_border_color.svg) +-- +-- @property border_color +-- @param color +-- @see gears.color + +--- The pie elements border width. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_piechart_border_width.svg) +-- +-- @property border_width +-- @tparam[opt=1] number border_width + +--- The pie chart colors. +-- If no color is set, only the border will be drawn. If less colors than +-- required are set, colors will be re-used in order. +-- @property colors +-- @tparam table colors A table of colors, one for each elements +-- @see gears.color + +--- The border color. +-- If none is set, it will use current foreground (text) color. +-- @beautiful beautiful.piechart_border_color +-- @param color +-- @see gears.color + +--- If the pie chart has labels. +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_piechart_label.svg) +-- +-- @property display_labels +-- @param[opt=true] boolean + +--- The pie elements border width. +-- @beautiful beautiful.piechart_border_width +-- @tparam[opt=1] number border_width + +--- The pie chart colors. +-- If no color is set, only the border will be drawn. If less colors than +-- required are set, colors will be re-used in order. +-- @beautiful beautiful.piechart_colors +-- @tparam table colors A table of colors, one for each elements +-- @see gears.color + +for _, prop in ipairs {"data_list", "border_color", "border_width", "colors", + "display_labels" + } do + piechart["set_"..prop] = function(self, value) + self._private[prop] = value + self:emit_signal("property::"..prop) + if prop == "data_list" then + self:emit_signal("property::data") + end + self:emit_signal("widget::redraw_needed") + end + piechart["get_"..prop] = function(self) + return self._private[prop] or beautiful["piechart_"..prop] + end +end + +function piechart:set_data(value) + local list = {} + for k, v in pairs(value) do + table.insert(list, { k, v }) + end + self:set_data_list(list) +end + +function piechart:get_data() + local list = {} + for _, entry in ipairs(self:get_data_list()) do + list[entry[1]] = entry[2] + end + return list +end + +local function new(data_list) + + local ret = base.make_widget(nil, nil, { + enable_properties = true, + }) + + util.table.crush(ret, piechart) + + rawset(ret, "fit" , fit ) + rawset(ret, "draw", draw) + + ret:set_data_list(data_list) + + return ret +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(module, { __call = function(_, ...) return new(...) end }) +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/progressbar.lua b/lib/wibox/widget/progressbar.lua new file mode 100644 index 0000000..943c49f --- /dev/null +++ b/lib/wibox/widget/progressbar.lua @@ -0,0 +1,754 @@ +--------------------------------------------------------------------------- +--- A progressbar widget. +-- +-- To add text on top of the progressbar, a `wibox.layout.stack` can be used: +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_progressbar_text.svg) +-- +-- +-- wibox.widget { +-- { +-- max_value = 1, +-- value = 0.5, +-- forced_height = 20, +-- forced_width = 100, +-- paddings = 1, +-- border_width = 1, +-- border_color = beautiful.border_color, +-- widget = wibox.widget.progressbar, +-- }, +-- { +-- text = '50%', +-- widget = wibox.widget.textbox, +-- }, +-- layout = wibox.layout.stack +-- } +-- +-- To display the progressbar vertically, use a `wibox.container.rotate` widget: +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_progressbar_vertical.svg) +-- +-- +-- wibox.widget { +-- { +-- max_value = 1, +-- value = 0.33, +-- widget = wibox.widget.progressbar, +-- }, +-- forced_height = 100, +-- forced_width = 20, +-- direction = 'east', +-- layout = wibox.container.rotate, +-- } +-- +-- By default, this widget will take all the available size. To prevent this, +-- a `wibox.container.constraint` widget or the `forced_width`/`forced_height` +-- properties have to be used. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_defaults_progressbar.svg) +-- +-- @usage +--wibox.widget { +-- max_value = 1, +-- value = 0.33, +-- forced_height = 20, +-- forced_width = 100, +-- shape = gears.shape.rounded_bar, +-- border_width = 2, +-- border_color = beautiful.border_color, +-- widget = wibox.widget.progressbar, +--} +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @classmod wibox.widget.progressbar +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local ipairs = ipairs +local math = math +local util = require("awful.util") +local base = require("wibox.widget.base") +local color = require("gears.color") +local beautiful = require("beautiful") +local shape = require("gears.shape") + +local progressbar = { mt = {} } + +--- The progressbar border color. +-- If the value is nil, no border will be drawn. +-- +-- @property border_color +-- @tparam gears.color color The border color to set. +-- @see gears.color + +--- The progressbar border width. +-- @property border_width + +--- The progressbar inner border color. +-- If the value is nil, no border will be drawn. +-- +-- @property bar_border_color +-- @tparam gears.color color The border color to set. +-- @see gears.color + +--- The progressbar inner border width. +-- @property bar_border_width + +--- The progressbar foreground color. +-- +-- @property color +-- @tparam gears.color color The progressbar color. +-- @see gears.color + +--- The progressbar background color. +-- +-- @property background_color +-- @tparam gears.color color The progressbar background color. +-- @see gears.color + +--- The progressbar inner shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_progressbar_bar_shape.svg) +-- +-- @usage +--for _, shape in ipairs {'rounded_bar', 'octogon', 'hexagon', 'powerline' } do +-- l:add(wibox.widget { +-- value = 0.33, +-- bar_shape = gears.shape[shape], +-- bar_border_color = beautiful.border_color, +-- bar_border_width = 1, +-- border_width = 2, +-- border_color = beautiful.border_color, +-- paddings = 1, +-- widget = wibox.widget.progressbar, +-- }) +--end +-- +-- @property bar_shape +-- @tparam[opt=gears.shape.rectangle] gears.shape shape +-- @see gears.shape + +--- The progressbar shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_progressbar_shape.svg) +-- +-- @usage +--for _, shape in ipairs {'rounded_bar', 'octogon', 'hexagon', 'powerline' } do +-- l:add(wibox.widget { +-- value = 0.33, +-- shape = gears.shape[shape], +-- border_width = 2, +-- border_color = beautiful.border_color, +-- widget = wibox.widget.progressbar, +-- }) +--end +-- +-- @property shape +-- @tparam[opt=gears.shape.rectangle] gears.shape shape +-- @see gears.shape + +--- Set the progressbar to draw vertically. +-- This doesn't do anything anymore, use a `wibox.container.rotate` widget. +-- @deprecated set_vertical +-- @tparam boolean vertical + +--- Force the inner part (the bar) to fit in the background shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_progressbar_clip.svg) +-- +-- @usage +--wibox.widget { +-- value = 75, +-- max_value = 100, +-- border_width = 2, +-- border_color = beautiful.border_color, +-- color = beautiful.border_color, +-- shape = gears.shape.rounded_bar, +-- bar_shape = gears.shape.rounded_bar, +-- clip = false, +-- forced_height = 30, +-- forced_width = 100, +-- paddings = 5, +-- margins = { +-- top = 12, +-- bottom = 12, +-- }, +-- widget = wibox.widget.progressbar, +--} +-- +-- @property clip +-- @tparam[opt=true] boolean clip + +--- The progressbar to draw ticks. Default is false. +-- +-- @property ticks +-- @param boolean + +--- The progressbar ticks gap. +-- +-- @property ticks_gap +-- @param number + +--- The progressbar ticks size. +-- +-- @property ticks_size +-- @param number + +--- The maximum value the progressbar should handle. +-- +-- @property max_value +-- @param number + +--- The progressbar background color. +-- @beautiful beautiful.progressbar_bg + +--- The progressbar foreground color. +-- @beautiful beautiful.progressbar_fg + +--- The progressbar shape. +-- @beautiful beautiful.progressbar_shape +-- @see gears.shape + +--- The progressbar border color. +-- @beautiful beautiful.progressbar_border_color + +--- The progressbar outer border width. +-- @beautiful beautiful.progressbar_border_width + +--- The progressbar inner shape. +-- @beautiful beautiful.progressbar_bar_shape +-- @see gears.shape + +--- The progressbar bar border width. +-- @beautiful beautiful.progressbar_bar_border_width + +--- The progressbar bar border color. +-- @beautiful beautiful.progressbar_bar_border_color + +--- The progressbar margins. +-- Note that if the `clip` is disabled, this allows the background to be smaller +-- than the bar. +-- +-- See the `clip` example. +-- +-- @tparam[opt=0] (table|number|nil) margins A table for each side or a number +-- @tparam[opt=0] number margins.top +-- @tparam[opt=0] number margins.bottom +-- @tparam[opt=0] number margins.left +-- @tparam[opt=0] number margins.right +-- @property margins +-- @see clip + +--- The progressbar padding. +-- Note that if the `clip` is disabled, this allows the bar to be taller +-- than the background. +-- +-- See the `clip` example. +-- +-- @tparam[opt=0] (table|number|nil) padding A table for each side or a number +-- @tparam[opt=0] number padding.top +-- @tparam[opt=0] number padding.bottom +-- @tparam[opt=0] number padding.left +-- @tparam[opt=0] number padding.right +-- @property paddings +-- @see clip + +--- The progressbar margins. +-- Note that if the `clip` is disabled, this allows the background to be smaller +-- than the bar. +-- @tparam[opt=0] (table|number|nil) margins A table for each side or a number +-- @tparam[opt=0] number margins.top +-- @tparam[opt=0] number margins.bottom +-- @tparam[opt=0] number margins.left +-- @tparam[opt=0] number margins.right +-- @beautiful beautiful.progressbar_margins +-- @see clip + +--- The progressbar padding. +-- Note that if the `clip` is disabled, this allows the bar to be taller +-- than the background. +-- @tparam[opt=0] (table|number|nil) padding A table for each side or a number +-- @tparam[opt=0] number padding.top +-- @tparam[opt=0] number padding.bottom +-- @tparam[opt=0] number padding.left +-- @tparam[opt=0] number padding.right +-- @beautiful beautiful.progressbar_paddings +-- @see clip + +local properties = { "border_color", "color" , "background_color", + "value" , "max_value" , "ticks", + "ticks_gap" , "ticks_size", "border_width", + "shape" , "bar_shape" , "bar_border_width", + "clip" , "margins" , "bar_border_color", + "paddings", + } + +function progressbar.draw(pbar, _, cr, width, height) + local ticks_gap = pbar._private.ticks_gap or 1 + local ticks_size = pbar._private.ticks_size or 4 + + -- We want one pixel wide lines + cr:set_line_width(1) + + local max_value = pbar._private.max_value + + local value = math.min(max_value, math.max(0, pbar._private.value)) + + if value >= 0 then + value = value / max_value + end + local border_width = pbar._private.border_width + or beautiful.progressbar_border_width or 0 + + local bcol = pbar._private.border_color or beautiful.progressbar_border_color + + border_width = bcol and border_width or 0 + + local bg = pbar._private.background_color or + beautiful.progressbar_bg or "#ff0000aa" + + local bg_width, bg_height = width, height + + local clip = pbar._private.clip ~= false and beautiful.progressbar_clip ~= false + + -- Apply the margins + local margin = pbar._private.margins or beautiful.progressbar_margins + + if margin then + if type(margin) == "number" then + cr:translate(margin, margin) + bg_width, bg_height = bg_width - 2*margin, bg_height - 2*margin + else + cr:translate(margin.left or 0, margin.top or 0) + bg_height = bg_height - + (margin.top or 0) - (margin.bottom or 0) + bg_width = bg_width - + (margin.left or 0) - (margin.right or 0) + end + end + + -- Draw the background shape + if border_width > 0 then + -- Cairo draw half of the border outside of the path area + cr:translate(border_width/2, border_width/2) + bg_width, bg_height = bg_width - border_width, bg_height - border_width + cr:set_line_width(border_width) + end + + local background_shape = pbar._private.shape or + beautiful.progressbar_shape or shape.rectangle + + background_shape(cr, bg_width, bg_height) + + cr:set_source(color(bg)) + + local over_drawn_width = bg_width + border_width + local over_drawn_height = bg_height + border_width + + if border_width > 0 then + cr:fill_preserve() + + -- Draw the border + cr:set_source(color(bcol)) + + cr:stroke() + + over_drawn_width = over_drawn_width - 2*border_width + over_drawn_height = over_drawn_height - 2*border_width + else + cr:fill() + end + + -- Undo the translation + cr:translate(-border_width/2, -border_width/2) + + -- Make sure the bar stay in the shape + if clip then + background_shape(cr, bg_width, bg_height) + cr:clip() + cr:translate(border_width, border_width) + else + -- Assume the background size is irrelevant to the bar itself + if type(margin) == "number" then + cr:translate(-margin, -margin) + else + cr:translate(-(margin.left or 0), -(margin.top or 0)) + end + + over_drawn_height = height + over_drawn_width = width + end + + -- Apply the padding + local padding = pbar._private.paddings or beautiful.progressbar_paddings + + if padding then + if type(padding) == "number" then + cr:translate(padding, padding) + over_drawn_height = over_drawn_height - 2*padding + over_drawn_width = over_drawn_width - 2*padding + else + cr:translate(padding.left or 0, padding.top or 0) + + over_drawn_height = over_drawn_height - + (padding.top or 0) - (padding.bottom or 0) + over_drawn_width = over_drawn_width - + (padding.left or 0) - (padding.right or 0) + end + end + + over_drawn_width = math.max(over_drawn_width , 0) + over_drawn_height = math.max(over_drawn_height, 0) + + local rel_x = over_drawn_width * value + + + -- Draw the progressbar shape + + local bar_shape = pbar._private.bar_shape or + beautiful.progressbar_bar_shape or shape.rectangle + + local bar_border_width = pbar._private.bar_border_width or + beautiful.progressbar_bar_border_width or pbar._private.border_width or + beautiful.progressbar_border_width or 0 + + local bar_border_color = pbar._private.bar_border_color or + beautiful.progressbar_bar_border_color + + bar_border_width = bar_border_color and bar_border_width or 0 + + over_drawn_width = over_drawn_width - bar_border_width + over_drawn_height = over_drawn_height - bar_border_width + cr:translate(bar_border_width/2, bar_border_width/2) + + bar_shape(cr, rel_x, over_drawn_height) + + cr:set_source(color(pbar._private.color or beautiful.progressbar_fg or "#ff0000")) + + if bar_border_width > 0 then + cr:fill_preserve() + cr:set_source(color(bar_border_color)) + cr:set_line_width(bar_border_width) + cr:stroke() + else + cr:fill() + end + + if pbar._private.ticks then + for i=0, width / (ticks_size+ticks_gap)-border_width do + local rel_offset = over_drawn_width / 1 - (ticks_size+ticks_gap) * i + + if rel_offset <= rel_x then + cr:rectangle(rel_offset, + border_width, + ticks_gap, + over_drawn_height) + end + end + cr:set_source(color(pbar._private.background_color or "#000000aa")) + cr:fill() + end +end + +function progressbar:fit(_, width, height) + return width, height +end + +--- Set the progressbar value. +-- @param value The progress bar value between 0 and 1. +function progressbar:set_value(value) + value = value or 0 + + self._private.value = value + + self:emit_signal("widget::redraw_needed") + return self +end + +function progressbar:set_max_value(max_value) + + self._private.max_value = max_value + + self:emit_signal("widget::redraw_needed") +end + +--- Set the progressbar height. +-- This method is deprecated. Use a `wibox.container.constraint` widget or +-- `forced_height`. +-- @param height The height to set. +-- @deprecated set_height +function progressbar:set_height(height) + util.deprecate("Use a `wibox.container.constraint` widget or `forced_height`") + self:set_forced_height(height) +end + +--- Set the progressbar width. +-- This method is deprecated. Use a `wibox.container.constraint` widget or +-- `forced_width`. +-- @param width The width to set. +-- @deprecated set_width +function progressbar:set_width(width) + util.deprecate("Use a `wibox.container.constraint` widget or `forced_width`") + self:set_forced_width(width) +end + +-- Build properties function +for _, prop in ipairs(properties) do + if not progressbar["set_" .. prop] then + progressbar["set_" .. prop] = function(pbar, value) + pbar._private[prop] = value + pbar:emit_signal("widget::redraw_needed") + return pbar + end + end +end + +function progressbar:set_vertical(value) --luacheck: no unused_args + util.deprecate("Use a `wibox.container.rotate` widget") +end + + +--- Create a progressbar widget. +-- @param args Standard widget() arguments. You should add width and height +-- key to set progressbar geometry. +-- @return A progressbar widget. +-- @function wibox.widget.progressbar +function progressbar.new(args) + args = args or {} + + local pbar = base.make_widget(nil, nil, { + enable_properties = true, + }) + + pbar._private.width = args.width or 100 + pbar._private.height = args.height or 20 + pbar._private.value = 0 + pbar._private.max_value = 1 + + util.table.crush(pbar, progressbar, true) + + return pbar +end + +function progressbar.mt:__call(...) + return progressbar.new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(progressbar, progressbar.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/slider.lua b/lib/wibox/widget/slider.lua new file mode 100644 index 0000000..fd590c6 --- /dev/null +++ b/lib/wibox/widget/slider.lua @@ -0,0 +1,709 @@ +--------------------------------------------------------------------------- +-- An interactive mouse based slider widget. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_defaults_slider.svg) +-- +-- @usage +--wibox.widget { +-- bar_shape = gears.shape.rounded_rect, +-- bar_height = 3, +-- bar_color = beautiful.border_color, +-- handle_color = beautiful.bg_normal, +-- handle_shape = gears.shape.circle, +-- handle_border_color = beautiful.border_color, +-- handle_border_width = 1, +-- value = 25, +-- widget = wibox.widget.slider, +--} +-- +-- @author Grigory Mishchenko <grishkokot@gmail.com> +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2015 Grigory Mishchenko, 2016 Emmanuel Lepage Vallee +-- @classmod wibox.widget.slider +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local type = type +local color = require("gears.color") +local util = require("awful.util") +local beautiful = require("beautiful") +local base = require("wibox.widget.base") +local shape = require("gears.shape") +local capi = { + mouse = mouse, + mousegrabber = mousegrabber, + root = root, +} + +local slider = {mt={}} + +--- The slider handle shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_handle_shape.svg) +-- +-- +-- @property handle_shape +-- @tparam[opt=gears shape rectangle] gears.shape shape +-- @see gears.shape + +--- The slider handle color. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_handle_color.svg) +-- +-- +-- @property handle_color +-- @param color + +--- The slider handle margins. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_handle_margins.svg) +-- +-- +-- @property handle_margins +-- @tparam[opt={}] table margins +-- @tparam[opt=0] number margins.left +-- @tparam[opt=0] number margins.right +-- @tparam[opt=0] number margins.top +-- @tparam[opt=0] number margins.bottom + +--- The slider handle width. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_handle_width.svg) +-- +-- +-- @property handle_width +-- @param number + +--- The handle border_color. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_handle_border.svg) +-- +-- +-- @property handle_border_color +-- @param color + +--- The handle border width. +-- @property handle_border_width +-- @param[opt=0] number + +--- The bar (background) shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_bar_shape.svg) +-- +-- +-- @property bar_shape +-- @tparam[opt=gears shape rectangle] gears.shape shape +-- @see gears.shape + +--- The bar (background) height. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_bar_height.svg) +-- +-- +-- @property bar_height +-- @param number + +--- The bar (background) color. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_bar_color.svg) +-- +-- +-- @property bar_color +-- @param color + +--- The bar (background) margins. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_bar_margins.svg) +-- +-- +-- @property bar_margins +-- @tparam[opt={}] table margins +-- @tparam[opt=0] number margins.left +-- @tparam[opt=0] number margins.right +-- @tparam[opt=0] number margins.top +-- @tparam[opt=0] number margins.bottom + +--- The bar (background) border width. +-- @property bar_border_width +-- @param[opt=0] numbergb + +--- The bar (background) border_color. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_bar_border.svg) +-- +-- +-- @property bar_border_color +-- @param color + +--- The slider value. +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_slider_value.svg) +-- +-- +-- @property value +-- @param[opt=0] number + +--- The slider minimum value. +-- @property minimum +-- @param[opt=0] number + +--- The slider maximum value. +-- @property maximum +-- @param[opt=100] number + +--- The bar (background) border width. +-- @beautiful beautiful.slider_bar_border_width +-- @param number + +--- The bar (background) border color. +-- @beautiful beautiful.slider_bar_border_color +-- @param color + +--- The handle border_color. +-- @beautiful beautiful.slider_handle_border_color +-- @param color + +--- The handle border width. +-- @beautiful beautiful.slider_handle_border_width +-- @param number + +--- The handle . +-- @beautiful beautiful.slider_handle_width +-- @param number + +-- @beautiful beautiful.slider_handle_color +-- @param color + +--- The handle shape. +-- @beautiful beautiful.slider_handle_shape +-- @tparam[opt=gears shape rectangle] gears.shape shape +-- @see gears.shape + +--- The bar (background) shape. +-- @beautiful beautiful.slider_bar_shape +-- @tparam[opt=gears shape rectangle] gears.shape shape +-- @see gears.shape + +--- The bar (background) height. +-- @beautiful beautiful.slider_bar_height +-- @param number + +--- The bar (background) margins. +-- @beautiful beautiful.slider_bar_margins +-- @tparam[opt={}] table margins +-- @tparam[opt=0] number margins.left +-- @tparam[opt=0] number margins.right +-- @tparam[opt=0] number margins.top +-- @tparam[opt=0] number margins.bottom + +--- The slider handle margins. +-- @beautiful beautiful.slider_handle_margins +-- @tparam[opt={}] table margins +-- @tparam[opt=0] number margins.left +-- @tparam[opt=0] number margins.right +-- @tparam[opt=0] number margins.top +-- @tparam[opt=0] number margins.bottom + +--- The bar (background) color. +-- @beautiful beautiful.slider_bar_color +-- @param color + +local properties = { + -- Handle + handle_shape = shape.rectangle, + handle_color = false, + handle_margins = {}, + handle_width = false, + handle_border_width = 0, + handle_border_color = false, + + -- Bar + bar_shape = shape.rectangle, + bar_height = false, + bar_color = false, + bar_margins = {}, + bar_border_width = 0, + bar_border_color = false, + + -- Content + value = 0, + minimum = 0, + maximum = 100, +} + +-- Create the accessors +for prop in pairs(properties) do + slider["set_"..prop] = function(self, value) + local changed = self._private[prop] ~= value + self._private[prop] = value + + if changed then + self:emit_signal("property::"..prop) + self:emit_signal("widget::redraw_needed") + end + end + + slider["get_"..prop] = function(self) + -- Ignoring the false's is on purpose + return self._private[prop] == nil + and properties[prop] + or self._private[prop] + end +end + +-- Add some validation to set_value +function slider:set_value(value) + value = math.min(value, self:get_maximum()) + value = math.max(value, self:get_minimum()) + local changed = self._private.value ~= value + + self._private.value = value + + if changed then + self:emit_signal( "property::value" ) + self:emit_signal( "widget::redraw_needed" ) + end +end + +local function get_extremums(self) + local min = self._private.minimum or properties.minimum + local max = self._private.maximum or properties.maximum + local interval = max - min + + return min, max, interval +end + +function slider:draw(_, cr, width, height) + local bar_height = self._private.bar_height + + -- If there is no background, then skip this + local bar_color = self._private.bar_color + or beautiful.slider_bar_color + + if bar_color then + cr:set_source(color(bar_color)) + end + + local margins = self._private.bar_margins + or beautiful.slider_bar_margins + + local x_offset, right_margin, y_offset = 0, 0 + + if margins then + if type(margins) == "number" then + bar_height = bar_height or (height - 2*margins) + x_offset, y_offset = margins, margins + right_margin = margins + else + bar_height = bar_height or ( + height - (margins.top or 0) - (margins.bottom or 0) + ) + x_offset, y_offset = margins.left or 0, margins.top or 0 + right_margin = margins.right or 0 + end + else + bar_height = bar_height or beautiful.slider_bar_height or height + y_offset = (height - bar_height)/2 + end + + + cr:translate(x_offset, y_offset) + + local bar_shape = self._private.bar_shape + or beautiful.slider_bar_shape + or properties.bar_shape + + local bar_border_width = self._private.bar_border_width + or beautiful.slider_bar_border_width + or properties.bar_border_width + + bar_shape(cr, width - x_offset - right_margin, bar_height or height) + + if bar_color then + if bar_border_width == 0 then + cr:fill() + else + cr:fill_preserve() + end + end + + -- Draw the bar border + if bar_border_width > 0 then + local bar_border_color = self._private.bar_border_color + or beautiful.slider_bar_border_color + or properties.bar_border_color + + cr:set_line_width(bar_border_width) + + if bar_border_color then + cr:save() + cr:set_source(color(bar_border_color)) + cr:stroke() + cr:restore() + else + cr:stroke() + end + end + + cr:translate(-x_offset, -y_offset) + + -- Paint the handle + local handle_color = self._private.handle_color + or beautiful.slider_handle_color + + -- It is ok if there is no color, it will be inherited + if handle_color then + cr:set_source(color(handle_color)) + end + + local handle_height, handle_width = height, self._private.handle_width + or beautiful.slider_handle_width + or height/2 + + local handle_shape = self._private.handle_shape + or beautiful.slider_handle_shape + or properties.handle_shape + + -- Lets get the margins for the handle + margins = self._private.handle_margins + or beautiful.slider_handle_margins + + x_offset, y_offset = 0, 0 + + if margins then + if type(margins) == "number" then + x_offset, y_offset = margins, margins + handle_width = handle_width - 2*margins + handle_height = handle_height - 2*margins + else + x_offset, y_offset = margins.left or 0, margins.top or 0 + handle_width = handle_width - + (margins.left or 0) - (margins.right or 0) + handle_height = handle_height - + (margins.top or 0) - (margins.bottom or 0) + end + end + + local value = self._private.value or self._private.min or 0 + + -- Get the widget size back to it's non-transfored value + local min, _, interval = get_extremums(self) + local rel_value = ((value-min)/interval) * (width-handle_width) + + cr:translate(x_offset + rel_value, y_offset) + + local handle_border_width = self._private.handle_border_width + or beautiful.slider_handle_border_width + or properties.handle_border_width or 0 + + handle_shape(cr, handle_width, handle_height) + + if handle_border_width > 0 then + cr:fill_preserve() + else + cr:fill() + end + + -- Draw the handle border + if handle_border_width > 0 then + local handle_border_color = self._private.handle_border_color + or beautiful.slider_handle_border_color + or properties.handle_border_color + + if handle_border_color then + cr:set_source(color(handle_border_color)) + end + + cr:set_line_width(handle_border_width) + cr:stroke() + end +end + +function slider:fit(_, width, height) + -- Use all the space, this should be used with a constraint widget + return width, height +end + +-- Move the handle to the correct location +local function move_handle(self, width, x, _) + local _, _, interval = get_extremums(self) + self:set_value(math.floor((x*interval)/width)) +end + +local function mouse_press(self, x, y, button_id, _, geo) + if button_id ~= 1 then return end + + local matrix_from_device = geo.hierarchy:get_matrix_from_device() + + -- Sigh. geo.width/geo.height is in device space. We need it in our own + -- coordinate system + local width = geo.widget_width + + move_handle(self, width, x, y) + + -- Calculate a matrix transforming from screen coordinates into widget coordinates + local wgeo = geo.drawable.drawable:geometry() + local matrix = matrix_from_device:translate(-wgeo.x, -wgeo.y) + + capi.mousegrabber.run(function(mouse) + if not mouse.buttons[1] then + return false + end + + -- Calculate the point relative to the widget + move_handle(self, width, matrix:transform_point(mouse.x, mouse.y)) + + return true + end,"fleur") +end + +--- Create a slider widget. +-- @tparam[opt={}] table args +-- @function wibox.widget.slider +local function new(args) + local ret = base.make_widget(nil, nil, { + enable_properties = true, + }) + + util.table.crush(ret._private, args or {}) + + util.table.crush(ret, slider, true) + + ret:connect_signal("button::press", mouse_press) + + return ret +end + +function slider.mt:__call(_, ...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(slider, slider.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/systray.lua b/lib/wibox/widget/systray.lua new file mode 100644 index 0000000..e83347c --- /dev/null +++ b/lib/wibox/widget/systray.lua @@ -0,0 +1,186 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.widget.systray +--------------------------------------------------------------------------- + +local wbase = require("wibox.widget.base") +local beautiful = require("beautiful") +local util = require("awful.util") +local capi = { + awesome = awesome, + screen = screen +} +local setmetatable = setmetatable +local error = error +local abs = math.abs + +local systray = { mt = {} } + +local instance = nil +local horizontal = true +local base_size = nil +local reverse = false +local display_on_screen = "primary" + +--- The systray background color. +-- @beautiful beautiful.bg_systray +-- @param string The color (string like "#ff0000" only) + +--- The systray icon spacing. +-- @beautiful beautiful.systray_icon_spacing +-- @tparam[opt=0] integer The icon spacing + +local function should_display_on(s) + if display_on_screen == "primary" then + return s == capi.screen.primary + end + return s == display_on_screen +end + +function systray:draw(context, cr, width, height) + if not should_display_on(context.screen) then + return + end + + local x, y, _, _ = wbase.rect_to_device_geometry(cr, 0, 0, width, height) + local num_entries = capi.awesome.systray() + local bg = beautiful.bg_systray or beautiful.bg_normal or "#000000" + local spacing = beautiful.systray_icon_spacing or 0 + + if context and not context.wibox then + error("The systray widget can only be placed inside a wibox.") + end + + -- Figure out if the cairo context is rotated + local dir_x, dir_y = cr:user_to_device_distance(1, 0) + local is_rotated = abs(dir_x) < abs(dir_y) + + local in_dir, ortho, base + if horizontal then + in_dir, ortho = width, height + is_rotated = not is_rotated + else + ortho, in_dir = width, height + end + if ortho * num_entries <= in_dir then + base = ortho + else + base = in_dir / num_entries + end + capi.awesome.systray(context.wibox.drawin, math.ceil(x), math.ceil(y), + base, is_rotated, bg, reverse, spacing) +end + +function systray:fit(context, width, height) + if not should_display_on(context.screen) then + return 0, 0 + end + + local num_entries = capi.awesome.systray() + local base = base_size + local spacing = beautiful.systray_icon_spacing or 0 + if num_entries == 0 then + return 0, 0 + end + if base == nil then + if width < height then + base = width + else + base = height + end + end + base = base + spacing + if horizontal then + return base * num_entries - spacing, base + end + return base, base * num_entries - spacing +end + +-- Check if the function was called like :foo() or .foo() and do the right thing +local function get_args(self, ...) + if self == instance then + return ... + end + return self, ... +end + +--- Set the size of a single icon. +-- If this is set to nil, then the size is picked dynamically based on the +-- available space. Otherwise, any single icon has a size of `size`x`size`. +-- @tparam integer|nil size The base size +function systray:set_base_size(size) + base_size = get_args(self, size) + if instance then + instance:emit_signal("widget::layout_changed") + end +end + +--- Decide between horizontal or vertical display. +-- @tparam boolean horiz Use horizontal mode? +function systray:set_horizontal(horiz) + horizontal = get_args(self, horiz) + if instance then + instance:emit_signal("widget::layout_changed") + end +end + +--- Should the systray icons be displayed in reverse order? +-- @tparam boolean rev Display in reverse order +function systray:set_reverse(rev) + reverse = get_args(self, rev) + if instance then + instance:emit_signal("widget::redraw_needed") + end +end + +--- Set the screen that the systray should be displayed on. +-- This can either be a screen, in which case the systray will be displayed on +-- exactly that screen, or the string `"primary"`, in which case it will be +-- visible on the primary screen. The default value is "primary". +-- @tparam screen|"primary" s The screen to display on. +function systray:set_screen(s) + display_on_screen = get_args(self, s) + if instance then + instance:emit_signal("widget::layout_changed") + end +end + +--- Create the systray widget. +-- Note that this widget can only exist once. +-- @tparam boolean revers Show in the opposite direction +-- @treturn table The new `systray` widget +-- @function wibox.widget.systray + +local function new(revers) + local ret = wbase.make_widget() + + util.table.crush(ret, systray, true) + + if revers then + ret:set_reverse(true) + end + + capi.awesome.connect_signal("systray::update", function() + ret:emit_signal("widget::layout_changed") + ret:emit_signal("widget::redraw_needed") + end) + capi.screen.connect_signal("primary_changed", function() + if display_on_screen == "primary" then + ret:emit_signal("widget::layout_changed") + end + end) + + return ret +end + +function systray.mt:__call(...) + if not instance then + instance = new(...) + end + return instance +end + +return setmetatable(systray, systray.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/textbox.lua b/lib/wibox/widget/textbox.lua new file mode 100644 index 0000000..add35e9 --- /dev/null +++ b/lib/wibox/widget/textbox.lua @@ -0,0 +1,507 @@ +--------------------------------------------------------------------------- +-- +-- +-- +--![Usage example](../images/AUTOGEN_wibox_widget_defaults_textbox.svg) +-- +-- @usage +--wibox.widget{ +-- markup = 'This is a textbox!!!', +-- align = 'center', +-- valign = 'center', +-- widget = wibox.widget.textbox +--} +-- @author Uli Schlachter +-- @author dodo +-- @copyright 2010, 2011 Uli Schlachter, dodo +-- @classmod wibox.widget.textbox +--------------------------------------------------------------------------- + +local base = require("wibox.widget.base") +local gdebug = require("gears.debug") +local beautiful = require("beautiful") +local lgi = require("lgi") +local util = require("awful.util") +local Pango = lgi.Pango +local PangoCairo = lgi.PangoCairo +local setmetatable = setmetatable + +local textbox = { mt = {} } + +--- The textbox font. +-- @beautiful beautiful.font + +--- Set the DPI of a Pango layout +local function setup_dpi(box, dpi) + if box._private.dpi ~= dpi then + box._private.dpi = dpi + box._private.ctx:set_resolution(dpi) + box._private.layout:context_changed() + end +end + +--- Setup a pango layout for the given textbox and dpi +local function setup_layout(box, width, height, dpi) + box._private.layout.width = Pango.units_from_double(width) + box._private.layout.height = Pango.units_from_double(height) + setup_dpi(box, dpi) +end + +-- Draw the given textbox on the given cairo context in the given geometry +function textbox:draw(context, cr, width, height) + setup_layout(self, width, height, context.dpi) + cr:update_layout(self._private.layout) + local _, logical = self._private.layout:get_pixel_extents() + local offset = 0 + if self._private.valign == "center" then + offset = (height - logical.height) / 2 + elseif self._private.valign == "bottom" then + offset = height - logical.height + end + cr:move_to(0, offset) + cr:show_layout(self._private.layout) +end + +local function do_fit_return(self) + local _, logical = self._private.layout:get_pixel_extents() + if logical.width == 0 or logical.height == 0 then + return 0, 0 + end + return logical.width, logical.height +end + +-- Fit the given textbox +function textbox:fit(context, width, height) + setup_layout(self, width, height, context.dpi) + return do_fit_return(self) +end + +--- Get the preferred size of a textbox. +-- This returns the size that the textbox would use if infinite space were +-- available. +-- @tparam integer|screen s The screen on which the textbox will be displayed. +-- @treturn number The preferred width. +-- @treturn number The preferred height. +function textbox:get_preferred_size(s) + return self:get_preferred_size_at_dpi(beautiful.xresources.get_dpi(s)) +end + +--- Get the preferred height of a textbox at a given width. +-- This returns the height that the textbox would use when it is limited to the +-- given width. +-- @tparam number width The available width. +-- @tparam integer|screen s The screen on which the textbox will be displayed. +-- @treturn number The needed height. +function textbox:get_height_for_width(width, s) + return self:get_height_for_width_at_dpi(width, beautiful.xresources.get_dpi(s)) +end + +--- Get the preferred size of a textbox. +-- This returns the size that the textbox would use if infinite space were +-- available. +-- @tparam number dpi The DPI value to render at. +-- @treturn number The preferred width. +-- @treturn number The preferred height. +function textbox:get_preferred_size_at_dpi(dpi) + local max_lines = 2^20 + setup_dpi(self, dpi) + self._private.layout.width = -1 -- no width set + self._private.layout.height = -max_lines -- show this many lines per paragraph + return do_fit_return(self) +end + +--- Get the preferred height of a textbox at a given width. +-- This returns the height that the textbox would use when it is limited to the +-- given width. +-- @tparam number width The available width. +-- @tparam number dpi The DPI value to render at. +-- @treturn number The needed height. +function textbox:get_height_for_width_at_dpi(width, dpi) + local max_lines = 2^20 + setup_dpi(self, dpi) + self._private.layout.width = Pango.units_from_double(width) + self._private.layout.height = -max_lines -- show this many lines per paragraph + local _, h = do_fit_return(self) + return h +end + +--- Set the text of the textbox (with +-- [Pango markup](https://developer.gnome.org/pango/stable/PangoMarkupFormat.html)). +-- @tparam string text The text to set. This can contain pango markup (e.g. +-- `bold`). You can use `awful.util.escape` to escape +-- parts of it. +-- @treturn[1] boolean true +-- @treturn[2] boolean false +-- @treturn[2] string Error message explaining why the markup was invalid. +function textbox:set_markup_silently(text) + if self._private.markup == text then + return true + end + + local attr, parsed = Pango.parse_markup(text, -1, 0) + -- In case of error, attr is false and parsed is a GLib.Error instance. + if not attr then + return false, parsed.message or tostring(parsed) + end + + self._private.markup = text + self._private.layout.text = parsed + self._private.layout.attributes = attr + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + return true +end + +--- Set the text of the textbox (with +-- [Pango markup](https://developer.gnome.org/pango/stable/PangoMarkupFormat.html)). +-- @property markup +-- @tparam string text The text to set. This can contain pango markup (e.g. +-- `bold`). You can use `awful.util.escape` to escape +-- parts of it. +-- @see text + +function textbox:set_markup(text) + local success, message = self:set_markup_silently(text) + if not success then + gdebug.print_error(message) + end +end + +function textbox:get_markup() + return self._private.markup +end + +--- Set a textbox' text. +-- @property text +-- @param text The text to display. Pango markup is ignored and shown as-is. +-- @see markup + +function textbox:set_text(text) + if self._private.layout.text == text and self._private.layout.attributes == nil then + return + end + self._private.markup = nil + self._private.layout.text = text + self._private.layout.attributes = nil + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") +end + +function textbox:get_text() + return self._private.layout.text +end + +--- Set a textbox' ellipsize mode. +-- @property ellipsize +-- @param mode Where should long lines be shortened? "start", "middle" or "end" + +function textbox:set_ellipsize(mode) + local allowed = { none = "NONE", start = "START", middle = "MIDDLE", ["end"] = "END" } + if allowed[mode] then + if self._private.layout:get_ellipsize() == allowed[mode] then + return + end + self._private.layout:set_ellipsize(allowed[mode]) + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + end +end + +--- Set a textbox' wrap mode. +-- @property wrap +-- @param mode Where to wrap? After "word", "char" or "word_char" + +function textbox:set_wrap(mode) + local allowed = { word = "WORD", char = "CHAR", word_char = "WORD_CHAR" } + if allowed[mode] then + if self._private.layout:get_wrap() == allowed[mode] then + return + end + self._private.layout:set_wrap(allowed[mode]) + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + end +end + +--- The textbox' vertical alignment +-- @property valign +-- @param mode Where should the textbox be drawn? "top", "center" or "bottom" + +function textbox:set_valign(mode) + local allowed = { top = true, center = true, bottom = true } + if allowed[mode] then + if self._private.valign == mode then + return + end + self._private.valign = mode + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + end +end + +--- Set a textbox' horizontal alignment. +-- @property align +-- @param mode Where should the textbox be drawn? "left", "center" or "right" + +function textbox:set_align(mode) + local allowed = { left = "LEFT", center = "CENTER", right = "RIGHT" } + if allowed[mode] then + if self._private.layout:get_alignment() == allowed[mode] then + return + end + self._private.layout:set_alignment(allowed[mode]) + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + end +end + +--- Set a textbox' font +-- @property font +-- @param font The font description as string + +function textbox:set_font(font) + self._private.layout:set_font_description(beautiful.get_font(font)) + self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") +end + +--- Create a new textbox. +-- @tparam[opt=""] string text The textbox content +-- @tparam[opt=false] boolean ignore_markup Ignore the pango/HTML markup +-- @treturn table A new textbox widget +-- @function wibox.widget.textbox +local function new(text, ignore_markup) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, textbox, true) + + ret._private.dpi = -1 + ret._private.ctx = PangoCairo.font_map_get_default():create_context() + ret._private.layout = Pango.Layout.new(ret._private.ctx) + + ret:set_ellipsize("end") + ret:set_wrap("word_char") + ret:set_valign("center") + ret:set_align("left") + ret:set_font(beautiful and beautiful.font) + + if text then + if ignore_markup then + ret:set_text(text) + else + ret:set_markup(text) + end + end + + return ret +end + +function textbox.mt.__call(_, ...) + return new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(textbox, textbox.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/textclock.lua b/lib/wibox/widget/textclock.lua new file mode 100644 index 0000000..c688028 --- /dev/null +++ b/lib/wibox/widget/textclock.lua @@ -0,0 +1,254 @@ +--------------------------------------------------------------------------- +--- Text clock widget. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @classmod wibox.widget.textclock +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local os = os +local textbox = require("wibox.widget.textbox") +local timer = require("gears.timer") +local DateTime = require("lgi").GLib.DateTime + +local textclock = { mt = {} } + +--- This lowers the timeout so that it occurs "correctly". For example, a timeout +-- of 60 is rounded so that it occurs the next time the clock reads ":00 seconds". +local function calc_timeout(real_timeout) + return real_timeout - os.time() % real_timeout +end + +--- Create a textclock widget. It draws the time it is in a textbox. +-- +-- @tparam[opt=" %a %b %d, %H:%M "] string format The time format. +-- @tparam[opt=60] number timeout How often update the time (in seconds). +-- @treturn table A textbox widget. +-- @function wibox.widget.textclock +function textclock.new(format, timeout) + format = format or " %a %b %d, %H:%M " + timeout = timeout or 60 + + local w = textbox() + local t + function w._private.textclock_update_cb() + w:set_markup(DateTime.new_now_local():format(format)) + t.timeout = calc_timeout(timeout) + t:again() + return true -- Continue the timer + end + t = timer.weak_start_new(timeout, w._private.textclock_update_cb) + t:emit_signal("timeout") + return w +end + +function textclock.mt:__call(...) + return textclock.new(...) +end + +--Imported documentation + + +--- Get a widex index. +-- @param widget The widget to look for +-- @param[opt] recursive Also check sub-widgets +-- @param[opt] ... Aditional widgets to add at the end of the \"path\" +-- @return The index +-- @return The parent layout +-- @return The path between \"self\" and \"widget\" +-- @function index + +--- Get all direct and indirect children widgets. +-- This will scan all containers recursively to find widgets +-- Warning: This method it prone to stack overflow id the widget, or any of its +-- children, contain (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children + +--- Set a declarative widget hierarchy description. +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html) +-- @param args An array containing the widgets disposition +-- @function setup + +--- Force a widget height. +-- @property forced_height +-- @tparam number|nil height The height (`nil` for automatic) + +--- Force a widget width. +-- @property forced_width +-- @tparam number|nil width The width (`nil` for automatic) + +--- The widget opacity (transparency). +-- @property opacity +-- @tparam[opt=1] number opacity The opacity (between 0 and 1) + +--- The widget visibility. +-- @property visible +-- @param boolean + +--- Set/get a widget's buttons. +-- @param _buttons The table of buttons that should bind to the widget. +-- @function buttons + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive + +--- When the layout (size) change. +-- This signal is emitted when the previous results of `:layout()` and `:fit()` +-- are no longer valid. Unless this signal is emitted, `:layout()` and `:fit()` +-- must return the same result when called with the same arguments. +-- @signal widget::layout_changed +-- @see widget::redraw_needed + +--- When the widget content changed. +-- This signal is emitted when the content of the widget changes. The widget will +-- be redrawn, it is not re-layouted. Put differently, it is assumed that +-- `:layout()` and `:fit()` would still return the same results as before. +-- @signal widget::redraw_needed +-- @see widget::layout_changed + +--- When a mouse button is pressed over the widget. +-- @signal button::press +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When a mouse button is released over the widget. +-- @signal button::release +-- @tparam number lx The horizontal position relative to the (0,0) position in +-- the widget. +-- @tparam number ly The vertical position relative to the (0,0) position in the +-- widget. +-- @tparam number button The button number. +-- @tparam table mods The modifiers (mod4, mod1 (alt), Control, Shift) +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse enter a widget. +-- @signal mouse::enter +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + +--- When the mouse leave a widget. +-- @signal mouse::leave +-- @tparam table find_widgets_result The entry from the result of +-- @{wibox.drawable:find_widgets} for the position that the mouse hit. +-- @tparam wibox.drawable find_widgets_result.drawable The drawable containing +-- the widget. +-- @tparam widget find_widgets_result.widget The widget being displayed. +-- @tparam wibox.hierarchy find_widgets_result.hierarchy The hierarchy +-- managing the widget's geometry. +-- @tparam number find_widgets_result.x An approximation of the X position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.y An approximation of the Y position that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.width An approximation of the width that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.height An approximation of the height that +-- the widget is visible at on the surface. +-- @tparam number find_widgets_result.widget_width The exact width of the widget +-- in its local coordinate system. +-- @tparam number find_widgets_result.widget_height The exact height of the widget +-- in its local coordinate system. +-- @see mouse + + +--Imported documentation + + +--- Disconnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +-- @function disconnect_signal + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +-- @function emit_signal + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function connect_signal + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- +-- **Warning:** +-- Only use this function if you really, really, really know what you +-- are doing. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +-- @function weak_connect_signal + + +return setmetatable(textclock, textclock.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 -- cgit v1.2.3