diff options
Diffstat (limited to 'lib/wibox/widget/base.lua')
-rw-r--r-- | lib/wibox/widget/base.lua | 694 |
1 files changed, 694 insertions, 0 deletions
diff --git a/lib/wibox/widget/base.lua b/lib/wibox/widget/base.lua new file mode 100644 index 0000000..dd80ec7 --- /dev/null +++ b/lib/wibox/widget/base.lua @@ -0,0 +1,694 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod wibox.widget.base +--------------------------------------------------------------------------- + +local object = require("gears.object") +local cache = require("gears.cache") +local matrix = require("gears.matrix") +local protected_call = require("gears.protected_call") +local util = require("awful.util") +local setmetatable = setmetatable +local pairs = pairs +local type = type +local table = table + +local base = {} + +-- {{{ Functions on widgets + +--- Functions available on all widgets. +base.widget = {} + +--- Set/get a widget's buttons. +-- @tab _buttons The table of buttons that is bound to the widget. +-- @function buttons +function base.widget:buttons(_buttons) + if _buttons then + self._private.widget_buttons = _buttons + end + return self._private.widget_buttons +end + +--- Set a widget's visibility. +-- @tparam boolean b Whether the widget is visible. +-- @function set_visible +function base.widget:set_visible(b) + if b ~= self._private.visible then + self._private.visible = b + self:emit_signal("widget::layout_changed") + -- In case something ignored fit and drew the widget anyway. + self:emit_signal("widget::redraw_needed") + end +end + +--- Is the widget visible? +-- @treturn boolean +-- @function get_visible +function base.widget:get_visible() + return self._private.visible or false +end + +--- Set a widget's opacity. +-- @tparam number o The opacity to use (a number from 0 (transparent) to 1 +-- (opaque)). +-- @function set_opacity +function base.widget:set_opacity(o) + if o ~= self._private.opacity then + self._private.opacity = o + self:emit_signal("widget::redraw") + end +end + +--- Get the widget's opacity. +-- @treturn number The opacity (between 0 (transparent) and 1 (opaque)). +-- @function get_opacity +function base.widget:get_opacity() + return self._private.opacity +end + +--- Set the widget's forced width. +-- @tparam[opt] number width With `nil` the default mechanism of calling the +-- `:fit` method is used. +-- @see fit_widget +-- @function set_forced_width +function base.widget:set_forced_width(width) + if width ~= self._private.forced_width then + self._private.forced_width = width + self:emit_signal("widget::layout_changed") + end +end + +--- Get the widget's forced width. +-- +-- Note that widget instances can be used in different places simultaneously, +-- and therefore can have multiple dimensions. +-- If there is no forced width/height, then the only way to get the widget's +-- actual size is during a `mouse::enter`, `mouse::leave` or button event. +-- @treturn[opt] number The forced width (nil if automatic). +-- @see fit_widget +-- @function get_forced_width +function base.widget:get_forced_width() + return self._private.forced_width +end + +--- Set the widget's forced height. +-- @tparam[opt] number height With `nil` the default mechanism of calling the +-- `:fit` method is used. +-- @see fit_widget +-- @function set_height +function base.widget:set_forced_height(height) + if height ~= self._private.forced_height then + self._private.forced_height = height + self:emit_signal("widget::layout_changed") + end +end + +--- Get the widget's forced height. +-- +-- Note that widget instances can be used in different places simultaneously, +-- and therefore can have multiple dimensions. +-- If there is no forced width/height, then the only way to get the widget's +-- actual size is during a `mouse::enter`, `mouse::leave` or button event. +-- @treturn[opt] number The forced height (nil if automatic). +-- @function get_forced_height +function base.widget:get_forced_height() + return self._private.forced_height +end + +--- Get the widget's direct children widgets. +-- +-- This method should be re-implemented by the relevant widgets. +-- @treturn table The children +-- @function get_children +function base.widget:get_children() + return {} +end + +--- Replace the layout children. +-- +-- The default implementation does nothing, this must be re-implemented by +-- all layout and container widgets. +-- @tab children A table composed of valid widgets. +-- @function set_children +function base.widget:set_children(children) -- luacheck: no unused + -- Nothing on purpose +end + +-- It could have been merged into `get_all_children`, but it's not necessary. +local function digg_children(ret, tlw) + for _, w in ipairs(tlw:get_children()) do + table.insert(ret, w) + digg_children(ret, w) + end +end + +--- Get all direct and indirect children widgets. +-- +-- This will scan all containers recursively to find widgets. +-- +-- *Warning*: This method it prone to stack overflow if the widget, or any of +-- its children, contains (directly or indirectly) itself. +-- @treturn table The children +-- @function get_all_children +function base.widget:get_all_children() + local ret = {} + digg_children(ret, self) + return ret +end + +--- Emit a signal and ensure all parent widgets in the hierarchies also +-- forward the signal. This is useful to track signals when there is a dynamic +-- set of containers and layouts wrapping the widget. +-- +-- Note that this function has two flaws: +-- +-- 1. The signal is only forwarded once the widget tree has been built. This +-- happens after all currently scheduled functions have been executed. +-- Therefore, it will not start to work right away. +-- 2. In case the widget is present multiple times in a single widget tree, +-- this function will also forward the signal multiple time (one per upward +-- tree path). +-- +-- @tparam string signal_name +-- @param ... Other arguments +-- @function emit_signal_recursive +function base.widget:emit_signal_recursive(signal_name, ...) + -- This is a convenience wrapper, the real implementation is in the + -- hierarchy. + + self:emit_signal("widget::emit_recursive", signal_name, ...) +end + +--- Get the index of a widget. +-- @tparam widget widget The widget to look for. +-- @tparam[opt] boolean recursive Also check sub-widgets? +-- @tparam[opt] widget ... Additional widgets to add at the end of the "path" +-- @treturn number The index. +-- @treturn widget The parent widget. +-- @treturn table The path between "self" and "widget". +-- @function index +function base.widget:index(widget, recursive, ...) + local widgets = self:get_children() + for idx, w in ipairs(widgets) do + if w == widget then + return idx, self, {...} + elseif recursive then + local child_idx, l, path = w:index(widget, true, self, ...) + if child_idx and l then + return child_idx, l, path + end + end + end + return nil, self, {} +end +-- }}} + +-- {{{ Caches + +-- Indexes are widgets, allow them to be garbage-collected. +local widget_dependencies = setmetatable({}, { __mode = "kv" }) + +-- Get the cache of the given kind for this widget. This returns a gears.cache +-- that calls the callback of kind `kind` on the widget. +local function get_cache(widget, kind) + if not widget._private.widget_caches[kind] then + widget._private.widget_caches[kind] = cache.new(function(...) + return protected_call(widget[kind], widget, ...) + end) + end + return widget._private.widget_caches[kind] +end + +-- Special value to skip the dependency recording that is normally done by +-- base.fit_widget() and base.layout_widget(). The caller must ensure that no +-- caches depend on the result of the call and/or must handle the children's +-- widget::layout_changed signal correctly when using this. +base.no_parent_I_know_what_I_am_doing = {} + +-- Record a dependency from parent to child: The layout of `parent` depends on +-- the layout of `child`. +local function record_dependency(parent, child) + if parent == base.no_parent_I_know_what_I_am_doing then + return + end + + base.check_widget(parent) + base.check_widget(child) + + local deps = widget_dependencies[child] or {} + deps[parent] = true + widget_dependencies[child] = deps +end + +-- Clear the caches for `widget` and all widgets that depend on it. +local clear_caches +function clear_caches(widget) + local deps = widget_dependencies[widget] or {} + widget_dependencies[widget] = {} + widget._private.widget_caches = {} + for w in pairs(deps) do + clear_caches(w) + end +end + +-- }}} + +--- Figure out the geometry in the device coordinate space. +-- +-- This gives only tight bounds if no rotations by non-multiples of 90° are +-- used. +-- @function wibox.widget.base.rect_to_device_geometry +function base.rect_to_device_geometry(cr, x, y, width, height) + return matrix.transform_rectangle(cr.matrix, x, y, width, height) +end + +--- Fit a widget for the given available width and height. +-- +-- This calls the widget's `:fit` callback and caches the result for later use. +-- Never call `:fit` directly, but always through this function! +-- @tparam widget parent The parent widget which requests this information. +-- @tab context The context in which we are fit. +-- @tparam widget widget The widget to fit (this uses +-- `widget:fit(context, width, height)`). +-- @tparam number width The available width for the widget. +-- @tparam number height The available height for the widget. +-- @treturn number The width that the widget wants to use. +-- @treturn number The height that the widget wants to use. +-- @function wibox.widget.base.fit_widget +function base.fit_widget(parent, context, widget, width, height) + record_dependency(parent, widget) + + if not widget._private.visible then + return 0, 0 + end + + -- Sanitize the input. This also filters out e.g. NaN. + width = math.max(0, width) + height = math.max(0, height) + + local w, h = 0, 0 + if widget.fit then + w, h = get_cache(widget, "fit"):get(context, width, height) + else + -- If it has no fit method, calculate based on the size of children + local children = base.layout_widget(parent, context, widget, width, height) + for _, info in ipairs(children or {}) do + local x, y, w2, h2 = matrix.transform_rectangle(info._matrix, + 0, 0, info._width, info._height) + w, h = math.max(w, x + w2), math.max(h, y + h2) + end + end + + -- Apply forced size and handle nil's + w = widget._private.forced_width or w or 0 + h = widget._private.forced_height or h or 0 + + -- Also sanitize the output. + w = math.max(0, math.min(w, width)) + h = math.max(0, math.min(h, height)) + return w, h +end + +--- Lay out a widget for the given available width and height. +-- +-- This calls the widget's `:layout` callback and caches the result for later +-- use. Never call `:layout` directly, but always through this function! +-- However, normally there shouldn't be any reason why you need to use this +-- function. +-- @tparam widget parent The parent widget which requests this information. +-- @tab context The context in which we are laid out. +-- @tparam widget widget The widget to layout (this uses +-- `widget:layout(context, width, height)`). +-- @tparam number width The available width for the widget. +-- @tparam number height The available height for the widget. +-- @treturn[opt] table The result from the widget's `:layout` callback. +-- @function wibox.widget.base.layout_widget +function base.layout_widget(parent, context, widget, width, height) + record_dependency(parent, widget) + + if not widget._private.visible then + return + end + + -- Sanitize the input. This also filters out e.g. NaN. + width = math.max(0, width) + height = math.max(0, height) + + if widget.layout then + return get_cache(widget, "layout"):get(context, width, height) + end +end + +--- Handle a button event on a widget. +-- +-- This is used internally and should not be called directly. +-- @function wibox.widget.base.handle_button +function base.handle_button(event, widget, x, y, button, modifiers, geometry) + x = x or y -- luacheck: no unused + local function is_any(mod) + return #mod == 1 and mod[1] == "Any" + end + + local function tables_equal(a, b) + if #a ~= #b then + return false + end + for k, v in pairs(b) do + if a[k] ~= v then + return false + end + end + return true + end + + -- Find all matching button objects. + local matches = {} + for _, v in pairs(widget._private.widget_buttons) do + local match = true + -- Is it the right button? + if v.button ~= 0 and v.button ~= button then match = false end + -- Are the correct modifiers pressed? + if (not is_any(v.modifiers)) and (not tables_equal(v.modifiers, modifiers)) then match = false end + if match then + table.insert(matches, v) + end + end + + -- Emit the signals. + for _, v in pairs(matches) do + v:emit_signal(event,geometry) + end +end + +--- Create widget placement information. This should be used in a widget's +-- `:layout()` callback. +-- @tparam widget widget The widget that should be placed. +-- @param mat A matrix transforming from the parent widget's coordinate +-- system. For example, use matrix.create_translate(1, 2) to draw a +-- widget at position (1, 2) relative to the parent widget. +-- @tparam number width The width of the widget in its own coordinate system. +-- That is, after applying the transformation matrix. +-- @tparam number height The height of the widget in its own coordinate system. +-- That is, after applying the transformation matrix. +-- @treturn table An opaque object that can be returned from `:layout()`. +-- @function wibox.widget.base.place_widget_via_matrix +function base.place_widget_via_matrix(widget, mat, width, height) + return { + _widget = widget, + _width = width, + _height = height, + _matrix = mat + } +end + +--- Create widget placement information. This should be used for a widget's +-- `:layout()` callback. +-- @tparam widget widget The widget that should be placed. +-- @tparam number x The x coordinate for the widget. +-- @tparam number y The y coordinate for the widget. +-- @tparam number width The width of the widget in its own coordinate system. +-- That is, after applying the transformation matrix. +-- @tparam number height The height of the widget in its own coordinate system. +-- That is, after applying the transformation matrix. +-- @treturn table An opaque object that can be returned from `:layout()`. +-- @function wibox.widget.base.place_widget_at +function base.place_widget_at(widget, x, y, width, height) + return base.place_widget_via_matrix(widget, matrix.create_translate(x, y), width, height) +end + +-- Read the table, separate attributes from widgets. +local function parse_table(t, leave_empty) + local max = 0 + local attributes, widgets = {}, {} + for k,v in pairs(t) do + if type(k) == "number" then + if v then + -- Since `ipairs` doesn't always work on sparse tables, update + -- the maximum. + if k > max then + max = k + end + + widgets[k] = v + end + else + attributes[k] = v + end + end + + -- Pack the sparse table, if the container doesn't support sparse tables. + if not leave_empty then + widgets = util.table.from_sparse(widgets) + max = #widgets + end + + return max, attributes, widgets +end + +-- Recursively build a container from a declarative table. +local function drill(ids, content) + if not content then return end + + -- Alias `widget` to `layout` as they are handled the same way. + content.layout = content.layout or content.widget + + -- Make sure the layout is not indexed on a function. + local layout = type(content.layout) == "function" and content.layout() or content.layout + + -- Create layouts based on metatable's __call. + local l = layout.is_widget and layout or layout() + + -- Get the number of children widgets (including nil widgets). + local max, attributes, widgets = parse_table(content, l.allow_empty_widget) + + -- Get the optional identifier to create a virtual widget tree to place + -- in an "access table" to be able to retrieve the widget. + local id = attributes.id + + -- Clear the internal attributes. + attributes.id, attributes.layout, attributes.widget = nil, nil, nil + + -- Set layout attributes. + -- This has to be done before the widgets are added because it might affect + -- the output. + for attr, val in pairs(attributes) do + if l["set_"..attr] then + l["set_"..attr](l, val) + elseif type(l[attr]) == "function" then + l[attr](l, val) + else + l[attr] = val + end + end + + -- Add all widgets. + for k = 1, max do + -- ipairs cannot be used on sparse tables. + local v, id2, e = widgets[k], id, nil + if v then + -- It is another declarative container, parse it. + if not v.is_widget then + e, id2 = drill(ids, v) + widgets[k] = e + end + base.check_widget(widgets[k]) + + -- Place the widget in the access table. + if id2 then + l [id2] = e + ids[id2] = ids[id2] or {} + table.insert(ids[id2], e) + end + end + end + -- Replace all children (if any) with the new ones. + l:set_children(widgets) + return l, id +end + +-- Only available when the declarative system is used. +local function get_children_by_id(self, name) + if rawget(self, "_private") then + return self._private.by_id[name] or {} + else + return rawget(self, "_by_id")[name] or {} + end +end + +--- Set a declarative widget hierarchy description. +-- +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html). +-- @tab args A table containing the widget's disposition. +-- @function setup +function base.widget:setup(args) + local f,ids = self.set_widget or self.add or self.set_first,{} + local w, id = drill(ids, args) + f(self,w) + if id then + -- Avoid being dropped by wibox metatable -> drawin + rawset(self, id, w) + ids[id] = ids[id] or {} + table.insert(ids[id], 1, w) + end + + if rawget(self, "_private") then + self._private.by_id = ids + else + rawset(self, "_by_id", ids) + end + + rawset(self, "get_children_by_id", get_children_by_id) +end + +--- Create a widget from a declarative description. +-- +-- See [The declarative layout system](../documentation/03-declarative-layout.md.html). +-- @tab args A table containing the widgets disposition. +-- @function wibox.widget.base.make_widget_declarative +function base.make_widget_declarative(args) + local ids = {} + + if (not args.layout) and (not args.widget) then + args.widget = base.make_widget(nil, args.id) + end + + local w, id = drill(ids, args) + + local mt = getmetatable(w) or {} + local orig_string = tostring(w) + + -- Add the main id (if any) + if id then + ids[id] = ids[id] or {} + table.insert(ids[id], 1, w) + end + + if rawget(w, "_private") then + w._private.by_id = ids + else + rawset(w, "_by_id", ids) + end + + rawset(w, "get_children_by_id", get_children_by_id) + + mt.__tostring = function() + return string.format("%s (%s)", id or w.widget_name or "N/A", orig_string) + end + + return setmetatable(w, mt) +end + +--- Create an empty widget skeleton. +-- +-- See [Creating new widgets](../documentation/04-new-widget.md.html). +-- @tparam[opt] widget proxy If this is set, the returned widget will be a +-- proxy for this widget. It will be equivalent to this widget. +-- This means it looks the same on the screen. +-- @tparam[opt] string widget_name Name of the widget. If not set, it will be +-- set automatically via @{gears.object.modulename}. +-- @tparam[opt={}] table args Widget settings +-- @tparam[opt=false] boolean args.enable_properties Enable automatic getter +-- and setter methods. +-- @tparam[opt=nil] table args.class The widget class +-- @see fit_widget +-- @function wibox.widget.base.make_widget +function base.make_widget(proxy, widget_name, args) + args = args or {} + local ret = object { + enable_properties = args.enable_properties, + class = args.class, + } + + -- Backwards compatibility. + -- TODO: Remove this + ret:connect_signal("widget::updated", function() + ret:emit_signal("widget::layout_changed") + ret:emit_signal("widget::redraw_needed") + end) + + -- Create a table used to store the widgets internal data. + rawset(ret, "_private", {}) + + -- No buttons yet. + ret._private.widget_buttons = {} + + -- Widget is visible. + ret._private.visible = true + + -- Widget is fully opaque. + ret._private.opacity = 1 + + -- Differentiate tables from widgets. + rawset(ret, "is_widget", true) + + -- Size is not restricted/forced. + ret._private.forced_width = nil + ret._private.forced_height = nil + + -- Make buttons work. + ret:connect_signal("button::press", function(...) + return base.handle_button("press", ...) + end) + ret:connect_signal("button::release", function(...) + return base.handle_button("release", ...) + end) + + if proxy then + rawset(ret, "fit", function(_, context, width, height) + return base.fit_widget(ret, context, proxy, width, height) + end) + rawset(ret, "layout", function(_, _, width, height) + return { base.place_widget_at(proxy, 0, 0, width, height) } + end) + proxy:connect_signal("widget::layout_changed", function() + ret:emit_signal("widget::layout_changed") + end) + proxy:connect_signal("widget::redraw_needed", function() + ret:emit_signal("widget::redraw_needed") + end) + end + + -- Set up caches. + clear_caches(ret) + ret:connect_signal("widget::layout_changed", function() + clear_caches(ret) + end) + + -- Add functions. + for k, v in pairs(base.widget) do + rawset(ret, k, v) + end + + -- Add __tostring method to metatable. + rawset(ret, "widget_name", widget_name or object.modulename(3)) + local mt = getmetatable(ret) or {} + local orig_string = tostring(ret) + mt.__tostring = function() + return string.format("%s (%s)", ret.widget_name, orig_string) + end + return setmetatable(ret, mt) +end + +--- Generate an empty widget which takes no space and displays nothing. +-- @function wibox.widget.base.empty_widget +function base.empty_widget() + return base.make_widget() +end + +--- Do some sanity checking on a widget. +-- +-- This function raises an error if the widget is not valid. +-- @function wibox.widget.base.check_widget +function base.check_widget(widget) + assert(type(widget) == "table", "Type should be table, but is " .. tostring(type(widget))) + assert(widget.is_widget, "Argument is not a widget!") + for _, func in pairs({ "connect_signal", "disconnect_signal" }) do + assert(type(widget[func]) == "function", func .. " is not a function") + end +end + +return base + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |