notification.action: Allow actions to be shared by multiple notification

The reason is that if actions are provided by rules, only one instance
exist. It was a mistake to couple actions with their notifications. It
could not work reliably and has to be removed.

The commit also change the notification action storage to be a copy
instead of the original table. This allows to append actions (not part
of this commit) without risking adding them to the wrong notification.

**WARNING** This break an unreleased API by removing the `notification`
property of an action.
This commit is contained in:
Emmanuel Lepage Vallee 2019-07-17 00:26:44 -04:00
parent f8cbb54913
commit e524f93baa
4 changed files with 81 additions and 54 deletions

View File

@ -2,7 +2,8 @@
--- A notification action. --- A notification action.
-- --
-- A notification can have multiple actions to chose from. This module allows -- A notification can have multiple actions to chose from. This module allows
-- to manage such actions. -- to manage such actions. An action object can be shared by multiple
-- notifications.
-- --
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> -- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
-- @copyright 2019 Emmanuel Lepage Vallee -- @copyright 2019 Emmanuel Lepage Vallee
@ -29,8 +30,8 @@ local action = {}
-- If the action is selected. -- If the action is selected.
-- --
-- Only a single action can be selected per notification. It will be applied -- Only a single action can be selected per notification. This is useful to
-- when `my_notification:apply()` is called. -- implement keyboard navigation.
-- --
-- @property selected -- @property selected
-- @param boolean -- @param boolean
@ -50,10 +51,6 @@ local action = {}
-- @property icon_only -- @property icon_only
-- @param[opt=false] boolean -- @param[opt=false] boolean
--- The notification.
-- @property notification
-- @tparam naughty.notification notification
--- When a notification is invoked. --- When a notification is invoked.
-- @signal invoked -- @signal invoked
@ -64,10 +61,7 @@ end
function action:set_selected(value) function action:set_selected(value)
self._private.selected = value self._private.selected = value
self:emit_signal("property::selected", value) self:emit_signal("property::selected", value)
self:emit_signal("_changed")
if self._private.notification then
self._private.notification:emit_signal("property::actions")
end
--TODO deselect other actions from the same notification --TODO deselect other actions from the same notification
end end
@ -79,15 +73,12 @@ end
function action:set_position(value) function action:set_position(value)
self._private.position = value self._private.position = value
self:emit_signal("property::position", value) self:emit_signal("property::position", value)
self:emit_signal("_changed")
if self._private.notification then
self._private.notification:emit_signal("property::actions")
end
--TODO make sure the position is unique --TODO make sure the position is unique
end end
for _, prop in ipairs { "name", "icon", "notification", "icon_only" } do for _, prop in ipairs { "name", "icon", "icon_only" } do
action["get_"..prop] = function(self) action["get_"..prop] = function(self)
return self._private[prop] return self._private[prop]
end end
@ -95,22 +86,18 @@ for _, prop in ipairs { "name", "icon", "notification", "icon_only" } do
action["set_"..prop] = function(self, value) action["set_"..prop] = function(self, value)
self._private[prop] = value self._private[prop] = value
self:emit_signal("property::"..prop, value) self:emit_signal("property::"..prop, value)
self:emit_signal("_changed")
-- Make sure widgets with as an actionlist is updated.
if self._private.notification then
self._private.notification:emit_signal("property::actions")
end
end end
end end
local set_notif = action.set_notification --TODO v4.5, remove this.
function action.set_notification()
function action.set_notification(self, value) -- It didn't work because it prevented actions defined in the rules to be
local old = self._private.notification -- in multiple notifications at once.
set_notif(self, value) assert(
if old then false,
old:emit_signal("property::actions") "Setting a notification object was a bad idea and is now forbidden"
end )
end end
--- Execute this action. --- Execute this action.
@ -118,11 +105,12 @@ end
-- This only emits the `invoked` signal. -- This only emits the `invoked` signal.
-- --
-- @method invoke -- @method invoke
function action:invoke() -- @tparam[opt={}] naughty.notification notif A notification object on which
assert(self._private.notification, -- the action was invoked. If a notification is shared by many object (like
"Cannot invoke an action without a notification") -- a "mute" or "snooze" action added to all notification), calling `:invoke()`
-- without adding the `notif` context will cause unexpected results.
self:emit_signal("invoked") function action:invoke(notif)
self:emit_signal("invoked", notif)
end end
local function new(_, args) local function new(_, args)

View File

