naughty: Turn actions into object.

The current API is non-compliant with the 1.0 spec and cannot represent
the v1.2 spec at all. The pair of name and callback fails to represent
the explicit ordering and cannot support the icons cleanly.

Plus to support the keyboard navigation use case, the notification
action need to be able to get some sort of focus state. Having an
object makes this easy.
This commit is contained in:
Emmanuel Lepage Vallee 2019-01-04 02:52:02 -05:00
parent e70822a6a4
commit 6d5d016a2a
5 changed files with 197 additions and 11 deletions

127
lib/naughty/action.lua Normal file
View File

@ -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})

View File

@ -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 })

View File

@ -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

View File

@ -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('☛ <u>%s</u>', action))
actiontextbox:set_markup(string.format('☛ <u>%s</u>', 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

View File

@ -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