From 4b21ca918421026dbcca90c52d123c019979f73f Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 17 May 2016 00:34:09 -0400 Subject: [PATCH 1/7] object: Add dynamic properties support. Similar systems already exist un luaobject, wibox and the declarative widget system. This close the gap and also bring the property based syntax to wibox and other gears.object users. While this need to be enabled explicitly for legacy reasons, it doesn't break the API. Once widespread, this implementation will replace the one found in wibox.widget.base_widget. --- lib/gears/object.lua | 75 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/lib/gears/object.lua b/lib/gears/object.lua index 9c21e6810..600725554 100644 --- a/lib/gears/object.lua +++ b/lib/gears/object.lua @@ -122,11 +122,58 @@ function object:emit_signal(name, ...) end end ---- Returns a new object. You can call :emit_signal(), :disconnect_signal, --- :connect_signal() and :add_signal() on the resulting object. -local function new() +local function get_miss(self, key) + local class = rawget(self, "_class") + + if rawget(self, "get_"..key) then + return rawget(self, "get_"..key)(self) + elseif class and class["get_"..key] then + return class["get_"..key](self) + elseif class then + return class[key] + end + +end + +local function set_miss(self, key, value) + local class = rawget(self, "_class") + + if rawget(self, "set_"..key) then + return rawget(self, "set_"..key)(self, value) + elseif class and class["set_"..key] then + return class["set_"..key](self, value) + elseif rawget(self, "_enable_auto_signals") then + local changed = class[key] ~= value + class[key] = value + + if changed then + self:emit_signal("property::"..key, value) + end + else + return rawset(self, key, value) + end +end + +--- Returns a new object. You can call `:emit_signal()`, `:disconnect_signal()`, +-- `:connect_signal()` and `:add_signal()` on the resulting object. +-- +-- Note that `args.enable_auto_signals` is only supported when +-- `args.enable_properties` is true. +-- +-- @tparam[opt={}] table args The arguments +-- @tparam[opt=false] boolean args.enable_properties Automatically call getters and setters +-- @tparam[opt=false] boolean args.enable_auto_signals Generate "property::xxxx" signals +-- when an unknown property is set. +-- @tparam[opt=nil] table args.class +-- @treturn table A new object +-- @function gears.object +local function new(args) + args = args or {} local ret = {} + -- Automatic signals cannot work without both miss handlers. + assert(not (args.enable_auto_signals and args.enable_properties ~= true)) + -- Copy all our global functions to our new object for k, v in pairs(object) do if type(v) == "function" then @@ -136,7 +183,27 @@ local function new() ret._signals = {} - return ret + local mt = {} + + -- Look for methods in another table + ret._class = args.class + ret._enable_auto_signals = args.enable_auto_signals + + -- To catch all changes, a proxy is required + if args.enable_auto_signals then + ret._class = ret._class and setmetatable({}, {__index = args.class}) or {} + end + + if args.enable_properties then + -- Check got existing get_xxxx and set_xxxx + mt.__index = get_miss + mt.__newindex = set_miss + elseif args.class then + -- Use the class table a miss handler + mt.__index = ret._class + end + + return setmetatable(ret, mt) end function object.mt.__call(_, ...) From b1c33fbd09fb4ba14a3ee6a05f723f96cedf77a8 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 17 May 2016 00:38:46 -0400 Subject: [PATCH 2/7] object: Add type information to documentation --- lib/gears/object.lua | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/gears/object.lua b/lib/gears/object.lua index 600725554..92c4a1706 100644 --- a/lib/gears/object.lua +++ b/lib/gears/object.lua @@ -21,10 +21,10 @@ local function check(obj) end --- Find a given signal --- @param obj The object to search in --- @param name The signal to find --- @param error_msg Error message for if the signal is not found --- @return The signal table +-- @tparam table obj The object to search in +-- @tparam string name The signal to find +-- @tparam string error_msg Error message for if the signal is not found +-- @treturn table The signal table local function find_signal(obj, name, error_msg) check(obj) if not obj._signals[name] then @@ -34,7 +34,7 @@ local function find_signal(obj, name, error_msg) end --- Add a signal to an object. All signals must be added before they can be used. --- @param name The name of the new signal. +-- @tparam string name The name of the new signal. function object:add_signal(name) check(self) assert(type(name) == "string", "name must be a string, got: " .. type(name)) @@ -46,9 +46,9 @@ function object:add_signal(name) end end ---- Connect to a signal --- @param name The name of the signal --- @param func The callback to call when the signal is emitted +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted function object:connect_signal(name, func) assert(type(func) == "function", "callback must be a function, got: " .. type(func)) local sig = find_signal(self, name, "connect to") @@ -88,8 +88,8 @@ end --- Connect to a signal weakly. This allows the callback function to be garbage -- collected and automatically disconnects the signal when that happens. --- @param name The name of the signal --- @param func The callback to call when the signal is emitted +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted function object:weak_connect_signal(name, func) assert(type(func) == "function", "callback must be a function, got: " .. type(func)) local sig = find_signal(self, name, "connect to") @@ -97,18 +97,18 @@ function object:weak_connect_signal(name, func) sig.weak[func] = make_the_gc_obey(func) end ---- Disonnect to a signal --- @param name The name of the signal --- @param func The callback that should be disconnected +--- Disonnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected function object:disconnect_signal(name, func) local sig = find_signal(self, name, "disconnect from") sig.weak[func] = nil sig.strong[func] = nil end ---- Emit a signal +--- Emit a signal. -- --- @param name The name of the 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() From adebef629b5b200e6a49accae62eeac7a7b62452 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 17 May 2016 00:44:20 -0400 Subject: [PATCH 3/7] object: Add an header description --- lib/gears/object.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/gears/object.lua b/lib/gears/object.lua index 92c4a1706..b9bae661a 100644 --- a/lib/gears/object.lua +++ b/lib/gears/object.lua @@ -1,4 +1,9 @@ --------------------------------------------------------------------------- +-- The object oriented programming base class used by various Awesome +-- widgets and components. +-- +-- It provide basic observer pattern, signaling and dynamic properties. +-- -- @author Uli Schlachter -- @copyright 2010 Uli Schlachter -- @release @AWESOME_VERSION@ From 08df8fbf03c5a200a9765d935e4ae2928126a138 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 17 May 2016 00:58:33 -0400 Subject: [PATCH 4/7] doc: Add a generic template for text only tests --- tests/examples/text/template.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/examples/text/template.lua diff --git a/tests/examples/text/template.lua b/tests/examples/text/template.lua new file mode 100644 index 000000000..fa7d093cf --- /dev/null +++ b/tests/examples/text/template.lua @@ -0,0 +1,19 @@ +local file_path, _, luacovpath = ... + +-- Set the global shims +-- luacheck: globals awesome root tag screen client mouse drawin +awesome = require( "awesome" ) +root = require( "root" ) +tag = require( "tag" ) +screen = require( "screen" ) +client = require( "client" ) +mouse = require( "mouse" ) +drawin = require( "drawin" ) + +-- If luacov is available, use it. Else, do nothing. +pcall(function() + require("luacov.runner")(luacovpath) +end) + +-- Execute the test +loadfile(file_path)() From f810d78e7b90612aceba2ade06a9a48724a95443 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 17 May 2016 00:59:48 -0400 Subject: [PATCH 5/7] object: Add a signal example --- lib/gears/object.lua | 4 +++ tests/examples/text/gears/object/signal.lua | 27 +++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/examples/text/gears/object/signal.lua diff --git a/lib/gears/object.lua b/lib/gears/object.lua index b9bae661a..7bf63de3d 100644 --- a/lib/gears/object.lua +++ b/lib/gears/object.lua @@ -39,6 +39,8 @@ local function find_signal(obj, name, error_msg) end --- Add a signal to an object. All signals must be added before they can be used. +-- +--@DOC_text_gears_object_signal_EXAMPLE@ -- @tparam string name The name of the new signal. function object:add_signal(name) check(self) @@ -54,6 +56,7 @@ end --- Connect to a signal. -- @tparam string name The name of the signal -- @tparam function func The callback to call when the signal is emitted +-- @see add_signal function object:connect_signal(name, func) assert(type(func) == "function", "callback must be a function, got: " .. type(func)) local sig = find_signal(self, name, "connect to") @@ -105,6 +108,7 @@ end --- Disonnect to a signal. -- @tparam string name The name of the signal -- @tparam function func The callback that should be disconnected +-- @see add_signal function object:disconnect_signal(name, func) local sig = find_signal(self, name, "disconnect from") sig.weak[func] = nil diff --git a/tests/examples/text/gears/object/signal.lua b/tests/examples/text/gears/object/signal.lua new file mode 100644 index 000000000..50f9c1986 --- /dev/null +++ b/tests/examples/text/gears/object/signal.lua @@ -0,0 +1,27 @@ +local gears = require("gears") --DOC_HIDE + +local o = gears.object{} + + -- Add a new signals to the object. This is used to catch typos +o:add_signal "my_signal" + + -- Function can be attached to signals +local function slot(obj, a, b, c) + print("In slot", obj, a, b, c) +end + +o:connect_signal("my_signal", slot) + + -- Emit can be done without argument. In that case, the object will be + -- implicitly added as an argument. +o:emit_signal "my_signal" + + -- It is also possible to add as many random arguments are required. +o:emit_signal("my_signal", "foo", "bar", 42) + + -- Finally, to allow the object to be garbage collected (the memory freed), it + -- is necessary to disconnect the signal or use `weak_connect_signal` +o:disconnect_signal("my_signal", slot) + + -- This time, the `slot` wont be called as it is no longer connected. +o:emit_signal "my_signal" From d6a7b6c645b9e75decd1aa9b038d9054a1c2556b Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 17 May 2016 01:17:22 -0400 Subject: [PATCH 6/7] object: Add a dynamic property example --- lib/gears/object.lua | 1 + .../examples/text/gears/object/properties.lua | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/examples/text/gears/object/properties.lua diff --git a/lib/gears/object.lua b/lib/gears/object.lua index 7bf63de3d..85bfcc3c2 100644 --- a/lib/gears/object.lua +++ b/lib/gears/object.lua @@ -169,6 +169,7 @@ end -- Note that `args.enable_auto_signals` is only supported when -- `args.enable_properties` is true. -- +--@DOC_text_gears_object_properties_EXAMPLE@ -- @tparam[opt={}] table args The arguments -- @tparam[opt=false] boolean args.enable_properties Automatically call getters and setters -- @tparam[opt=false] boolean args.enable_auto_signals Generate "property::xxxx" signals diff --git a/tests/examples/text/gears/object/properties.lua b/tests/examples/text/gears/object/properties.lua new file mode 100644 index 000000000..032ff9cce --- /dev/null +++ b/tests/examples/text/gears/object/properties.lua @@ -0,0 +1,55 @@ +local gears = require("gears") --DOC_HIDE + + -- Create a class for this object. It will be used as a backup source for + -- methods and acessors. It is also possible to set them diretly on the + -- object. +local class = {} + +function class:get_foo() + print("In get foo", self._foo or "bar") + return self._foo or "bar" +end + +function class:set_foo(value) + print("In set foo", value) + + -- In case it is necessary to bypass the object property system, use + -- `rawset` + rawset(self, "_foo", value) + + -- When using custom accessors, the signals need to be handled manually + self:emit_signal("property::foo", value) +end + +function class:method(a, b, c) + print("In a mathod", a, b, c) +end + +local o = gears.object { + class = class, + enable_properties = true, + enable_auto_signals = true, +} + +o:add_signal "property::foo" + +print(o.foo) + +o.foo = 42 + +print(o.foo) + +o:method(1, 2, 3) + + -- Random properties can also be added, the signal will be emited automatically. +o:add_signal "property::something" + +o:connect_signal("property::something", function(obj, value) + print("In the connection handler!", obj, value) +end) + +print(o.something) + +o.something = "a cow" + +print(o.something) From 625a5dd407ff51a3bf829caac95ee01d19d821e8 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Wed, 18 May 2016 00:47:46 -0400 Subject: [PATCH 7/7] tests: Test gears.object optional features --- spec/gears/object_spec.lua | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/spec/gears/object_spec.lua b/spec/gears/object_spec.lua index 2f41f0d1f..e73bc43ed 100644 --- a/spec/gears/object_spec.lua +++ b/spec/gears/object_spec.lua @@ -161,6 +161,64 @@ describe("gears.object", function() assert.is_true(finalized) obj:emit_signal("signal") end) + + it("dynamic property disabled", function() + local class = {} + function class:get_foo() return "bar" end + function class:set_foo() end + + local obj2 = object{class=class} + + obj2.foo = 42 + + assert.is_true(obj2.foo == 42) + end) + + it("dynamic property disabled", function() + local class = {} + function class:get_foo() return "bar" end + function class:set_foo() end + + local obj2 = object{class=class, enable_properties = true} + + obj2.foo = 42 + + assert.is_true(obj2.foo == "bar") + end) + + it("auto emit disabled", function() + local got_it = false + obj:add_signal("property::foo") + obj:connect_signal("property::foo", function() got_it=true end) + + obj.foo = 42 + + assert.is_false(got_it) + end) + + it("auto emit enabled", function() + local got_it = false + local obj2 = object{enable_auto_signals=true, enable_properties=true} + obj2:add_signal("property::foo") + obj2:connect_signal("property::foo", function() got_it=true end) + + obj2.foo = 42 + + assert.is_true(got_it) + end) + + it("auto emit enabled", function() + assert.has.errors(function() + local obj2 = object{enable_auto_signals=true, enable_properties=true} + obj2.foo = "bar" + end) + end) + + it("auto emit without dynamic properties", function() + assert.has.errors(function() + object{enable_auto_signals=true, enable_properties=false} + end) + end) end) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80