naughty: Bump the SPEC version compliance to v1.2.

* action icons
 * persistence
 * residence
 * categories
 * animated icons
 * more ways to get icons

In addition, the commit also tries its best to attach notifications to
objects using various dubious semi compliant hints or the DBus PID. It
works often enough to be useful.
This commit is contained in:
Emmanuel Lepage Vallee 2019-07-14 00:03:12 -04:00
parent 620241e056
commit e076bc664e
4 changed files with 474 additions and 35 deletions

View File

@ -134,10 +134,36 @@ gtable.crush(naughty, require("naughty.constants"))
-- @property auto_reset_timeout
-- @tparam[opt=true] boolean auto_reset_timeout
--- Enable or disable naughty ability to claim to support animations.
--
-- When this is true, applications which query `naughty` feature support
-- will see that animations are supported. Note that there is *very little*
-- support for this and enabled it will cause bugs.
--
-- @property image_animations_enabled
-- @param[opt=false] boolean
--- Enable or disable the persistent notifications.
--
-- This is very annoying when using `naughty.layout.box` popups, but tolerable
-- when using `naughty.list.notifications`.
--
-- Note that enabling this **does nothing** in `naughty` itself. The timeouts
-- are still honored and notifications still destroyed. It is the user
-- responsibility to disable the dismiss timer. However, this tells the
-- applications that notification persistence is supported so they might
-- stop using systray icons for the sake of displaying or other changes like
-- that.
--
-- @property persistence_enabled
-- @param[opt=false] boolean
local properties = {
suspended = false,
expiration_paused = false,
auto_reset_timeout= true,
suspended = false,
expiration_paused = false,
auto_reset_timeout = true,
image_animations_enabled = false,
persistence_enabled = false,
}
--TODO v5 Deprecate the public `naughty.notifications` (to make it private)
@ -514,6 +540,18 @@ end
-- including, but not limited to, all `naughty.notification` properties.
-- @signal request::preset
--- Emitted when an action requires an icon it doesn't know.
--
-- The implementation should look in the icon theme for an action icon or
-- provide something natively.
--
-- If an icon is found, the handler must set the `icon` property on the `action`
-- object to a path or a `gears.surface`.
--
-- @signal request::icon
-- @tparam naughty.action action The action.
-- @tparam string icon_name The icon name.
-- Register a new notification object.
local function register(notification, args)
-- Add the some more properties
@ -564,6 +602,8 @@ local function set_index_miss(_, key, value)
if not value then
resume()
end
naughty.emit_signal("property::"..key)
else
rawset(naughty, key, value)
end

View File

