diff --git a/lib/gears/object.lua b/lib/gears/object.lua index 9c21e681..85bfcc3c 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@ @@ -21,10 +26,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 +39,9 @@ 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. +-- +--@DOC_text_gears_object_signal_EXAMPLE@ +-- @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 +53,10 @@ 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 +-- @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") @@ -88,8 +96,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 +105,19 @@ 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 +-- @see add_signal 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() @@ -122,11 +131,59 @@ 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. +-- +--@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 +-- 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 +193,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(_, ...) diff --git a/spec/gears/object_spec.lua b/spec/gears/object_spec.lua index 2f41f0d1..e73bc43e 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 diff --git a/tests/examples/text/gears/object/properties.lua b/tests/examples/text/gears/object/properties.lua new file mode 100644 index 00000000..032ff9cc --- /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) diff --git a/tests/examples/text/gears/object/signal.lua b/tests/examples/text/gears/object/signal.lua new file mode 100644 index 00000000..50f9c198 --- /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" diff --git a/tests/examples/text/template.lua b/tests/examples/text/template.lua new file mode 100644 index 00000000..fa7d093c --- /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)()