@ -430,12 +430,10 @@ function naughty.default_notification_handler(notification, args)
actionmarginbox:buttons(gtable.join( actionmarginbox:buttons(gtable.join(
button({ }, 1, function() button({ }, 1, function()
action:invoke() action:invoke(notification)
notification:destroy()
end), end),
button({ }, 3, function() button({ }, 3, function()
action:invoke() action:invoke(notification)
notification:destroy()
end) end)
)) ))
actionslayout:add(actionmarginbox) actionslayout:add(actionmarginbox)

View File

@ -117,10 +117,6 @@ local module = {}
-- @tparam gears.surface|string action_bgimage_selected -- @tparam gears.surface|string action_bgimage_selected
-- @see gears.surface -- @see gears.surface
local default_buttons = gtable.join(
abutton({ }, 1, function(a) a:invoke() end)
)
local props = {"shape_border_color", "bg_image" , "fg", local props = {"shape_border_color", "bg_image" , "fg",
"shape_border_width", "underline", "bg", "shape_border_width", "underline", "bg",
"shape", "icon_size", } "shape", "icon_size", }
@ -176,7 +172,7 @@ local function update(self)
awcommon.list_update( awcommon.list_update(
self._private.layout, self._private.layout,
default_buttons, self._private.default_buttons,
function(o) return wb_label(o, self) end, function(o) return wb_label(o, self) end,
self._private.data, self._private.data,
self._private.notification.actions, self._private.notification.actions,
@ -273,7 +269,7 @@ end
--- Create an action list. --- Create an action list.
-- --
-- @tparam table args -- @tparam table args
-- @tparam naughty.notification args.notification The notification/ -- @tparam naughty.notification args.notification The notification.
-- @tparam widget args.base_layout The action layout. -- @tparam widget args.base_layout The action layout.
-- @tparam table args.style Override the beautiful values. -- @tparam table args.style Override the beautiful values.
-- @tparam boolean args.style.underline_normal -- @tparam boolean args.style.underline_normal
@ -312,6 +308,10 @@ local function new(_, args)
update_style(wdg) update_style(wdg)
wdg._private.default_buttons = gtable.join(
abutton({ }, 1, function(a) a:invoke(args.notification) end)
)
return wdg return wdg
end end

View File

@ -641,6 +641,34 @@ function notification.get_clients(self)
return self._private.clients return self._private.clients
end end
function notification.set_actions(self, new_actions)
for _, a in ipairs(self._private.actions or {}) do
a:disconnect_signal("_changed", self._private.action_cb )
a:disconnect_signal("invoked" , self._private.invoked_cb)
end
-- Clone so `append_actions` doesn't add unwanted actions to other
-- notifications.
self._private.actions = gtable.clone(new_actions, false)
for _, a in ipairs(self._private.actions or {}) do
a:connect_signal("_changed", self._private.action_cb )
a:connect_signal("invoked" , self._private.invoked_cb)
end
self:emit_signal("property::actions", new_actions)
-- When a notification is updated over dbus or by setting a property,
-- it is usually convenient to reset the timeout.
local reset = ((not self.suspended)
and self.auto_reset_timeout ~= false
and naughty.auto_reset_timeout)
if reset then
self:reset_timeout()
end
end
--TODO v6: remove this --TODO v6: remove this
local function convert_actions(actions) local function convert_actions(actions)
gdebug.deprecate( gdebug.deprecate(
@ -808,21 +836,34 @@ local function create(args)
private[k] = v private[k] = v
end end
-- notif.actions should not be nil to allow cheching if there is actions
-- using the shorthand `if #notif.actions > 0 then`
private.actions = private.actions or {}
-- Make sure the action are for this notification. Sharing actions with
-- multiple notification is not supported.
for _, a in ipairs(private.actions) do
a.notification = n
end
-- It's an automatic property -- It's an automatic property
n.is_expired = false n.is_expired = false
gtable.crush(n, notification, true) gtable.crush(n, notification, true)
-- Always emit property::actions when any of the action change to allow
-- some widgets to be updated without over complicated built-in tracking
-- of all options.
function n._private.action_cb() n:emit_signal("property::actions") end
-- Listen to action press and destroy non-resident notifications.
function n._private.invoked_cb(a, notif)
if (not notif) or notif == n then
n:emit_signal("invoked", a)
if not n.resident then
n:destroy(cst.notification_closed_reason.dismissed_by_user)
end
end
end
-- notif.actions should not be nil to allow checking if there is actions
-- using the shorthand `if #notif.actions > 0 then`
private.actions = {}
if args.actions then
notification.set_actions(n, args.actions)
end
n.id = n.id or notification._gen_next_id() n.id = n.id or notification._gen_next_id()
-- Register the notification before requesting a widget -- Register the notification before requesting a widget