@ -14,6 +14,7 @@ local string = string
local capi = { awesome = awesome }
local gtable = require("gears.table")
local gsurface = require("gears.surface")
local gdebug = require("gears.debug")
local protected_call = require("gears.protected_call")
local lgi = require("lgi")
local cairo, Gio, GLib, GObject = lgi.cairo, lgi.Gio, lgi.GLib, lgi.GObject
@ -28,6 +29,10 @@ local cst = require("naughty.constants")
local nnotif = require("naughty.notification")
local naction = require("naughty.action")
local capabilities = {
"body", "body-markup", "icon-static", "actions", "action-icons"
}
--- Notification library, dbus bindings
local dbus = { config = {} }
@ -37,8 +42,8 @@ local bus_connection
-- DBUS Notification constants
-- https://developer.gnome.org/notification-spec/#urgency-levels
local urgency = {
low = "\0",
normal = "\1",
low = "\0",
normal = "\1",
critical = "\2"
}
@ -123,7 +128,7 @@ end
local notif_methods = {}
function notif_methods.Notify(sender, object_path, interface, method, parameters, invocation)
local appname, replaces_id, icon, title, text, actions, hints, expire =
local appname, replaces_id, app_icon, title, text, actions, hints, expire =
unpack(parameters.value)
local args = {}
@ -167,14 +172,26 @@ function notif_methods.Notify(sender, object_path, interface, method, parameters
notification:destroy(cst.notification_closed_reason.dismissed_by_user)
end
elseif action_id ~= nil and action_text ~= nil then
local a = naction {
name = action_text,
position = action_id,
id = action_id,
position = (i - 1)/2 + 1,
}
-- Right now `gears` doesn't have a great icon implementation
-- and `naughty` doesn't depend on `menubar`, so delegate the
-- icon "somewhere" using a request.
if hints["action-icons"] and action_id ~= "" then
naughty.emit_signal("request::icon", a, action_id)
end
a:connect_signal("invoked", function()
sendActionInvoked(notification.id, action_id)
notification:destroy(cst.notification_closed_reason.dismissed_by_user)
if not notification.resident then
notification:destroy(cst.notification_closed_reason.dismissed_by_user)
end
end)
table.insert(args.actions, a)
@ -189,22 +206,34 @@ function notif_methods.Notify(sender, object_path, interface, method, parameters
member = method, sender = sender, bus = "session"
}
if not preset.callback or (type(preset.callback) == "function" and
preset.callback(legacy_data, appname, replaces_id, icon, title, text, actions, hints, expire)) then
if icon ~= "" then
args.icon = icon
elseif hints.icon_data or hints.image_data then
preset.callback(legacy_data, appname, replaces_id, app_icon, title, text, actions, hints, expire)) then
if app_icon ~= "" then
args.app_icon = app_icon
end
if hints.icon_data or hints.image_data or hints["image-data"] then
-- Icon data is a bit complex and hence needs special care:
-- .value breaks with the array of bytes (ay) that we get here.
-- So, bypass it and look up the needed value differently
local icon_data
local icon_condidates = {}
for k, v in parameters:get_child_value(7 - 1):pairs() do
if k == "icon_data" then
icon_data = v
elseif k == "image_data" and icon_data == nil then
icon_data = v
if k == "image-data" then
icon_condidates[1] = v -- not deprecated
break
elseif k == "image_data" then -- deprecated
icon_condidates[2] = v
elseif k == "icon_data" then -- deprecated
icon_condidates[3] = v
end
end
-- The order is mandated by the spec.
local icon_data = icon_condidates[1]
or icon_condidates[2]
or icon_condidates[3]
-- icon_data is an array:
-- 1 -> width
-- 2 -> height
@ -218,20 +247,87 @@ function notif_methods.Notify(sender, object_path, interface, method, parameters
-- GVariant.data to get that as an LGI byte buffer. That one can
-- then by converted to a string via its __tostring metamethod.
local data = tostring(icon_data:get_child_value(7 - 1).data)
args.icon = convert_icon(icon_data[1], icon_data[2], icon_data[3], icon_data[6], data)
args.image = convert_icon(icon_data[1], icon_data[2], icon_data[3], icon_data[6], data)
-- Convert all animation frames.
if naughty.image_animations_enabled then
args.images = {args.image}
if #icon_data > 7 then
for frame=8, #icon_data do
data = tostring(icon_data:get_child_value(frame-1).data)
table.insert(
args.images,
convert_icon(
icon_data[1],
icon_data[2],
icon_data[3],
icon_data[6],
data
)
)
end
end
end
end
-- Alternate ways to set the icon. The specs recommends to allow both
-- the icon and image to co-exist since they serve different purpose.
-- However in case the icon isn't specified, use the image.
args.image = args.image
or hints["image-path"] -- not deprecated
or hints["image_path"] -- deprecated
if naughty.image_animations_enabled then
args.images = args.images or {}
end
if replaces_id and replaces_id ~= "" and replaces_id ~= 0 then
args.replaces_id = replaces_id
end
if expire and expire > -1 then
args.timeout = expire / 1000
end
args.freedesktop_hints = hints
-- Not very pretty, but given the current format is documented in the
-- public API... well, whatever...
if hints and hints.urgency then
for name, key in pairs(urgency) do
local b = string.char(hints.urgency)
if key == b then
args.urgency = name
end
end
end
args.urgency = args.urgency or "normal"
-- Try to update existing objects when possible
notification = naughty.get_by_id(replaces_id)
if notification then
if not notification._private._unique_sender then
-- If this happens, the notification is either trying to
-- highjack content created within AwesomeWM or it is garbage
-- to begin with.
gdebug.print_warning(
"A notification has been received, but tried to update "..
"the content of a notification it does not own."
)
elseif notification._private._unique_sender ~= sender then
-- Nothing says you cannot and some scripts may do it
-- accidentally, but this is rather unexpected.
gdebug.print_warning(
"Notification "..notification.title.." is being updated"..
"by a different DBus connection ("..sender.."), this is "..
"suspicious. The original connection was "..
notification._private._unique_sender
)
end
for k, v in pairs(args) do
if k == "destroy" then k = "destroy_cb" end
notification[k] = v
@ -240,6 +336,9 @@ function notif_methods.Notify(sender, object_path, interface, method, parameters
-- Even if no property changed, restart the timeout.
notification:reset_timeout()
else
-- Only set the sender for new notifications.
args._unique_sender = sender
notification = nnotif(args)
end
@ -261,16 +360,14 @@ end
function notif_methods.GetServerInformation(_, _, _, _, _, invocation)
-- name of notification app, name of vender, version, specification version
invocation:return_value(GLib.Variant("(ssss)", {
"naughty", "awesome", capi.awesome.version, "1.0"
"naughty", "awesome", capi.awesome.version, "1.2"
}))
end
function notif_methods.GetCapabilities(_, _, _, _, _, invocation)
-- We actually do display the body of the message, we support <b>, <i>
-- and <u> in the body and we handle static (non-animated) icons.
invocation:return_value(GLib.Variant("(as)", {
{ "body", "body-markup", "icon-static", "actions" }
}))
invocation:return_value(GLib.Variant("(as)", {capabilities}))
end
local function method_call(_, sender, object_path, interface, method, parameters, invocation)
@ -330,6 +427,101 @@ local function on_bus_acquire(conn, _)
GObject.Closure(method_call))
end
local bus_proxy, pid_for_unique_name = nil, {}
Gio.DBusProxy.new_for_bus(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
nil,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
nil,
function(proxy)
bus_proxy = proxy
end,
nil
)
--- Get the clients associated with a notification.
--
-- Note that is based on the process PID. PIDs roll over, so don't use this
-- with very old notifications.
--
-- Also note that some multi-process application can use a different process
-- for the clients and the service used to send the notifications.
--
-- Since this is based on PIDs, it is impossible to know which client sent the
-- notification if the process has multiple clients (windows). Using the
-- `client.type` can be used to further filter this list into more probable
-- candidates (tooltips, menus and dialogs are unlikely to send notifications).
--
-- @tparam naughty.notification notif A notification object.
-- @treturn table A table with all associated clients.
function dbus.get_clients(notif)
-- First, the trivial case, but I never found an app that implements it.
-- It isn't standardized, but mentioned as possible.
local win_id = notif.freedesktop_hints and (notif.freedesktop_hints.window_ID
or notif.freedesktop_hints["window-id"]
or notif.freedesktop_hints.windowID
or notif.freedesktop_hints.windowid)
if win_id then
for _, c in ipairs(client.get()) do
if c.window_id == win_id then
return {win_id}
end
end
end
-- Less trivial, but mentioned in the spec. Note that this isn't
-- recommended by the spec, let alone mandatory. It is mentioned it can
-- exist. This wont work with Flatpak or Snaps.
local pid = notif.freedesktop_hints and (
notif.freedesktop_hints.PID or notif.freedesktop_hints.pid
)
if ((not bus_proxy) or not notif._private._unique_sender) and not pid then
return {}
end
if (not pid) and (not pid_for_unique_name[notif._private._unique_sender]) then
local owner = GLib.Variant("(s)", {notif._private._unique_sender})
-- It is sync, but this isn't done very often and since it is DBus
-- daemon itself, it is very responsive. Doing this using the async
-- variant would cause the clients to be unavailable in the notification
-- rules.
pid = bus_proxy:call_sync("GetConnectionUnixProcessID",
owner,
Gio.DBusCallFlags.NONE,
-1
)
if (not pid) or (not pid.value) then return {} end
pid = pid.value and pid.value[1]
if not pid then return {} end
pid_for_unique_name[notif._private._unique_sender] = pid
end
pid = pid or pid_for_unique_name[notif._private._unique_sender]
if not pid then return {} end
local ret = {}
for _, c in ipairs(client.get()) do
if c.pid == pid then
table.insert(ret, c)
end
end
return ret
end
local function on_name_acquired(conn, _)
bus_connection = conn
end
@ -345,6 +537,35 @@ Gio.bus_own_name(Gio.BusType.SESSION, "org.freedesktop.Notifications",
-- For testing
dbus._notif_methods = notif_methods
local function remove_capability(cap)
for k, v in ipairs(capabilities) do
if v == cap then
table.remove(capabilities, k)
break
end
end
end
-- Update the capabilities.
naughty.connect_signal("property::persistence_enabled", function()
remove_capability("persistence")
if naughty.persistence_enabled then
table.insert(capabilities, "persistence")
end
end)
naughty.connect_signal("property::image_animations_enabled", function()
remove_capability("icon-multi")
remove_capability("icon-static")
table.insert(capabilities, naughty.persistence_enabled
and "icon-multi" or "icon-static"
)
end)
-- For the tests.
dbus._capabilities = capabilities
return dbus
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -15,12 +15,13 @@
-- @copyright 2017 Emmanuel Lepage Vallee
-- @coreclassmod naughty.notification
---------------------------------------------------------------------------
local gobject = require("gears.object")
local gtable = require("gears.table")
local timer = require("gears.timer")
local cst = require("naughty.constants")
local naughty = require("naughty.core")
local gdebug = require("gears.debug")
local gobject = require("gears.object")
local gtable = require("gears.table")
local gsurface = require("gears.surface")
local timer = require("gears.timer")
local cst = require("naughty.constants")
local naughty = require("naughty.core")
local gdebug = require("gears.debug")
local notification = {}
@ -95,6 +96,72 @@ local notification = {}
-- @property timeout
-- @param number
--- The notification urgency level.
--
-- The default urgency levels are:
--
-- * low
-- * normal
-- * critical
--
-- @property urgency
-- @param string
--- The notification category.
--
-- The category should be named using the `x-vendor.class.name` naming scheme or
-- use one of the default categories:
--
-- <table class='widget_list' border=1>
-- <tr style='font-weight: bold;'>
-- <th align='center'>Name</th>
-- <th align='center'>Description</th>
-- </tr>
-- <tr><td><b>device</b></td><td>A generic device-related notification that
-- doesn't fit into any other category.</td></tr>
-- <tr><td><b>device.added</b></td><td>A device, such as a USB device, was added to the system.</td></tr>
-- <tr><td><b>device.error</b></td><td>A device had some kind of error.</td></tr>
-- <tr><td><b>device.removed</b></td><td>A device, such as a USB device, was removed from the system.</td></tr>
-- <tr><td><b>email</b></td><td>A generic e-mail-related notification that doesn't fit into
-- any other category.</td></tr>
-- <tr><td><b>email.arrived</b></td><td>A new e-mail notification.</td></tr>
-- <tr><td><b>email.bounced</b></td><td>A notification stating that an e-mail has bounced.</td></tr>
-- <tr><td><b>im</b></td><td>A generic instant message-related notification that doesn't fit into
-- any other category.</td></tr>
-- <tr><td><b>im.error</b></td><td>An instant message error notification.</td></tr>
-- <tr><td><b>im.received</b></td><td>A received instant message notification.</td></tr>
-- <tr><td><b>network</b></td><td>A generic network notification that doesn't fit into any other
-- category.</td></tr>
-- <tr><td><b>network.connected</b></td><td>A network connection notification, such as successful
-- sign-on to a network service. <br />
-- This should not be confused with device.added for new network devices.</td></tr>
-- <tr><td><b>network.disconnected</b></td><td>A network disconnected notification. This should not
-- be confused with <br />
-- device.removed for disconnected network devices.</td></tr>
-- <tr><td><b>network.error</b></td><td>A network-related or connection-related error.</td></tr>
-- <tr><td><b>presence</b></td><td>A generic presence change notification that doesn't fit into any
-- other category, <br />
-- such as going away or idle.</td></tr>
-- <tr><td><b>presence.offline</b></td><td>An offline presence change notification.</td></tr>
-- <tr><td><b>presence.online</b></td><td>An online presence change notification.</td></tr>
-- <tr><td><b>transfer</b></td><td>A generic file transfer or download notification that doesn't
-- fit into any other category.</td></tr>
-- <tr><td><b>transfer.complete</b></td><td>A file transfer or download complete notification.</td></tr>
-- <tr><td><b>transfer.error</b></td><td>A file transfer or download error.</td></tr>
-- </table>
--
-- @property category
-- @tparam string|nil category
--- True if the notification should be kept when an action is pressed.
--
-- By default, invoking an action will destroy the notification. Some actions,
-- like the "Snooze" action of alarm clock, will cause the notification to
-- be updated with a date further in the future.
--
-- @property resident
-- @param[opt=false] boolean
--- Delay in seconds after which hovered popup disappears.
-- @property hover_timeout
-- @param number
@ -143,14 +210,59 @@ local notification = {}
-- @property font
-- @param string
--- Path to icon.
--- "All in one" way to access the default image or icon.
--
-- A notification can provide a combination of an icon, a static image, or if
-- enabled, a looping animation. Add to that the ability to import the icon
-- information from the client or from a `.desktop` file, there is multiple
-- conflicting sources of "icons".
--
-- On the other hand, the vast majority of notifications don't multiple or
-- ambiguous sources of icons. This property will pick the first of the
-- following.
--
-- * The `image`.
-- * The `app_icon`.
-- * The `icon` from a client with `normal` type.
-- * The `icon` of a client with `dialog` type.
--
-- @property icon
-- @tparam string|surface icon
-- @see app_icon
-- @see image
--- Desired icon size in px.
-- @property icon_size
-- @param number
--- The icon provided in the `app_icon` field of the DBus notification.
--
-- This should always be either the URI (path) to an icon or a valid XDG
-- icon name to be fetched from the theme.
--
-- @property app_icon
-- @param string
--- The notification image.
--
-- This is usually provided as a `gears.surface` object. The image is used
-- instead of the `app_icon` by notification assets which are auto-generated
-- or stored elsewhere than the filesystem (databases, web, Android phones, etc).
--
-- @property image
-- @tparam string|surface image
--- The notification (animated) images.
--
-- Note that calling this without first setting
-- `naughty.image_animations_enabled` to true will throw an exception.
--
-- Also note that there is *zero* support for this anywhere else in `naughty`
-- and very, very few applications support this.
--
-- @property images
-- @tparam nil|table images
--- Foreground color.
--
--@DOC_awful_notification_fg_EXAMPLE@
@ -284,11 +396,20 @@ local notification = {}
-- @property ignore_suspend If set to true this notification
-- will be shown even if notifications are suspended via `naughty.suspend`.
--- A list of clients associated with this notification.
--
-- When used with DBus notifications, this returns all clients sharing the PID
-- of the notification sender. Note that this is highly unreliable.
-- Applications that use a different process to send the notification or
-- applications (and scripts) calling the `notify-send` command wont have any
-- client.
--
-- @property clients
-- @param table
--FIXME remove the screen attribute, let the handlers decide
-- document all handler extra properties
--FIXME add methods such as persist
--- Destroy notification by notification object.
--
-- @method destroy
@ -398,7 +519,8 @@ local properties = {
"fg" , "bg" , "height" , "border_color" ,
"shape" , "opacity" , "margin" , "ignore_suspend",
"destroy" , "preset" , "callback", "actions" ,
"run" , "id" , "ignore" , "auto_reset_timeout"
"run" , "id" , "ignore" , "auto_reset_timeout",
"urgency" , "image" , "images" ,
}
for _, prop in ipairs(properties) do
@ -425,12 +547,68 @@ for _, prop in ipairs(properties) do
if reset then
self:reset_timeout()
end
return
end
end
local hints_default = {
urgency = "normal",
resident = false,
}
for _, prop in ipairs { "category", "resident" } do
notification["get_"..prop] = notification["get_"..prop] or function(self)
return self._private[prop] or (
self._private.freedesktop_hints and self._private.freedesktop_hints[prop]
) or hints_default[prop]
end
notification["set_"..prop] = notification["set_"..prop] or function(self, value)
self._private[prop] = value
self:emit_signal("property::"..prop, value)
end
end
function notification.get_icon(self)
if self._private.icon then
return self._private.icon == "" and nil or self._private.icon
elseif self.image and self.image ~= "" then
return self.image
elseif self._private.app_icon and self._private.app_icon ~= "" then
return self._private.app_icon
end
local clients = notification.get_clients(self)
for _, c in ipairs(clients) do
if c.type == "normal" then
self._private.icon = gsurface(c.icon)
return self._private.icon
end
end
for _, c in ipairs(clients) do
if c.type == "dialog" then
self._private.icon = gsurface(c.icon)
return self._private.icon
end
end
return nil
end
function notification.get_clients(self)
-- Clients from the future don't send notification, it's useless to reload
-- the list over and over.
if self._private.clients then return self._private.clients end
if not self._private._unique_sender then return {} end
self._private.clients = require("naughty.dbus").get_clients(self)
return self._private.clients
end
--TODO v6: remove this
local function convert_actions(actions)
gdebug.deprecate(

View File

@ -250,7 +250,7 @@ for f in $tests; do
error="$(echo "$error" | grep -vE ".{19} W: awesome: (Can't read color .* from GTK)" || true)"
if [[ $fail_on_warning ]]; then
# Filter out ignored warnings.
error="$(echo "$error" | grep -vE ".{19} W: awesome: (a_glib_poll|Cannot reliably detect EOF|beautiful: can't get colorscheme from xrdb|Can't read color .* from GTK+3 theme)" || true)"
error="$(echo "$error" | grep -vE ".{19} W: awesome: (a_glib_poll|Cannot reliably detect EOF|beautiful: can't get colorscheme from xrdb|Can't read color .* from GTK+3 theme|A notification|Notification)" || true)"
fi
if [[ -n "$error" ]]; then
color_red