diff options
Diffstat (limited to 'awesome/lib/gears/object.lua')
-rw-r--r-- | awesome/lib/gears/object.lua | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/awesome/lib/gears/object.lua b/awesome/lib/gears/object.lua new file mode 100644 index 0000000..e6436e3 --- /dev/null +++ b/awesome/lib/gears/object.lua @@ -0,0 +1,285 @@ +--------------------------------------------------------------------------- +-- 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 +-- @classmod gears.object +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local pairs = pairs +local type = type +local error = error +local properties = require("gears.object.properties") + +local object = { properties = properties, mt = {} } + +--- Verify that obj is indeed a valid object as returned by new() +local function check(obj) + if type(obj) ~= "table" or type(obj._signals) ~= "table" then + error("called on non-object") + end +end + +--- Find a given signal +-- @tparam table obj The object to search in +-- @tparam string name The signal to find +-- @treturn table The signal table +local function find_signal(obj, name) + check(obj) + if not obj._signals[name] then + assert(type(name) == "string", "name must be a string, got: " .. type(name)) + obj._signals[name] = { + strong = {}, + weak = setmetatable({}, { __mode = "kv" }) + } + end + return obj._signals[name] +end + +function object.add_signal() + require("awful.util").deprecate("Use signals without explicitly adding them. This is now done implicitly.") +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 +function object:connect_signal(name, func) + assert(type(func) == "function", "callback must be a function, got: " .. type(func)) + local sig = find_signal(self, name) + assert(sig.weak[func] == nil, "Trying to connect a strong callback which is already connected weakly") + sig.strong[func] = true +end + +local function make_the_gc_obey(func) + if _VERSION <= "Lua 5.1" then + -- Lua 5.1 only has the behaviour we want if a userdata is used as the + -- value in a weak table. Thus, do some magic so that we get a userdata. + + -- luacheck: globals newproxy getfenv setfenv + local userdata = newproxy(true) + getmetatable(userdata).__gc = function() end + -- Now bind the lifetime of userdata to the lifetime of func. For this, + -- we mess with the function's environment and add a table for all the + -- various userdata that it should keep alive. + local key = "_secret_key_used_by_gears_object_in_Lua51" + local old_env = getfenv(func) + if old_env[key] then + -- Assume the code in the else branch added this and the function + -- already has its own, private environment + table.insert(old_env[key], userdata) + else + -- No table yet, add it + local new_env = { [key] = { userdata } } + setmetatable(new_env, { __index = old_env, __newindex = old_env }) + setfenv(func, new_env) + end + assert(_G[key] == nil, "Something broke, things escaped to _G") + return userdata + end + -- Lua 5.2+ already behaves the way we want with functions directly, no magic + return func +end + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- @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) + assert(sig.strong[func] == nil, "Trying to connect a weak callback which is already connected strongly") + sig.weak[func] = make_the_gc_obey(func) +end + +--- 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) + sig.weak[func] = nil + sig.strong[func] = nil +end + +--- 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 object:emit_signal(name, ...) + local sig = find_signal(self, name) + for func in pairs(sig.strong) do + func(self, ...) + end + for func in pairs(sig.weak) do + func(self, ...) + end +end + +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 + elseif (not rawget(self, "get_"..key)) + and not (class and class["get_"..key]) then + return rawset(self, key, value) + else + error("Cannot set '" .. tostring(key) .. "' on " .. tostring(self) + .. " because it is read-only") + end +end + +--- Returns a new object. You can call `:emit_signal()`, `:disconnect_signal()` +-- and `:connect_signal()` on the resulting object. +-- +-- Note that `args.enable_auto_signals` is only supported when +-- `args.enable_properties` is true. +-- +-- +-- +-- +--**Usage example output**: +-- +-- In get foo bar +-- bar +-- In set foo 42 +-- In get foo 42 +-- 42 +-- In a mathod 1 2 3 +-- nil +-- In the connection handler! a cow +-- a cow +-- +-- +-- @usage +-- -- Create a class for this object. It will be used as a backup source for +-- -- methods and accessors. It is also possible to set them directly 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, +--} +--print(o.foo) +--o.foo = 42 +--print(o.foo) +--o:method(1, 2, 3) +-- -- Random properties can also be added, the signal will be emitted automatically. +--o:connect_signal('property::something', function(obj, value) +-- assert(obj == o) +-- print('In the connection handler!', value) +--end) +--print(o.something) +--o.something = 'a cow' +--print(o.something) +-- @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 + ret[k] = v + end + end + + ret._signals = {} + + 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(_, ...) + return new(...) +end + +--- Helper function to get the module name out of `debug.getinfo`. +-- @usage +-- local mt = {} +-- mt.__tostring = function(o) +-- return require("gears.object").modulename(2) +-- end +-- return setmetatable(ret, mt) +-- +-- @tparam[opt=2] integer level Level for `debug.getinfo(level, "S")`. +-- Typically 2 or 3. +-- @treturn string The module name, e.g. "wibox.container.background". +function object.modulename(level) + return debug.getinfo(level, "S").source:gsub(".*/lib/", ""):gsub("/", "."):gsub("%.lua", "") +end + +return setmetatable(object, object.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |