From 67b2b26683de9a8801103777c7550d9dd56d8cb1 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 16 Oct 2022 01:30:57 -0700 Subject: [PATCH] template: Bring to feature parity with the awful.widget.common implementation. `awful.widget.common` has the ability to set "roles" and apply properties to the entire widget tree. This was missing from the previous commit. --- lib/wibox/widget/template.lua | 281 +++++++++++++++--- .../sequences/widget/tmpl/bind_property.lua | 96 ++++++ .../template/basic_textbox_declarative.lua | 25 -- .../examples/wibox/widget/template/clone1.lua | 149 ++++++++++ .../concrete_implementation_module.lua | 27 +- .../widget/template/set_property_custom.lua | 67 +++++ .../widget/template/set_property_existing.lua | 52 ++++ 7 files changed, 613 insertions(+), 84 deletions(-) create mode 100644 tests/examples/sequences/widget/tmpl/bind_property.lua delete mode 100644 tests/examples/wibox/widget/template/basic_textbox_declarative.lua create mode 100644 tests/examples/wibox/widget/template/clone1.lua create mode 100644 tests/examples/wibox/widget/template/set_property_custom.lua create mode 100644 tests/examples/wibox/widget/template/set_property_existing.lua diff --git a/lib/wibox/widget/template.lua b/lib/wibox/widget/template.lua index 709a0afb3..b67b351bd 100644 --- a/lib/wibox/widget/template.lua +++ b/lib/wibox/widget/template.lua @@ -17,11 +17,6 @@ -- --@DOC_wibox_widget_template_basic_textbox_EXAMPLE@ -- --- Alternatively, you can declare the `template` widget instance using the --- declarative pattern (both variants are strictly equivalent): --- ---@DOC_wibox_widget_template_basic_textbox_declarative_EXAMPLE@ --- -- Usage in libraries -- ================== -- @@ -64,36 +59,62 @@ local template = { queued_updates = {}, } +local function lazy_load_child(self) + if self._private.widget then return self._private.widget end + + local widget_instance = wbase.make_widget_from_value(self._private.widget_template) + + if widget_instance then + wbase.check_widget(widget_instance) + end + + self._private.widget = widget_instance + + local rem = {} + + for k, conn in ipairs(self._private.connections) do + conn.apply() + + if conn.once then + if conn.src_obj then + conn.src_obj:disconnect_signal("property::"..conn.src_prop, conn.apply) + end + table.insert(rem, k) + end + end + + for i = #rem, 1, -1 do + table.remove(self._private.connections, rem[i]) + end + + return self._private.widget +end + -- Layout this layout. -- @method layout -- @hidden function template:layout(_, width, height) - if not self._private.widget then - return - end + local w = lazy_load_child(self) - return { wbase.place_widget_at(self._private.widget, 0, 0, width, height) } + if not w then return end + + return { wbase.place_widget_at(w, 0, 0, width, height) } end -- Fit this layout into the given area. -- @method fit -- @hidden function template:fit(context, width, height) - if not self._private.widget then + local w = lazy_load_child(self) + + if not w then return 0, 0 end - return wbase.fit_widget(self, context, self._private.widget, width, height) + return wbase.fit_widget(self, context, w, width, height) end --- Draw the widget if it's actually a widget instance --- @method draw --- @hidden -function template:draw(...) - if type(self._private.widget.draw) == "function" then - return self._private.widget:draw(...) - end -end +function template:draw() end -- Call the update widget method now and clean the queue for this widget -- instance. @@ -109,14 +130,17 @@ function template:_do_update_now() end --- Update the widget. +-- -- This function will call the `update_callback` function at the end of the -- current GLib event loop. Updates are batched by event loop, it means that the -- widget can only be update once by event loop. If the `template:update` method -- is called multiple times during the same GLib event loop, only the first call -- will be run. -- All arguments are passed to the queued `update_callback` call. --- @tparam[opt] table args A table to pass to the widget update function. +-- +-- @tparam[opt={}] table args A table to pass to the widget update function. -- @method update +-- @noreturn function template:update(args) if type(args) == "table" then self._private.update_args = gtable.crush( @@ -134,36 +158,158 @@ function template:update(args) end --- Change the widget template. --- @tparam table|widget widget_template The new widget to use as a +-- +-- Note that this will discard the existing widget instance. Thus, any +-- `set_property` will no longer be honored. `bind_property`, on the other hand, +-- will still be honored. +-- +-- @property template +-- @tparam[opt=nil] template|nil template The new widget to use as a -- template. --- @method set_template -- @emits widget::redraw_needed --- @hidden +-- @emits widget::layout_changed +-- @propemits true false + function template:set_template(widget_template) - local widget = widget_template or wbase.empty_widget() + if widget_template == self._private.widget_template then return end - local widget_instance = wbase.make_widget_from_value(widget) - - if widget_instance then - wbase.check_widget(widget_instance) - end - - self._private.template = widget - self._private.widget = widget_instance - - -- We need to connect to these signals to actually redraw the template - -- widget when its child needs to. - local signals = { - "widget::redraw_needed", - "widget::layout_changed", - } - for _, signal in pairs(signals) do - self._private.widget:connect_signal(signal, function(...) - self:emit_signal(signal, ...) - end) - end + self._private.widget = nil + self._private.widget_template = widget_template self:emit_signal("widget::redraw_needed") + self:emit_signal("widget::layout_changed") + self:emit_signal("property::template", widget_template) +end + +--- Set a property on one or more template sub-widget instances. +-- +-- This method allows to set a value at any time on any of the sub widget of +-- the template. To use this, you can set, or document a set of `ids` which +-- your template support. These are usually referred as "roles" across the +-- other APIs. +-- +--@DOC_wibox_widget_template_set_property_existing_EXAMPLE@ +-- +-- It is also possible to take this one step further and apply a property +-- to the entire sub-widget tree. This allows users to implement their own +-- template even if it doesn't use the same "roles" as the default one: +-- +--@DOC_wibox_widget_template_set_property_custom_EXAMPLE@ +-- +-- @method set_property +-- @tparam string property The property name. +-- @param value The property value. +-- @tparam[opt=nil] nil|string|table ids A sub-widget `id` or list of `id`s. Use +-- `nil` for "all sub widgets". +-- @noreturn +-- @see bind_property +function template:set_property(property, value, ids) + local widgets + local target = self._private.widget + + -- Lazy load later. + if not target then + table.insert(self._private.connections, { + src_obj = nil, + src_prop = nil, + once = true, + apply = function() + self:set_property(property, value, ids) + end, + }) + return + end + + if ids then + widgets = {} + for _, id in ipairs(type(ids) == "string" and {ids} or ids) do + for _, widget in ipairs(target:get_children_by_id(id)) do + table.insert(widgets, widget) + end + end + end + + widgets = widgets or {target} + + for _, widget in ipairs(widgets) do + if widget["set_"..property] then + widget["set_"..property](widget, value) + end + end +end + +-- Do not use, backward compatibility only. +function template:_set_property_on_tree(w, property, value) + local apply_property + apply_property = function(wdgs) + for _, widget in ipairs(wdgs) do + if widget["set_"..property] then + widget["set_"..property](widget, value) + end + + if widget.get_children then + apply_property(widget:get_children()) + end + end + end + + apply_property({w}) +end + +--- Monitor the value of a property on a source object and apply it on a target. +-- +-- This is the equivalent of: +-- +-- src_obj:connect_signal( +-- "property::"..src_prop, +-- function(v) +-- self:set_property(dest_prop, v, dest_ids) +-- end +-- ) +-- self:set_property(dest_prop, v, dest_ids) +-- +-- @DOC_sequences_widget_tmpl_bind_property_EXAMPLE@ +-- +-- @method bind_property +-- @tparam object src_obj The source object (must be derived from `gears.object`). +-- @tparam string src_prop The source object property name. +-- @tparam string dest_prop The destination widget property name. +-- @tparam[opt=nil] table|string|nil dest_ids A sub-widget `id` or list of `id`s. +-- Use `nil` for "all sub widgets". +-- @noreturn +-- @see clear_bindings +-- @see set_property + +function template:bind_property(src_obj, src_prop, dest_prop, dest_ids) + local function apply() + self:set_property(dest_prop, src_obj[src_prop], dest_ids) + end + + table.insert(self._private.connections, { + src_obj = src_obj, + src_prop = src_prop, + apply = apply, + }) + + src_obj:connect_signal("property::"..src_prop, apply) + + apply() +end + + +--- Disconnect all signals created in `bind_property`. +-- @method clear_bindings +-- @tparam[opt=nil] widget|nil src_obj Disconnect only for this specific object. +-- @noreturn +-- @see bind_property + +function template:clear_bindings(src_obj) + for _, conn in ipairs(self._private.connections) do + if conn.src_obj and (conn.src_obj == src_obj or not src_obj) then + conn.src_obj:disconnect_signal("property::"..conn.src_prop, conn.apply) + end + end + self._private.connections = {} end --- Give the internal widget instance. @@ -171,7 +317,7 @@ end -- @method get_widget -- @hidden function template:get_widget() - return self._private.widget + return lazy_load_child(self) end --- Set the update_callback property. @@ -200,6 +346,27 @@ function template:set_update_now(update_now) end end + +--- Create a new `wibox.widget.template` instance using the same template. +-- +-- This copy will be blank. Note that `set_property`, `bind_property` or +-- `update_callback` from `self` will **NOT** be transferred over to the copy. +-- +-- The following example is a new widget to list a bunch of colors. It uses +-- `wibox.widget.template` to let the module user define their own color +-- widget. It does so by cloning the original template into new instances. This +-- example doesn't handle removing or updating them to keep the size small. +-- +--@DOC_wibox_widget_template_clone1_EXAMPLE@ +-- +-- @method clone +-- @treturn wibox.widget.template The copy. +function template:clone() + return template { + template = self._private.widget_template + } +end + --- Create a new `wibox.widget.template` instance. -- @tparam[opt] table args -- @tparam[opt] table|widget args.template The widget template to use. @@ -215,6 +382,7 @@ function template.new(args) local ret = wbase.make_widget(nil, nil, { enable_properties = true }) gtable.crush(ret, template, true) + ret._private.connections = {} ret:set_template(args.template) ret:set_update_callback(args.update_callback) @@ -223,9 +391,32 @@ function template.new(args) -- Apply the received buttons, visible, forced_width and so on gtable.crush(ret, args) + rawset(ret, "_is_template", true) + return ret end +--- Create a `wibox.widget.template` from a table. +-- +-- @staticfct wibox.widget.template.make_from_value +-- @tparam[opt=nil] table|wibox.widget.template|nil value A template declaration. +-- @treturn wibox.widget.template The template object. +function template.make_from_value(value) + if not value then return nil end + + assert( + not rawget(value, "is_widget"), + "This property requires a widget template, not a widget object.\n".. + "Use `wibox.template` instead of `wibox.widget`" + ) + + if rawget(value, "_is_template") then return value:clone() end + + return template.new { + template = value + } +end + function template.mt:__call(...) return template.new(...) end diff --git a/tests/examples/sequences/widget/tmpl/bind_property.lua b/tests/examples/sequences/widget/tmpl/bind_property.lua new file mode 100644 index 000000000..b8f1289e2 --- /dev/null +++ b/tests/examples/sequences/widget/tmpl/bind_property.lua @@ -0,0 +1,96 @@ + --DOC_GEN_IMAGE --DOC_HIDE_START --DOC_NO_USAGE +local module = ... +local wibox = require("wibox") +local beautiful = require("beautiful") +local gears = {surface = require("gears.surface")} + +client.focus = client.gen_fake{ + class = "client", + name = "A client!", + icon = beautiful.awesome_icon, +} + + --DOC_HIDE_END + + local my_template_widget = wibox.widget.template { + template = { + { + { + set_icon = function(self, icon) + self.image = gears.surface(icon) + end, + id = "icon_role", + widget = wibox.widget.imagebox + }, + { + id = "title_role", + widget = wibox.widget.textbox + }, + widget = wibox.layout.fixed.horizontal, + }, + widget = wibox.container.background, + id = "background_role", + set_urgent = function(self, status) + self.bg = status and "#ff0000" or nil + end, + forced_width = 200, --DOC_HIDE + forced_height = 24, --DOC_HIDE + } + } + +--DOC_NEWLINE +--DOC_HIDE_START +module.display_widget(my_template_widget, 200, 24) + +module.add_event("Original state", function() + --DOC_HIDE_END + + -- Use the normal widget properties. + my_template_widget:bind_property(client.focus, "name", "text", "title_role") + my_template_widget:bind_property(client.focus, "icon", "icon", "icon_role") + --DOC_NEWLINE + -- This one uses an inline setter method. + my_template_widget:bind_property(client.focus, "urgent", "urgent", "background_role") + + --DOC_HIDE_START +end) + +--DOC_NEWLINE +module.display_widget(my_template_widget, 200, 24) + +module.add_event("Change the client name.", function() + --DOC_HIDE_END + --DOC_NEWLINE + -- Change the client name. + client.focus.name = "New name!" + --DOC_HIDE_START +end) + +--DOC_NEWLINE +module.display_widget(my_template_widget, 200, 24) + +module.add_event("Make urgent", function() + --DOC_HIDE_END + --DOC_NEWLINE + -- Make urgent. + client.focus.urgent = true + --DOC_HIDE_START +end) + +module.display_widget(my_template_widget, 200, 24) + +module.add_event("Make not urgent", function() + --DOC_HIDE_END + --DOC_NEWLINE + -- "Make not urgent. + client.focus.urgent = false + --DOC_HIDE_START +end) + +module.display_widget(my_template_widget, 200, 24) + + +module.execute { display_screen = false, display_clients = true, + display_label = false, display_client_name = true, + display_mouse = true , +} diff --git a/tests/examples/wibox/widget/template/basic_textbox_declarative.lua b/tests/examples/wibox/widget/template/basic_textbox_declarative.lua deleted file mode 100644 index 9dad45fff..000000000 --- a/tests/examples/wibox/widget/template/basic_textbox_declarative.lua +++ /dev/null @@ -1,25 +0,0 @@ ---DOC_NO_USAGE - ---DOC_HIDE_START -local wibox = require("wibox") ---DOC_HIDE_END - - local my_template_widget = { - id = "mytemplatewidget", - template = wibox.widget.textbox, - update_callback = function(template_widget, args) - local text = args.text or "???" - template_widget.widget.text = text - end, - widget = wibox.widget.template, - } - ---DOC_HIDE_START - --- Generate the widget to use the update method in the example. --- A better approch would have been to use an ID and the `get_children_by_id` --- on the parent. -local widget = wibox.widget(my_template_widget) -widget:update { text = "Cool text to update the template!" } - --- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/tests/examples/wibox/widget/template/clone1.lua b/tests/examples/wibox/widget/template/clone1.lua new file mode 100644 index 000000000..330c6b606 --- /dev/null +++ b/tests/examples/wibox/widget/template/clone1.lua @@ -0,0 +1,149 @@ +--DOC_GEN_IMAGE --DOC_NO_USAGE + +--DOC_HIDE_START +local parent = ... +local wibox = require("wibox") +local gears = { + shape = require("gears.shape"), + surface = require("gears.surface"), + color = require("gears.color"), + table = require("gears.table"), +} +local beautiful = require("beautiful") + +client.focus = client.gen_fake{ + class = "client", + name = "A client!", + icon = beautiful.awesome_icon, +} + +--DOC_HIDE_END + -- Put this in a Lua file + local module = {} + --DOC_NEWLINE + + local default_template = wibox.widget.template { + template = { + { + { + { + forced_height = 16, + forced_width = 16, + shape = gears.shape.circle, + widget = wibox.widget.separator + }, + margins = 3, + widget = wibox.container.margin + }, + { + set_color = function(self, color) + self.text = color + end, + text = "N/A", + widget = wibox.widget.textbox + }, + spacing = 5, + widget = wibox.layout.fixed.horizontal, + }, + set_color = function(self, color) + self.border_color = gears.color.to_rgba_string(color):sub(1,7).."44" + self.fg = color + end, + border_width = 1, + shape = gears.shape.octogon, + widget = wibox.container.background, + forced_width = 100, --DOC_HIDE + } + } + --DOC_NEWLINE + + --- Set the widget template. + -- @property widget_template + -- @tparam[opt=nil] wibox.template|nil + function module:set_widget_template(t) + self._private.widget_template = wibox.widget.template.make_from_value(t) + end + + --DOC_NEWLINE + function module:get_widget_template() + return self._private.widget_template or default_template + end + --DOC_NEWLINE + + --- Add a color to the list. + function module:add_color(color_name) + local instance = self.widget_template:clone() + instance:set_property("color", color_name, nil, true) + + self._private.base_layout:add(instance) + end + --DOC_NEWLINE + + --- my_module_name constructor. + -- + -- @constructorfct my_module_name + -- @tparam[opt={}] table args + -- @tparam wibox.template args.widget_template A widget template. + -- @tparam wibox.layout args.base_layout A layout to use instead of + -- `wibox.layout.fixed.vertical`. + local function new(_, args) + args = args or {} + + --DOC_NEWLINE + -- Create the base_layout instance. + local l = wibox.widget.base.make_widget_from_value( + args.base_layout or wibox.widget { + spacing = 5, + widget = wibox.layout.fixed.vertical + } + ) + + --DOC_NEWLINE + -- Create the base widget as a proxy widget. + local ret = wibox.widget.base.make_widget(l, "my_module_name", { + enable_properties = true, + }) + + --DOC_NEWLINE + -- Set the default (fallback) template + ret._private.base_layout = l + + --DOC_NEWLINE + -- Add the methods and apply set the initial properties. + gears.table.crush(ret, module, true) + gears.table.crush(ret, args) + + --DOC_NEWLINE + return ret + end + + --[[ --DOC_HIDE + return setmetatable(module, {__call = new}) + ]]--DOC_HIDE + + +--[[ --DOC_HIDE +--DOC_NEWLINE +Now, lets use this module to list some colors: +--DOC_NEWLINE + local my_module_name = require("my_module_name") +--DOC_NEWLINE +]] --DOC_HIDE +local my_module_name = new --DOC_HIDE +--DOC_NEWLINE + + local my_color_list = my_module_name {} +--DOC_NEWLINE + + my_color_list:add_color("#ff0000") + my_color_list:add_color("#00ff00") + my_color_list:add_color("#0000ff") + my_color_list:add_color("#ff00ff") + my_color_list:add_color("#ffff00") + my_color_list:add_color("#00ffff") + +--DOC_HIDE_START + +parent:add(my_color_list) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/tests/examples/wibox/widget/template/concrete_implementation_module.lua b/tests/examples/wibox/widget/template/concrete_implementation_module.lua index 111b3f1dd..97deb39ec 100644 --- a/tests/examples/wibox/widget/template/concrete_implementation_module.lua +++ b/tests/examples/wibox/widget/template/concrete_implementation_module.lua @@ -1,4 +1,4 @@ ---DOC_NO_USAGE +--DOC_NO_USAGE --DOC_GEN_IMAGE --DOC_HIDE_START local parent = ... @@ -6,12 +6,12 @@ local parent = ... local gears = require("gears") local wibox = require("wibox") -local concrete_widget_template_builder --DOC_HIDE_END -- Build the default widget used as a fallback if user doesn't provide a template local default_widget = { - template = wibox.widget.textclock, + template = wibox.widget.textbox, + text = "N/A", update_callback = function(widget_template, args) local text = args and args.text or "???" widget_template.widget.text = text @@ -19,29 +19,28 @@ local concrete_widget_template_builder } --DOC_NEWLINE - function concrete_widget_template_builder(args) + local function concrete_widget_template_builder(args) args = args or {} --DOC_NEWLINE -- Build an instance of the template widget with either, the -- user provided parameters or the default - local ret = wibox.widget.template( - args.widget_template and args.widget_template or - default_widget - ) + local ret = wibox.widget.template { + template = args.widget_template or default_widget + } --DOC_NEWLINE - -- Patch the methods and fields the widget instance should have + -- Patch the methods and fields the widget instance should have --DOC_NEWLINE - -- e.g. Apply the received buttons, visible, forced_width and so on - gears.table.crush(ret, args) + -- e.g. Apply the received buttons, visible, forced_width and so on + gears.table.crush(ret, args) --DOC_NEWLINE -- Optionally, call update once with parameters to prepare the widget - ret:update { - text = "default text", - } + ret:update { + text = "default text", + } --DOC_NEWLINE return ret diff --git a/tests/examples/wibox/widget/template/set_property_custom.lua b/tests/examples/wibox/widget/template/set_property_custom.lua new file mode 100644 index 000000000..5123baf3b --- /dev/null +++ b/tests/examples/wibox/widget/template/set_property_custom.lua @@ -0,0 +1,67 @@ +--DOC_GEN_IMAGE --DOC_NO_USAGE + +--DOC_HIDE_START +local parent = ... +local wibox = require("wibox") +local gears = {share = require("gears.shape"), surface = require("gears.surface")} +local beautiful = require("beautiful") + +client.focus = client.gen_fake{ + class = "client", + name = "A client!", + icon = beautiful.awesome_icon, +} + +--DOC_HIDE_END + + local my_template_widget = wibox.widget.template { + template = { + { + { + id = "client_role", + set_client = function(self, c) + self.image = gears.surface(c.icon) + end, + widget = wibox.widget.imagebox + }, + { + id = "client_role", + set_client = function(self, c) + -- If the value can change, don't forget to connect + -- some signals: + local function update() + local txt = "Name: "..c.name + if c.minimized then + txt = txt .. " (minimized)" + end + self.markup = txt + end + + update() + c:connect_signal("property::name", update) + c:connect_signal("property::minimized", update) + end, + widget = wibox.widget.textbox + }, + widget = wibox.layout.fixed.horizontal, + }, + bg = "#0000ff", + fg = "#ffffff", + shape = gears.share.rounded_rect, + widget = wibox.container.background, + forced_width = 200, --DOC_HIDE + forced_height = 24, --DOC_HIDE + } + } + +--DOC_NEWLINE + -- Later in the code + local c = client.focus + my_template_widget:set_property("client", c, "client_role") + c.minimized = true + +--DOC_HIDE_START + +parent:add(my_template_widget) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/tests/examples/wibox/widget/template/set_property_existing.lua b/tests/examples/wibox/widget/template/set_property_existing.lua new file mode 100644 index 000000000..b915310ea --- /dev/null +++ b/tests/examples/wibox/widget/template/set_property_existing.lua @@ -0,0 +1,52 @@ +--DOC_GEN_IMAGE --DOC_NO_USAGE + +--DOC_HIDE_START +local parent = ... +local wibox = require("wibox") +local gears = {share = require("gears.shape"), surface = require("gears.surface")} +local beautiful = require("beautiful") + +client.focus = client.gen_fake{ + class = "client", + name = "A client!", + icon = beautiful.awesome_icon, +} + +--DOC_HIDE_END + + local my_template_widget = wibox.widget.template { + template = { + { + { + id = "icon_role", + widget = wibox.widget.imagebox + }, + { + id = "title_role", + widget = wibox.widget.textbox + }, + widget = wibox.layout.fixed.horizontal, + }, + id = "background_role", + widget = wibox.container.background, + forced_width = 200, --DOC_HIDE + forced_height = 24, --DOC_HIDE + } + } + +--DOC_NEWLINE + -- Later in the code + local c = client.focus + my_template_widget:set_property("image", gears.surface(c.icon),"icon_role") + my_template_widget:set_property("text", c.name, "title_role") + my_template_widget:set_property("bg", "#0000ff", "background_role") + my_template_widget:set_property("fg", "#ffffff", "background_role") + my_template_widget:set_property( + "shape", gears.share.rounded_rect, "background_role" + ) + +--DOC_HIDE_START + +parent:add(my_template_widget) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80