diff options
author | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
---|---|---|
committer | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
commit | 22d656903563f75678f3634964731ccf93355dfd (patch) | |
tree | e3cb6279d95c9764093072d5e946566ea6533799 /lib/awful |
Init commit
Diffstat (limited to 'lib/awful')
56 files changed, 16284 insertions, 0 deletions
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 '<span font="' .. tostring(font) .. '">' .. tostring(text) ..'</span>' +end +-- Set the foreground. +function markup.fg(color, text) + return '<span foreground="' .. tostring(color) .. '">' .. tostring(text) .. '</span>' +end +-- Set the background. +function markup.bg(color, text) + return '<span background="' .. tostring(color) .. '">' .. tostring(text) .. '</span>' +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<available_height_items) and new_keys or overlap_leftovers), keys[i]) + end + keys = new_keys + table.insert(keys, {key=markup.fg(widget.modifiers_color, "▽"), description=""}) + end + if not current_column then + current_column = {layout=wibox.layout.fixed.vertical()} + end + current_column.layout:add(group_label(group)) + + local function insert_keys(_keys, _add_new_column) + local max_label_width = 0 + local max_label_content = "" + local joined_labels = "" + for i, key in ipairs(_keys) do + local length = string.len(key.key or '') + string.len(key.description or '') + local modifiers = key.mod + if not modifiers or modifiers == "none" then + modifiers = "" + else + length = length + string.len(modifiers) + 1 -- +1 for "+" character + modifiers = markup.fg(widget.modifiers_color, modifiers.."+") + end + local rendered_hotkey = markup.font(widget.title_font, + modifiers .. (key.key or "") .. " " + ) .. markup.font(widget.description_font, + key.description or "" + ) + if length > 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 "<u>" .. l .. "</u>" + end, 1)) + -- Set icon if needed + local icon, iconbox + local margin = wibox.container.margin() + margin:set_widget(label) + if args.icon then + icon = surface.load(args.icon) + end + if icon then + local iw = icon:get_width() + local ih = icon:get_height() + if iw > args.theme.width or ih > args.theme.height then + local w, h + if ((args.theme.height / ih) * iw) > args.theme.width then + w, h = args.theme.height, (args.theme.height / iw) * ih + else + w, h = (args.theme.height / ih) * iw, args.theme.height + end + -- We need to scale the image to size w x h + local img = cairo.ImageSurface(cairo.Format.ARGB32, w, h) + local cr = cairo.Context(img) + cr:scale(w / iw, h / ih) + cr:set_source_surface(icon, 0, 0) + cr:paint() + icon = img + end + iconbox = wibox.widget.imagebox() + if iconbox:set_image(icon) then + margin:set_left(dpi(2)) + else + iconbox = nil + end + end + if not iconbox then + margin:set_left(args.theme.height + dpi(2)) + end + -- Create the submenu icon widget + local submenu + if type(args.cmd) == "table" then + if args.theme.submenu_icon then + submenu = wibox.widget.imagebox() + submenu:set_image(args.theme.submenu_icon) + else + submenu = wibox.widget.textbox() + submenu:set_font(args.theme.font) + submenu:set_text(args.theme.submenu) + end + end + -- Add widgets to the wibox + local left = wibox.layout.fixed.horizontal() + if iconbox then + left:add(iconbox) + end + -- This contains the label + left:add(margin) + + local layout = wibox.layout.align.horizontal() + layout:set_left(left) + if submenu then + layout:set_right(submenu) + end + + return table_update(ret, { + label = label, + sep = submenu, + icon = iconbox, + widget = layout, + cmd = args.cmd, + akey = key, + }) +end + +-------------------------------------------------------------------------------- + +--- Create a menu popup. +-- @param args Table containing the menu informations. +-- +-- * Key items: Table containing the displayed items. Each element is a table by default (when element 'new' is awful.menu.entry) containing: item name, triggered action, submenu table or function, item icon (optional). +-- * Keys theme.[fg|bg]_[focus|normal], theme.border_color, theme.border_width, theme.submenu_icon, theme.height and theme.width override the default display for your menu and/or of your menu entry, each of them are optional. +-- * Key auto_expand controls the submenu auto expand behaviour by setting it to true (default) or false. +-- +-- @param parent Specify the parent menu if we want to open a submenu, this value should never be set by the user. +-- @usage -- The following function builds and shows a menu of clients that match +-- -- a particular rule. +-- -- Bound to a key, it can be used to select from dozens of terminals open on +-- -- several tags. +-- -- When using @{rules.match_any} instead of @{rules.match}, +-- -- a menu of clients with different classes could be build. +-- +-- function terminal_menu () +-- terms = {} +-- for i, c in pairs(client.get()) do +-- if awful.rules.match(c, {class = "URxvt"}) then +-- terms[i] = +-- {c.name, +-- function() +-- c.first_tag:view_only() +-- client.focus = c +-- end, +-- c.icon +-- } +-- end +-- end +-- awful.menu(terms):show() +-- end +function menu.new(args, parent) + args = args or {} + args.layout = args.layout or wibox.layout.flex.vertical + local _menu = table_update(object(), { + item_enter = menu.item_enter, + item_leave = menu.item_leave, + get_root = menu.get_root, + delete = menu.delete, + update = menu.update, + toggle = menu.toggle, + hide = menu.hide, + show = menu.show, + exec = menu.exec, + add = menu.add, + child = {}, + items = {}, + parent = parent, + layout = args.layout(), + theme = load_theme(args.theme or {}, parent and parent.theme) }) + + if parent then + _menu.auto_expand = parent.auto_expand + elseif args.auto_expand ~= nil then + _menu.auto_expand = args.auto_expand + else + _menu.auto_expand = true + end + + -- Create items + for _, v in ipairs(args) do _menu:add(v) end + if args.items then + for _, v in pairs(args.items) do _menu:add(v) end + end + + _menu._keygrabber = function (...) + grabber(_menu, ...) + end + + _menu.wibox = wibox({ + ontop = true, + fg = _menu.theme.fg_normal, + bg = _menu.theme.bg_normal, + border_color = _menu.theme.border, + border_width = _menu.theme.border_width, + type = "popup_menu" }) + _menu.wibox.visible = false + _menu.wibox:set_widget(_menu.layout) + set_size(_menu) + + _menu.x = _menu.wibox.x + _menu.y = _menu.wibox.y + return _menu +end + +function menu.mt:__call(...) + return menu.new(...) +end + +return setmetatable(menu, menu.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 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 +-- +-- +-- <h3>Compositing</h3> +-- +-- 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}) +-- +-- <h3>Common arguments</h3> +-- +-- **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 .. "<span background=\"" .. cursor_color .. + "\" foreground=\"" .. text_color .. "\" underline=\"" .. underline .. + "\">" .. char .. "</span>" .. text_end .. spacer + return ret +end + +--- Run a prompt in a box. +-- +-- The following readline keyboard shortcuts are implemented as expected: +-- <kbd>CTRL+A</kbd>, <kbd>CTRL+B</kbd>, <kbd>CTRL+C</kbd>, <kbd>CTRL+D</kbd>, +-- <kbd>CTRL+E</kbd>, <kbd>CTRL+J</kbd>, <kbd>CTRL+M</kbd>, <kbd>CTRL+F</kbd>, +-- <kbd>CTRL+H</kbd>, <kbd>CTRL+K</kbd>, <kbd>CTRL+U</kbd>, <kbd>CTRL+W</kbd>, +-- <kbd>CTRL+BACKSPACE</kbd>, <kbd>SHIFT+INSERT</kbd>, <kbd>HOME</kbd>, +-- <kbd>END</kbd> 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. +-- +-- * <kbd>CTRL+R</kbd>: reverse history search, matches any history entry +-- containing search term. +-- * <kbd>CTRL+S</kbd>: forward history search, matches any history entry +-- containing search term. +-- * <kbd>CTRL+UP</kbd>: ZSH up line or search, matches any history entry +-- starting with search term. +-- * <kbd>CTRL+DOWN</kbd>: ZSH down line or search, matches any history +-- entry starting with search term. +-- * <kbd>CTRL+DELETE</kbd>: 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 "<unknown>") + 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("<i><Invalid text></i>") + 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 = "<span font_desc='"..font.."'>" + if fg_color then + text = text .. "<span color='" .. util.ensure_pango_color(fg_color) .. + "'>" .. (util.escape(t.name) or "") .. "</span>" + else + text = text .. (util.escape(t.name) or "") + end + text = text .. "</span>" + 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. +-- +-- <a name="status_icons"></a> +-- **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. +-- +-- <table class='widget_list' border=1> +-- <tr style='font-weight: bold;'> +-- <th align='center'>Icon</th> +-- <th align='center'>Client property</th> +-- </tr> +-- <tr><td>▪</td><td><a href="./client.html#client.sticky">sticky</a></td></tr> +-- <tr><td>⌃</td><td><a href="./client.html#client.ontop">ontop</a></td></tr> +-- <tr><td>▴</td><td><a href="./client.html#client.above">above</a></td></tr> +-- <tr><td>▾</td><td><a href="./client.html#client.below">below</a></td></tr> +-- <tr><td>✈</td><td><a href="./client.html#client.floating">floating</a></td></tr> +-- <tr><td>+</td><td><a href="./client.html#client.maximized">maximized</a></td></tr> +-- <tr><td>⬌</td><td><a href="./client.html#client.maximized_horizontal">maximized_horizontal</a></td></tr> +-- <tr><td>⬍</td><td><a href="./client.html#client.maximized_vertical">maximized_vertical</a></td></tr> +-- </table> +-- +-- @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 <a href="status_icons">Status icons</a> 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 '<b>+</b>' + 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("<untitled>")) + else + name = name .. (util.escape(c.name) or util.escape("<untitled>")) + 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 .. "<span color='"..fg_focus.."'>"..name.."</span>" + 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 .. "<span color='"..fg_urgent.."'>"..name.."</span>" + 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 .. "<span color='"..fg_minimize.."'>"..name.."</span>" + 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 .. "<span color='"..fg_normal.."'>"..name.."</span>" + 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 |