diff --git a/lib/naughty/action.lua b/lib/naughty/action.lua new file mode 100644 index 00000000..a16a9570 --- /dev/null +++ b/lib/naughty/action.lua @@ -0,0 +1,127 @@ +--------------------------------------------------------------------------- +--- A notification action. +-- +-- A notification can have multiple actions to chose from. This module allows +-- to manage such actions. +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2019 Emmanuel Lepage Vallee +-- @classmod naughty.action +--------------------------------------------------------------------------- +local gtable = require("gears.table" ) +local gobject = require("gears.object") + +local action = {} + +--- Create a new action. +-- @function naughty.action +-- @tparam table args The arguments. +-- @tparam string args.name The name. +-- @tparam string args.position The position. +-- @tparam string args.icon The icon. +-- @tparam naughty.notification args.notification The notification object. +-- @tparam boolean args.selected If this action is currently selected. +-- @return A new action. + +-- The action name. +-- @property name +-- @tparam string name The name. + +-- If the action is selected. +-- +-- Only a single action can be selected per notification. It will be applied +-- when `my_notification:apply()` is called. +-- +-- @property selected +-- @param boolean + +--- The action position (index). +-- @property position +-- @param number + +--- The action icon. +-- @property icon +-- @param gears.surface + +--- The notification. +-- @property notification +-- @tparam naughty.notification notification + +--- When a notification is invoked. +-- @signal invoked + +function action:get_selected() + return self._private.selected +end + +function action:set_selected(value) + self._private.selected = value + self:emit_signal("property::selected", value) + + if self._private.notification then + self._private.notification:emit_signal("property::actions") + end + + --TODO deselect other actions from the same notification +end + +function action:get_position() + return self._private.position +end + +function action:set_position(value) + self._private.position = value + self:emit_signal("property::position", value) + + if self._private.notification then + self._private.notification:emit_signal("property::actions") + end + + --TODO make sure the position is unique +end + +for _, prop in ipairs { "name", "icon", "notification" } do + action["get_"..prop] = function(self) + return self._private[prop] + end + + action["set_"..prop] = function(self, value) + self._private[prop] = value + self:emit_signal("property::"..prop, value) + + -- Make sure widgets with as an actionlist is updated. + if self._private.notification then + self._private.notification:emit_signal("property::actions") + end + end +end + +--- Execute this action. +function action:invoke() + assert(self._private.notification, + "Cannot invoke an action without a notification") + + self:emit_signal("invoked") +end + +local function new(_, args) + args = args or {} + local ret = gobject { enable_properties = true } + + gtable.crush(ret, action, true) + + local default = { + -- See "table 1" of the spec about the default name + name = args.name or "default", + selected = args.selected == true, + position = args.position, + icon = args.icon, + notification = args.notification, + } + + rawset(ret, "_private", default) + + return ret +end + +return setmetatable(action, {__call = new}) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index ea382740..d7fee714 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -497,8 +497,7 @@ end -- @tparam[opt] func args.callback Function that will be called with all arguments. -- The notification will only be displayed if the function returns true. -- Note: this function is only relevant to notifications sent via dbus. --- @tparam[opt] table args.actions Mapping that maps a string to a callback when this --- action is selected. +-- @tparam[opt] table args.actions A list of `naughty.action`s. -- @bool[opt=false] args.ignore_suspend If set to true this notification -- will be shown even if notifications are suspended via `naughty.suspend`. -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 }) diff --git a/lib/naughty/dbus.lua b/lib/naughty/dbus.lua index 1d89a0d6..89ff9b94 100644 --- a/lib/naughty/dbus.lua +++ b/lib/naughty/dbus.lua @@ -27,6 +27,7 @@ local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility local naughty = require("naughty.core") local cst = require("naughty.constants") local nnotif = require("naughty.notification") +local naction = require("naughty.action") --- Notification library, dbus bindings local dbus = { config = {} } @@ -158,10 +159,17 @@ capi.dbus.connect_signal("org.freedesktop.Notifications", notification:destroy(cst.notification_closed_reason.dismissed_by_user) end elseif action_id ~= nil and action_text ~= nil then - args.actions[action_text] = function() + local a = naction { + name = action_text, + position = action_id, + } + + a:connect_signal("invoked", function() sendActionInvoked(notification.id, action_id) notification:destroy(cst.notification_closed_reason.dismissed_by_user) - end + end) + + table.insert(args.actions, a) end end end diff --git a/lib/naughty/layout/legacy.lua b/lib/naughty/layout/legacy.lua index 796a32b7..c46c96eb 100644 --- a/lib/naughty/layout/legacy.lua +++ b/lib/naughty/layout/legacy.lua @@ -404,23 +404,25 @@ function naughty.default_notification_handler(notification, args) local actions_max_width = 0 local actions_total_height = 0 if actions then - for action, callback in pairs(actions) do + for _, action in ipairs(actions) do + assert(type(action) == "table") + assert(action.name ~= nil) local actiontextbox = wibox.widget.textbox() local actionmarginbox = wibox.container.margin() actionmarginbox:set_margins(margin) actionmarginbox:set_widget(actiontextbox) actiontextbox:set_valign("middle") actiontextbox:set_font(font) - actiontextbox:set_markup(string.format('☛ %s', action)) + actiontextbox:set_markup(string.format('☛ %s', action.name)) -- calculate the height and width local w, h = actiontextbox:get_preferred_size(s) local action_height = h + 2 * margin local action_width = w + 2 * margin actionmarginbox:buttons(gtable.join( - button({ }, 1, callback), - button({ }, 3, callback) - )) + button({ }, 1, function() action:trigger() end), + button({ }, 3, function() action:trigger() end) + )) actionslayout:add(actionmarginbox) actions_total_height = actions_total_height + action_height diff --git a/lib/naughty/notification.lua b/lib/naughty/notification.lua index f85cf621..402e4257 100644 --- a/lib/naughty/notification.lua +++ b/lib/naughty/notification.lua @@ -325,6 +325,43 @@ for _, prop in ipairs(properties) do end +--TODO v6: remove this +local function convert_actions(actions) + gdebug.deprecate( + "The notification actions should now be of type `naughty.action`, ".. + "not strings or callback functions", + {deprecated_in=5} + ) + + local naction = require("naughty.action") + + -- Does not attempt to handle when there is a mix of strings and objects + for idx, name in pairs(actions) do + local cb = nil + + if type(name) == "function" then + cb = name + end + + if type(idx) == "string" then + name, idx = idx, nil + end + + local a = naction { + position = idx, + name = name, + } + + if cb then + a:connect_signal("invoked", cb) + end + + -- Yes, it modifies `args`, this is legacy code, cloning the args + -- just for this isn't worth it. + actions[idx] = a + end +end + --- Create a notification. -- -- @tab args The argument table containing any of the arguments below. @@ -364,8 +401,7 @@ end -- @tparam[opt] func args.callback Function that will be called with all arguments. -- The notification will only be displayed if the function returns true. -- Note: this function is only relevant to notifications sent via dbus. --- @tparam[opt] table args.actions Mapping that maps a string to a callback when this --- action is selected. +-- @tparam[opt] table args.actions A list of `naughty.action`s. -- @bool[opt=false] args.ignore_suspend If set to true this notification -- will be shown even if notifications are suspended via `naughty.suspend`. -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 }) @@ -378,6 +414,16 @@ local function create(args) if not args then return end end + args = args or {} + + -- Old actions usually have callbacks and names. But this isn't non + -- compliant with the spec. The spec has explicit ordering and optional + -- icons. The old format doesn't allow these metadata to be stored. + local is_old_action = args.actions and ( + (args.actions[1] and type(args.actions[1]) == "string") or + (type(next(args.actions)) == "string") + ) + local n = gobject { enable_properties = true, } @@ -396,6 +442,10 @@ local function create(args) rawget(n, "preset") or {} )) + if is_old_action then + convert_actions(args.actions) + end + for k, v in pairs(n.preset) do private[k] = v end