Merge pull request #2825 from Elv13/yet_more_notif_fixes

Support the notification spec v1.2
This commit is contained in:
Emmanuel Lepage Vallée 2019-08-10 12:47:09 -07:00 committed by GitHub
commit ed0918385c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1105 additions and 228 deletions

View File

@ -20,34 +20,15 @@ local hotkeys_popup = require("awful.hotkeys_popup")
require("awful.hotkeys_popup.keys")
-- {{{ Error handling
-- @DOC_ERROR_HANDLING@
-- Check if awesome encountered an error during startup and fell back to
-- another config (This code will only ever execute for the fallback config)
if awesome.startup_errors then
naughty.connect_signal("request::display_error", function(message, startup)
naughty.notification {
preset = naughty.config.presets.critical,
title = "Oops, there were errors during startup!",
message = awesome.startup_errors
urgency = "critical",
title = "Oops, an error happened"..(startup and " during startup!" or "!"),
message = message
}
end
-- Handle runtime errors after startup
do
local in_error = false
awesome.connect_signal("debug::error", function (err)
-- Make sure we don't go into an endless error loop
if in_error then return end
in_error = true
naughty.notification {
preset = naughty.config.presets.critical,
title = "Oops, an error happened!",
message = tostring(err)
}
in_error = false
end)
end
end)
-- }}}
-- {{{ Variable definitions

View File

@ -134,6 +134,42 @@ function object:emit_signal(name, ...)
end
end
function object._setup_class_signals(t)
local conns = {}
function t.connect_signal(name, func)
assert(name)
conns[name] = conns[name] or {}
table.insert(conns[name], func)
end
--- Emit a notification signal.
-- @tparam string name The signal name.
-- @param ... The signal callback arguments
function t.emit_signal(name, ...)
assert(name)
for _, func in pairs(conns[name] or {}) do
func(...)
end
end
--- Disconnect a signal from a source.
-- @tparam string name The name of the signal
-- @tparam function func The attached function
-- @treturn boolean If the disconnection was successful
function t.disconnect_signal(name, func)
for k, v in ipairs(conns[name] or {}) do
if v == func then
table.remove(conns[name], k)
return true
end
end
return false
end
return conns
end
local function get_miss(self, key)
local class = rawget(self, "_class")

View File

@ -12,11 +12,11 @@ local xpcall = xpcall
local protected_call = {}
local function error_handler(err)
function protected_call._error_handler(err)
gdebug.print_error(traceback("Error during a protected call: " .. tostring(err), 2))
end
local function handle_result(success, ...)
function protected_call._handle_result(success, ...)
if success then
return ...
end
@ -27,13 +27,13 @@ if not select(2, xpcall(function(a) return a end, error, true)) then
-- Lua 5.1 doesn't support arguments in xpcall :-(
do_pcall = function(func, ...)
local args = { ... }
return handle_result(xpcall(function()
return protected_call._handle_result(xpcall(function()
return func(unpack(args))
end, error_handler))
end, protected_call._error_handler))
end
else
do_pcall = function(func, ...)
return handle_result(xpcall(func, error_handler, ...))
return protected_call._handle_result(xpcall(func, protected_call._error_handler, ...))
end
end

View File

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

View File

@ -48,6 +48,21 @@ ret.config.presets = {
},
}
ret.config._urgency = {
low = "\0",
normal = "\1",
critical = "\2"
}
ret.config.mapping = {
{{urgency = ret.config._urgency.low }, ret.config.presets.low}, --compat
{{urgency = ret.config._urgency.normal }, ret.config.presets.normal}, --compat
{{urgency = ret.config._urgency.critical}, ret.config.presets.critical}, --compat
{{urgency = "low" }, ret.config.presets.low},
{{urgency = "normal" }, ret.config.presets.normal},
{{urgency = "critical"}, ret.config.presets.critical},
}
ret.config.defaults = {
timeout = 5,
text = "",
@ -55,7 +70,12 @@ ret.config.defaults = {
ontop = true,
margin = dpi(5),
border_width = dpi(1),
position = "top_right"
position = "top_right",
urgency = "normal",
message = "",
title = "",
app_name = "",
ignore = false,
}
ret.notification_closed_reason = {
@ -65,7 +85,7 @@ ret.notification_closed_reason = {
dismissedByUser = 2, --TODO v5 remove this undocumented legacy constant
dismissed_by_user = 2,
dismissedByCommand = 3, --TODO v5 remove this undocumented legacy constant
dismissed_by_vommand = 3,
dismissed_by_command = 3,
undefined = 4
}

View File

@ -14,9 +14,10 @@
-- Package environment
local capi = { screen = screen }
local gdebug = require("gears.debug")
local screen = require("awful.screen")
local gtable = require("gears.table")
local gdebug = require("gears.debug")
local screen = require("awful.screen")
local gtable = require("gears.table")
local gobject = require("gears.object")
local naughty = {}
@ -134,10 +135,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)
@ -197,7 +224,9 @@ local function update_index(n)
remove_from_index(n)
-- Add to the index again
local s = get_screen(n.screen or n.preset.screen or screen.focused())
local s = get_screen(n.screen
or (n.preset and n.preset.screen)
or screen.focused())
naughty.notifications[s] = naughty.notifications[s] or {}
table.insert(naughty.notifications[s][n.position], n)
end
@ -223,7 +252,7 @@ function naughty.suspend()
properties.suspended = true
end
local conns = {}
local conns = gobject._setup_class_signals(naughty)
--- Connect a global signal on the notification engine.
--
@ -235,41 +264,21 @@ local conns = {}
--
-- @tparam string name The name of the signal
-- @tparam function func The function to attach
-- @staticfct naughty.connect_signal
-- @usage naughty.connect_signal("added", function(notif)
-- -- do something
-- end)
-- @staticfct naughty.connect_signal
function naughty.connect_signal(name, func)
assert(name)
conns[name] = conns[name] or {}
table.insert(conns[name], func)
end
--- Emit a notification signal.
-- @tparam string name The signal name.
-- @param ... The signal callback arguments
-- @staticfct naughty.emit_signal
function naughty.emit_signal(name, ...)
assert(name)
for _, func in pairs(conns[name] or {}) do
func(...)
end
end
--- Disconnect a signal from a source.
-- @tparam string name The name of the signal
-- @tparam function func The attached function
-- @treturn boolean If the disconnection was successful
-- @staticfct naughty.disconnect_signal
function naughty.disconnect_signal(name, func)
for k, v in ipairs(conns[name] or {}) do
if v == func then
table.remove(conns[name], k)
return true
end
end
return false
end
-- @treturn boolean If the disconnection was successful
local function resume()
properties.suspended = false
@ -392,6 +401,11 @@ function naughty.get_has_display_handler()
return conns["request::display"] and #conns["request::display"] > 0 or false
end
-- Presets are "deprecated" when notification rules are used.
function naughty.get__has_preset_handler()
return conns["request::preset"] and #conns["request::preset"] > 0 or false
end
--- Set new notification timeout.
--
-- This function is deprecated, use `notification:reset_timeout(new_timeout)`.
@ -486,6 +500,12 @@ function naughty.set_expiration_paused(p)
end
end
--- Emitted when an error occurred and requires attention.
-- @signal request::display_error
-- @tparam string message The error message.
-- @tparam boolean startup If the error occurred during the initial loading of
-- rc.lua (and thus caused the fallback to kick in).
--- Emitted when a notification is created.
-- @signal added
-- @tparam naughty.notification notification The notification object
@ -514,13 +534,27 @@ 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
rawset(notification, "get_suspended", get_suspended)
--TODO v5 uncouple the notifications and the screen
local s = get_screen(args.screen or notification.preset.screen or screen.focused())
local s = get_screen(args.screen
or (notification.preset and notification.preset.screen)
or screen.focused())
-- insert the notification to the table
table.insert(naughty._active, notification)
@ -535,7 +569,7 @@ local function register(notification, args)
naughty.emit_signal("added", notification, args)
end
assert(rawget(notification, "preset"))
assert(rawget(notification, "preset") or naughty._has_preset_handler)
naughty.emit_signal("property::active")
@ -564,6 +598,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

@ -12,8 +12,8 @@ local pairs = pairs
local type = type
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 +28,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 +41,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"
}
@ -51,11 +55,7 @@ local urgency = {
-- @tfield table 2 normal urgency
-- @tfield table 3 critical urgency
-- @table config.mapping
dbus.config.mapping = {
{{urgency = urgency.low}, cst.config.presets.low},
{{urgency = urgency.normal}, cst.config.presets.normal},
{{urgency = urgency.critical}, cst.config.presets.critical}
}
dbus.config.mapping = cst.mapping
local function sendActionInvoked(notificationId, action)
if bus_connection then
@ -123,7 +123,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 = {}
@ -141,17 +141,12 @@ function notif_methods.Notify(sender, object_path, interface, method, parameters
return
end
end
if appname ~= "" then
args.appname = appname
end
for _, obj in pairs(dbus.config.mapping) do
local filter, preset = obj[1], obj[2]
if (not filter.urgency or filter.urgency == hints.urgency) and
(not filter.category or filter.category == hints.category) and
(not filter.appname or filter.appname == appname) then
args.preset = gtable.join(args.preset, preset)
end
args.appname = appname --TODO v6 Remove this.
args.app_name = appname
end
local preset = args.preset or cst.config.defaults
local notification
if actions then
@ -167,14 +162,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 +196,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 +237,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 +326,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 +350,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 +417,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 +527,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

@ -5,6 +5,7 @@
---------------------------------------------------------------------------
local naughty = require("naughty.core")
local capi = {awesome = awesome}
if dbus then
naughty.dbus = require("naughty.dbus")
end
@ -17,6 +18,31 @@ naughty.container = require("naughty.container")
naughty.action = require("naughty.action")
naughty.notification = require("naughty.notification")
-- Handle runtime errors during startup
if capi.awesome.startup_errors then
-- Wait until `rc.lua` is executed before creating the notifications.
-- Otherwise nothing is handling them (yet).
awesome.connect_signal("startup", function()
naughty.emit_signal(
"request::display_error", capi.awesome.startup_errors, true
)
end)
end
-- Handle runtime errors after startup
do
local in_error = false
capi.awesome.connect_signal("debug::error", function (err)
-- Make sure we don't go into an endless error loop
if in_error then return end
in_error = true
naughty.emit_signal("request::display_error", tostring(err), false)
in_error = false
end)
end
return naughty
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -17,6 +17,8 @@ local popup = require("awful.popup")
local awcommon = require("awful.widget.common")
local placement = require("awful.placement")
local abutton = require("awful.button")
local gpcall = require("gears.protected_call")
local dpi = require("beautiful").xresources.apply_dpi
local default_widget = require("naughty.widget._default")
@ -135,10 +137,29 @@ end
-- @param widget
local function generate_widget(args, n)
local w = wibox.widget.base.make_widget_from_value(
args.widget_template or default_widget
local w = gpcall(wibox.widget.base.make_widget_from_value,
args.widget_template or (n and n.widget_template) or default_widget
)
-- This will happen if the user-provided widget_template is invalid and/or
-- got unexpected notifications.
if not w then
w = gpcall(wibox.widget.base.make_widget_from_value, default_widget)
-- In case this happens in an error message itself, make sure the
-- private error popup code knowns it and can revert to the fallback
-- popup.
if not w then
n._private.widget_template_failed = true
end
return nil
end
if w.set_width then
w:set_width(n.max_width or beautiful.notification_max_width or dpi(500))
end
-- Call `:set_notification` on all children
awcommon._set_common_property(w, "notification", n or args.notification)
@ -148,8 +169,7 @@ end
local function init(self, notification)
local args = self._private.args
local preset = notification.preset
assert(preset)
local preset = notification.preset or {}
local position = args.position or notification.position or
beautiful.notification_position or preset.position or "top_right"
@ -225,7 +245,10 @@ local function new(args)
new_args = args and setmetatable(new_args, {__index = args}) or new_args
-- Generate the box before the popup is created to avoid the size changing
new_args.widget = generate_widget(new_args)
new_args.widget = generate_widget(new_args, new_args.notification)
-- It failed, request::fallback will be used, there is nothing left to do.
if not new_args.widget then return nil end
local ret = popup(new_args)
ret._private.args = new_args

View File

@ -297,10 +297,20 @@ end
naughty.connect_signal("destroyed", cleanup)
-- Don't copy paste the list of fallback, it is hard to spot mistakes.
local function get_value(notification, args, preset, prop)
return notification[prop] -- set by the rules
or args[prop] -- magic and undocumented, but used by the legacy API
or preset[prop] --deprecated
or beautiful["notification_"..prop] -- from the theme
end
function naughty.default_notification_handler(notification, args)
-- This is a fallback for users whose config doesn't have the newer
-- `request::display` section.
if naughty.has_display_handler then return end
if naughty.has_display_handler and not notification._private.widget_template_failed then
return
end
-- If request::display is called more than once, simply make sure the wibox
-- is visible.
@ -309,10 +319,15 @@ function naughty.default_notification_handler(notification, args)
return
end
local preset = notification.preset
local text = args.message or args.text or preset.message or preset.text
local title = args.title or preset.title
local s = get_screen(args.screen or preset.screen or screen.focused())
local preset = notification.preset or {}
local title = get_value(notification, args, preset, "title" )
local text = get_value(notification, args, preset, "message")
or args.text or preset.text
local s = get_screen(
get_value(notification, args, preset, "screen") or screen.focused()
)
if not s then
local err = "naughty.notify: there is no screen available to display the following notification:"
@ -321,45 +336,41 @@ function naughty.default_notification_handler(notification, args)
return
end
local timeout = args.timeout or preset.timeout
local icon = args.icon or preset.icon
local icon_size = args.icon_size or preset.icon_size
or beautiful.notification_icon_size
local ontop = args.ontop or preset.ontop
local hover_timeout = args.hover_timeout or preset.hover_timeout
local position = args.position or preset.position
local actions = args.actions
local destroy_cb = args.destroy
local timeout = get_value(notification, args, preset, "timeout" )
local icon = get_value(notification, args, preset, "icon" )
local icon_size = get_value(notification, args, preset, "icon_size" )
local ontop = get_value(notification, args, preset, "ontop" )
local hover_timeout = get_value(notification, args, preset, "hover_timeout")
local position = get_value(notification, args, preset, "position" )
local actions = notification.actions or args.actions
local destroy_cb = args.destroy
notification.screen = s
notification.destroy_cb = destroy_cb
notification.timeout = timeout
-- beautiful
local font = args.font or preset.font or beautiful.notification_font or
beautiful.font or capi.awesome.font
local fg = args.fg or preset.fg or
beautiful.notification_fg or beautiful.fg_normal or '#ffffff'
local bg = args.bg or preset.bg or
beautiful.notification_bg or beautiful.bg_normal or '#535d6c'
local border_color = args.border_color or preset.border_color or
beautiful.notification_border_color or beautiful.bg_focus or '#535d6c'
local border_width = args.border_width or preset.border_width or
beautiful.notification_border_width
local shape = args.shape or preset.shape or
beautiful.notification_shape
local width = args.width or preset.width or
beautiful.notification_width
local height = args.height or preset.height or
beautiful.notification_height
local max_width = args.max_width or preset.max_width or
beautiful.notification_max_width
local max_height = args.max_height or preset.max_height or
beautiful.notification_max_height
local margin = args.margin or preset.margin or
beautiful.notification_margin
local opacity = args.opacity or preset.opacity or
beautiful.notification_opacity
local font = get_value(notification, args, preset, "font" )
or beautiful.font or capi.awesome.font
local fg = get_value(notification, args, preset, "fg" )
or beautiful.fg_normal or '#ffffff'
local bg = get_value(notification, args, preset, "bg" )
or beautiful.bg_normal or '#535d6c'
local border_color = get_value(notification, args, preset, "border_color")
or beautiful.bg_focus or '#535d6c'
local border_width = get_value(notification, args, preset, "border_width")
local shape = get_value(notification, args, preset, "shape" )
local width = get_value(notification, args, preset, "width" )
local height = get_value(notification, args, preset, "height" )
local max_width = get_value(notification, args, preset, "max_width" )
local max_height = get_value(notification, args, preset, "max_height" )
local margin = get_value(notification, args, preset, "margin" )
local opacity = get_value(notification, args, preset, "opacity" )
notification.position = position
@ -421,12 +432,10 @@ function naughty.default_notification_handler(notification, args)
actionmarginbox:buttons(gtable.join(
button({ }, 1, function()
action:invoke()
notification:destroy()
action:invoke(notification)
end),
button({ }, 3, function()
action:invoke()
notification:destroy()
action:invoke(notification)
end)
))
actionslayout:add(actionmarginbox)

View File

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

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 = {}
@ -74,8 +75,7 @@ local notification = {}
-- This is the equivalent to a PID as allows external applications to select
-- notifications.
-- @property id
-- @param string
-- @see title
-- @param number
--- Text of the notification.
--
@ -96,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
@ -144,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@
@ -285,11 +396,45 @@ 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
--- The maximum popup width.
--
-- Some notifications have overlong message, cap them to this width. Note that
-- this is ignored by `naughty.list.notifications` because it delegate this
-- decision to the layout.
--
-- @property[opt=500] max_width
-- @param number
--- The application name specified by the notification.
--
-- This can be anything. It is usually less relevant than the `clients`
-- property, but can sometime to specified for remote or headless notifications.
-- In these case, it helps to triage and detect the notification from the rules.
-- @property app_name
-- @param string
--- The widget template used to represent the notification.
--
-- Some notifications, such as chat messages or music applications are better
-- off with a specialized notification widget.
--
-- @property widget_template
-- @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
@ -319,7 +464,11 @@ end
function notification:reset_timeout(new_timeout)
if self.timer then self.timer:stop() end
self.timeout = new_timeout or self.timeout
-- Do not set `self.timeout` to `self.timeout` since that would create the
-- timer before the constructor ends.
if new_timeout and self.timer then
self.timeout = new_timeout or self.timeout
end
if self.timer and not self.timer.started then
self.timer:start()
@ -333,6 +482,8 @@ function notification:set_id(new_id)
end
function notification:set_timeout(timeout)
timeout = timeout or 0
local die = function (reason)
if reason == cst.notification_closed_reason.expired then
self.is_expired = true
@ -399,7 +550,9 @@ 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" , "widget_template",
"max_width", "app_name",
}
for _, prop in ipairs(properties) do
@ -426,8 +579,107 @@ for _, prop in ipairs(properties) do
if reset then
self:reset_timeout()
end
end
return
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
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
--- Add more actions to the notification.
-- @method append_actions
-- @tparam table new_actions
function notification:append_actions(new_actions)
self._private.actions = self._private.actions or {}
for _, a in ipairs(new_actions or {}) do
a:connect_signal("_changed", self._private.action_cb )
a:connect_signal("invoked" , self._private.invoked_cb)
table.insert(self._private.actions, a)
end
end
@ -476,6 +728,33 @@ local function convert_actions(actions)
end
end
-- The old API used monkey-patched variable presets.
--
-- Monkey-patched anything is always an issue and prevent module from safely
-- doing anything without stepping on each other foot. In the background,
-- presets were selected with a rule-like API anyway.
local function select_legacy_preset(n, args)
for _, obj in pairs(cst.config.mapping) do
local filter, preset = obj[1], obj[2]
if (not filter.urgency or filter.urgency == args.urgency) and
(not filter.category or filter.category == args.category) and
(not filter.appname or filter.appname == args.appname) then
args.preset = gtable.join(args.preset or {}, preset)
end
end
-- gather variables together
rawset(n, "preset", gtable.join(
cst.config.defaults or {},
args.preset or cst.config.presets.normal or {},
rawget(n, "preset") or {}
))
for k, v in pairs(n.preset) do
n._private[k] = v
end
end
--- Create a notification.
--
-- @tab args The argument table containing any of the arguments below.
@ -557,60 +836,71 @@ local function create(args)
-- Avoid modifying the original table
local private = {}
rawset(n, "_private", private)
-- gather variables together
rawset(n, "preset", gtable.join(
cst.config.defaults or {},
args.preset or cst.config.presets.normal or {},
rawget(n, "preset") or {}
))
-- Allow extensions to create override the preset with custom data
if not naughty._has_preset_handler then
select_legacy_preset(n, args)
end
if is_old_action then
convert_actions(args.actions)
end
for k, v in pairs(n.preset) do
private[k] = v
end
for k, v in pairs(args) do
private[k] = v
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
n.is_expired = false
rawset(n, "_private", private)
gtable.crush(n, notification, true)
n.id = n.id or notification._gen_next_id()
-- 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
-- Allow extensions to create override the preset with custom data
naughty.emit_signal("request::preset", n, args)
-- 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()
-- Register the notification before requesting a widget
n:emit_signal("new", args)
-- The rules are attached to this.
if naughty._has_preset_handler then
naughty.emit_signal("request::preset", n, args)
end
-- Let all listeners handle the actual visual aspects
if (not n.ignore) and (not n.preset.ignore) then
if (not n.ignore) and ((not n.preset) or n.preset.ignore ~= true) then
naughty.emit_signal("request::display" , n, args)
naughty.emit_signal("request::fallback", n, args)
end
-- Because otherwise the setter logic would not be executed
if n._private.timeout then
n:set_timeout(n._private.timeout or n.preset.timeout)
n:set_timeout(n._private.timeout
or (n.preset and n.preset.timeout)
or cst.config.timeout
)
end
return n

View File

@ -19,6 +19,7 @@
local imagebox = require("wibox.widget.imagebox")
local gtable = require("gears.table")
local beautiful = require("beautiful")
local gsurface = require("gears.surface")
local dpi = require("beautiful.xresources").apply_dpi
local icon = {}
@ -85,7 +86,11 @@ function icon:set_notification(notif)
self._private.icon_changed_callback)
end
self:set_image(notif.icon)
local icn = gsurface.load_silently(notif.icon)
if icn then
self:set_image(icn)
end
self._private.notification = notif
@ -141,7 +146,11 @@ local function new(args)
gtable.crush(tb, icon, true)
function tb._private.icon_changed_callback()
tb:set_image(tb._private.notification.icon)
local icn = gsurface.load_silently(tb._private.notification.icon)
if icn then
tb:set_image()
end
end
if args.notification then

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

View File

@ -3,10 +3,13 @@
local spawn = require("awful.spawn")
local naughty = require("naughty" )
local gdebug = require("gears.debug")
local gtable = require("gears.table")
local cairo = require("lgi" ).cairo
local beautiful = require("beautiful")
local Gio = require("lgi" ).Gio
local GLib = require("lgi" ).GLib
local gpcall = require("gears.protected_call")
local dwidget = require("naughty.widget._default")
-- This module test deprecated APIs
require("gears.debug").deprecate = function() end
@ -555,6 +558,16 @@ table.insert(steps, function()
assert(n2.box.width +2*n2.box.border_width <= wa.width )
assert(n2.box.height+2*n2.box.border_width <= wa.height)
-- Check with client icons.
assert(not n1.icon)
n1._private.clients = {{icon= big_icon, type = "normal"}}
assert(n1.icon == big_icon)
assert(n1.box.width +2*n1.box.border_width <= wa.width )
assert(n1.box.height+2*n1.box.border_width <= wa.height)
assert(n2.box.width +2*n2.box.border_width <= wa.width )
assert(n2.box.height+2*n2.box.border_width <= wa.height)
n1:destroy()
n2:destroy()
@ -673,6 +686,9 @@ table.insert(steps, function()
assert(n.actions[2].name == "five" )
assert(n.actions[3].name == "six" )
n:destroy()
assert(#active == 0)
return true
end)
@ -719,6 +735,162 @@ table.insert(steps, function()
return true
end)
-- Test adding actions, resident mode and action sharing.
table.insert(steps, function()
local n1 = naughty.notification {
title = "foo",
message = "bar",
timeout = 25000,
resident = true,
actions = { naughty.action { name = "a1" } }
}
local n2 = naughty.notification {
title = "foo",
message = "bar",
resident = true,
timeout = 25000,
actions = { naughty.action { name = "a2" } }
}
local is_called = {}
n1:connect_signal("invoked", function() is_called[1] = true end)
n2:connect_signal("invoked", function() is_called[2] = true end)
n1.actions[1]:invoke(n1)
n2.actions[1]:invoke(n2)
assert(is_called[1])
assert(is_called[2])
assert(not n1._private.is_destroyed)
assert(not n2._private.is_destroyed)
local shared_a = naughty.action { name = "a3" }
n1:append_actions {shared_a}
n2:append_actions {shared_a}
n1:connect_signal("invoked", function() is_called[3] = true end)
n2:connect_signal("invoked", function() is_called[4] = true end)
assert(n1.actions[2] == shared_a)
assert(n2.actions[2] == shared_a)
shared_a:invoke(n1)
assert(is_called[3])
assert(not is_called[4])
assert(not n1._private.is_destroyed)
assert(not n2._private.is_destroyed)
n1.resident = false
n2.resident = false
shared_a:invoke(n1)
assert(n1._private.is_destroyed)
assert(not n2._private.is_destroyed)
shared_a:invoke(n2)
assert(n2._private.is_destroyed)
return true
end)
-- Test that exposing support for animations and persistence are exposed to DBus.
table.insert(steps, function()
assert(not naughty.persistence_enabled)
assert(not naughty.image_animations_enabled)
assert(gtable.hasitem(naughty.dbus._capabilities, "icon-static"))
assert(not gtable.hasitem(naughty.dbus._capabilities, "icon-multi"))
assert(not gtable.hasitem(naughty.dbus._capabilities, "persistence"))
naughty.persistence_enabled = true
naughty.image_animations_enabled = true
assert(naughty.persistence_enabled)
assert(naughty.image_animations_enabled)
assert(gtable.hasitem(naughty.dbus._capabilities, "icon-multi"))
assert(gtable.hasitem(naughty.dbus._capabilities, "persistence"))
assert(not gtable.hasitem(naughty.dbus._capabilities, "icon-static"))
naughty.persistence_enabled = false
naughty.image_animations_enabled = false
assert(not naughty.persistence_enabled)
assert(not naughty.image_animations_enabled)
assert( gtable.hasitem(naughty.dbus._capabilities, "icon-static"))
assert(not gtable.hasitem(naughty.dbus._capabilities, "icon-multi" ))
assert(not gtable.hasitem(naughty.dbus._capabilities, "persistence"))
return true
end)
local icon_requests = {}
-- Check if the action icon support is detected.
table.insert(steps, function()
assert(#active == 0)
naughty.connect_signal("request::icon", function(a, icon_name)
icon_requests[icon_name] = a
a.icon = icon_name == "list-add" and small_icon or big_icon
end)
local hints = {
["action-icons"] = GLib.Variant("b", true),
}
send_notify("Awesome test", 0, "", "title", "message body",
{ "list-add", "add", "list-remove", "remove" }, hints, 25000)
return true
end)
table.insert(steps, function()
if #active ~= 1 then return end
local n = active[1]
assert(n._private.freedesktop_hints)
assert(n._private.freedesktop_hints["action-icons"] == true)
assert(icon_requests["list-add" ] == n.actions[1])
assert(icon_requests["list-remove"] == n.actions[2])
assert(n.actions[1].icon == small_icon)
assert(n.actions[2].icon == big_icon )
assert(type(n.actions[1].position) == "number")
assert(type(n.actions[2].position) == "number")
assert(n.actions[1].position == 1)
assert(n.actions[2].position == 2)
n:destroy()
return true
end)
-- Test the error popup.
table.insert(steps, function()
local got = nil
naughty.connect_signal("request::display_error", function(err)
got = err
end)
awesome.emit_signal("debug::error", "foo")
assert(got == "foo")
return true
end)
-- Now check if the old deprecated (but still supported) APIs don't have errors.
table.insert(steps, function()
-- Tests are (by default) not allowed to call deprecated APIs
@ -830,6 +1002,82 @@ table.insert(steps, function()
return true
end)
-- Add a "new API" handler.
local current_template, had_error, handler_called = nil
table.insert(steps, function()
assert(naughty.has_display_handler == false)
naughty.connect_signal("request::display", function(n)
handler_called = true
naughty.layout.box {
notification = n,
widget_template = current_template
}
end)
return true
end)
-- Make sure the legacy popup is used when the new APIs fails.
table.insert(steps, function()
assert(naughty.has_display_handler == true)
function gpcall._error_handler()
had_error = true
end
local n = naughty.notification {
title = nil,
message = nil,
timeout = 25000,
}
assert(handler_called)
assert(not had_error)
assert(not n._private.widget_template_failed)
assert(not n.box)
n:destroy()
handler_called = false
-- Try with a broken template.
current_template = {widget = function() assert(false) end}
n = naughty.notification {
title = "foo",
message = "bar",
timeout = 25000,
}
assert(handler_called)
assert(had_error)
assert(not n.box)
handler_called = false
had_error = false
-- Break the default template
assert(dwidget.widget)
dwidget.widget = nil
dwidget.layout = function() assert(false) end
table.remove(dwidget, 1)
n = naughty.notification {
title = "foo",
message = "bar",
timeout = 25000,
}
assert(handler_called)
assert(n._private.widget_template_failed)
assert(had_error)
assert(n.box)
return true
end)
-- Test many screens.
require("_runner").run_steps(steps)