From 898db884434f95edc981e7ca596a8e78e2002d04 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Mon, 6 Aug 2018 15:53:55 -0400 Subject: [PATCH 01/21] Revert "fix(naughty: core): don't attempt to upscale small icons (#2283)" This reverts commit b77ffa86e02ed0ce4ce4a5e786e395871b8b17d7. --- lib/naughty/core.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index 5363a73a6..53cb2892c 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -787,7 +787,7 @@ function naughty.notify(args) if icon then iconbox = wibox.widget.imagebox() iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin) - if icon_size and (icon:get_height() > icon_size or icon:get_width() > icon_size) then + if icon_size then local scale_factor = icon_size / math.max(icon:get_height(), icon:get_width()) local scaled = From 0703156155333a88843a836846177b4ff9eab73d Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 1 Jan 2019 16:56:52 -0500 Subject: [PATCH 02/21] Revert "naughty: add notification max width and height (#2232)" This reverts commit 5e6f5343653a3f69ce353f3a3388a9b6f48e370a. --- lib/naughty/core.lua | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index 53cb2892c..9d88ff897 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -190,14 +190,6 @@ naughty.notificationClosedReason = { -- @beautiful beautiful.notification_height -- @tparam int notification_height ---- Notifications maximum width. --- @beautiful beautiful.notification_max_width --- @tparam int notification_max_width - ---- Notifications maximum height. --- @beautiful beautiful.notification_max_height --- @tparam int notification_max_height - --- Notifications icon size. -- @beautiful beautiful.notification_icon_size -- @tparam int notification_icon_size @@ -509,10 +501,6 @@ local function update_size(notification) width = s.actions_max_width end - if s.max_width then - width = math.min(width, s.max_width) - end - -- calculate the height if not height then local w = width - (n.iconbox and s.icon_w + 2 * margin or 0) - 2 * margin @@ -526,10 +514,6 @@ local function update_size(notification) height = height + s.actions_total_height - if s.max_height then - height = math.min(height, s.max_height) - end - -- crop to workarea size if too big local workarea = n.screen.workarea local border_width = s.border_width or 0 @@ -586,8 +570,6 @@ end -- @bool[opt=true] args.ontop Boolean forcing popups to display on top. -- @int[opt=`beautiful.notification_height` or auto] args.height Popup height. -- @int[opt=`beautiful.notification_width` or auto] args.width Popup width. --- @int[opt=`beautiful.notification_max_height` or auto] args.max_height Popup maximum height. --- @int[opt=`beautiful.notification_max_width` or auto] args.max_width Popup maximum width. -- @string[opt=`beautiful.notification_font` or `beautiful.font` or `awesome.font`] args.font Notification font. -- @string[opt] args.icon Path to icon. -- @int[opt] args.icon_size Desired icon size in px. @@ -663,10 +645,6 @@ function naughty.notify(args) 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 @@ -827,8 +805,6 @@ function naughty.notify(args) notification.size_info = { width = width, height = height, - max_width = max_width, - max_height = max_height, icon_w = icon_w, icon_h = icon_h, margin = margin, From a2f314c3499c79e6cd53611e6a621369e51a3a78 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Mon, 28 May 2018 13:19:50 -0400 Subject: [PATCH 03/21] Revert "Add support for resizing notification icon with respect to aspect (#2176)" This reverts commit 584c5cedb1f2f970612e245db3f30d026eff8e8d. --- lib/naughty/core.lua | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index 9d88ff897..f08e84faf 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -22,7 +22,6 @@ local screen = require("awful.screen") local util = require("awful.util") local gtable = require("gears.table") local gfs = require("gears.filesystem") -local gmath = require("gears.math") local beautiful = require("beautiful") local wibox = require("wibox") local surface = require("gears.surface") @@ -766,14 +765,9 @@ function naughty.notify(args) iconbox = wibox.widget.imagebox() iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin) if icon_size then - local scale_factor = icon_size / math.max(icon:get_height(), - icon:get_width()) - local scaled = - cairo.ImageSurface(cairo.Format.ARGB32, - gmath.round(icon:get_width() * scale_factor), - gmath.round(icon:get_height() * scale_factor)) + local scaled = cairo.ImageSurface(cairo.Format.ARGB32, icon_size, icon_size) local cr = cairo.Context(scaled) - cr:scale(scale_factor, scale_factor) + cr:scale(icon_size / icon:get_height(), icon_size / icon:get_width()) cr:set_source_surface(icon, 0, 0) cr:paint() icon = scaled From 74ef4dd8b734c0fbeff5453bd1713382b66c11a0 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Thu, 26 Oct 2017 17:20:48 +0200 Subject: [PATCH 04/21] Revert "naughty: Fix replaces_id + changing colors (#2041)" This reverts commit 4e42996d9a7e6a3f2da294f072b5288a6c34e4cd. --- lib/naughty/core.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index f08e84faf..f55e68d55 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -781,18 +781,18 @@ function naughty.notify(args) notification.iconbox = iconbox -- create container wibox - if not reuse_box then - notification.box = wibox({ type = "notification" }) - else + notification.box = wibox({ fg = fg, + bg = bg, + border_color = border_color, + border_width = border_width, + shape_border_color = shape and border_color, + shape_border_width = shape and border_width, + shape = shape, + type = "notification" }) + + if reuse_box then notification.box = reuse_box end - notification.box.fg = fg - notification.box.bg = bg - notification.box.border_color = border_color - notification.box.border_width = border_width - notification.box.shape_border_color = shape and border_color - notification.box.shape_border_width = shape and border_width - notification.box.shape = shape if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end From 3e70e877968f571161ae2c3ba85a47b8b7ade600 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 1 Jan 2019 16:46:36 -0500 Subject: [PATCH 05/21] Revert "dbus: fix nil notifications (#2180)" This reverts commit 7519c6966a50994c546a56c557a21f2a87e37108. --- lib/naughty/core.lua | 6 ------ lib/naughty/dbus.lua | 6 ++---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index f55e68d55..7c0ffafcf 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -411,12 +411,6 @@ function naughty.getById(id) end end ---- Increase notification ID by one -function naughty.get_next_notification_id() - counter = counter + 1 - return counter -end - --- Install expiration timer for notification object. -- @tparam notification notification Notification object. -- @tparam number timeout Time in seconds to be set as expiration timeout. diff --git a/lib/naughty/dbus.lua b/lib/naughty/dbus.lua index ad9303940..bb9fcfc50 100644 --- a/lib/naughty/dbus.lua +++ b/lib/naughty/dbus.lua @@ -191,11 +191,9 @@ capi.dbus.connect_signal("org.freedesktop.Notifications", end args.freedesktop_hints = hints notification = naughty.notify(args) - if notification ~= nil then - return "u", notification.id - end + return "u", notification.id end - return "u", naughty.get_next_notification_id() + return "u", "0" elseif data.member == "CloseNotification" then local obj = naughty.getById(appname) if obj then From 14eab7890f491966bea29cf60438c9ea7a33dc5f Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 15 Jul 2017 22:54:41 -0400 Subject: [PATCH 06/21] naughty: Add signal support --- lib/naughty/core.lua | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index 7c0ffafcf..24398b4b0 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -239,6 +239,51 @@ function naughty.suspend() suspended = true end +local conns = {} + +--- Connect a global signal on the notification engine. +-- +-- Functions connected to this signal source will be executed when any +-- notification object emits the signal. +-- +-- It is also used for some generic notification signals such as +-- `request::display`. +-- +-- @tparam string name The name of the signal +-- @tparam function func The function to attach +-- @usage naughty.connect_signal("added", function(notif) +-- -- do something +-- end) +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 +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 +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 + --- Resume notifications function naughty.resume() suspended = false From 9df77e5c765d1b523babcdbc6a45e6cbafebcc21 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 1 Jul 2017 22:43:26 -0400 Subject: [PATCH 07/21] naughty: Move the notification object into its own file. Mostly for the documentation, but also as the new base upon which to build the modular notification GUI. --- awesomerc.lua | 17 +- docs/config.ld | 3 + lib/awful/spawn.lua | 6 +- lib/naughty/constants.lua | 74 ++ lib/naughty/core.lua | 870 +++++------------- lib/naughty/dbus.lua | 21 +- lib/naughty/init.lua | 3 + lib/naughty/layout/init.lua | 9 + lib/naughty/layout/legacy.lua | 544 +++++++++++ lib/naughty/notification.lua | 416 +++++++++ .../text/awful/keygrabber/autostart.lua | 4 +- .../wibox/awidget/prompt/keypress.lua | 2 +- .../examples/wibox/awidget/prompt/simple.lua | 2 +- tests/test-screen-changes.lua | 2 +- 14 files changed, 1334 insertions(+), 639 deletions(-) create mode 100644 lib/naughty/constants.lua create mode 100644 lib/naughty/layout/init.lua create mode 100644 lib/naughty/layout/legacy.lua create mode 100644 lib/naughty/notification.lua diff --git a/awesomerc.lua b/awesomerc.lua index d3390c574..0bc0432cb 100644 --- a/awesomerc.lua +++ b/awesomerc.lua @@ -24,9 +24,11 @@ require("awful.hotkeys_popup.keys") -- 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.notify({ preset = naughty.config.presets.critical, - title = "Oops, there were errors during startup!", - text = awesome.startup_errors }) + naughty.notification { + preset = naughty.config.presets.critical, + title = "Oops, there were errors during startup!", + text = awesome.startup_errors + } end -- Handle runtime errors after startup @@ -37,9 +39,12 @@ do if in_error then return end in_error = true - naughty.notify({ preset = naughty.config.presets.critical, - title = "Oops, an error happened!", - text = tostring(err) }) + naughty.notification { + preset = naughty.config.presets.critical, + title = "Oops, an error happened!", + text = tostring(err) + } + in_error = false end) end diff --git a/docs/config.ld b/docs/config.ld index 7e9164c39..5a264241c 100644 --- a/docs/config.ld +++ b/docs/config.ld @@ -121,9 +121,12 @@ file = { '../lib/gears/init.lua', '../lib/wibox/layout/init.lua', '../lib/wibox/container/init.lua', + '../lib/naughty/constants.lua', + '../lib/naughty/dbus.lua', -- Ignore some parts of the widget library '../lib/awful/widget/init.lua', + '../lib/naughty/layout/init.lua', -- Deprecated classes for one years or more don't deserve entries -- in the index diff --git a/lib/awful/spawn.lua b/lib/awful/spawn.lua index 3ffb1f95d..37b634c1b 100644 --- a/lib/awful/spawn.lua +++ b/lib/awful/spawn.lua @@ -132,17 +132,17 @@ -- -- awful.spawn.with_line_callback(noisy, { -- stdout = function(line) --- naughty.notify { text = "LINE:"..line } +-- naughty.notification { text = "LINE:"..line } -- end, -- stderr = function(line) --- naughty.notify { text = "ERR:"..line} +-- naughty.notification { text = "ERR:"..line} -- end, -- }) -- -- If only the full output is needed, then `easy_async` is the right choice: -- -- awful.spawn.easy_async(noisy, function(stdout, stderr, reason, exit_code) --- naughty.notify { text = stdout } +-- naughty.notification { text = stdout } -- end) -- -- **Default applications**: diff --git a/lib/naughty/constants.lua b/lib/naughty/constants.lua new file mode 100644 index 000000000..aa7906bc1 --- /dev/null +++ b/lib/naughty/constants.lua @@ -0,0 +1,74 @@ +---------------------------------------------------------------------------- +--- This file hosts the shared constants used by the notification subsystem. +-- +-- [[documented in core.lua]] +-- +-- @author koniu <gkusnierz@gmail.com> +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2008 koniu +-- @copyright 2017 Emmanuel Lepage Vallee +---------------------------------------------------------------------------- +local beautiful = require("beautiful") +local dpi = beautiful.xresources.apply_dpi + +local ret = {} + +ret.config = { + padding = dpi(4), + spacing = dpi(1), + icon_dirs = { "/usr/share/pixmaps/", }, + icon_formats = { "png", "gif" }, + notify_callback = nil, +} + +ret.config.presets = { + low = { + timeout = 5 + }, + normal = {}, + critical = { + bg = "#ff0000", + fg = "#ffffff", + timeout = 0, + }, + ok = { + bg = "#00bb00", + fg = "#ffffff", + timeout = 5, + }, + info = { + bg = "#0000ff", + fg = "#ffffff", + timeout = 5, + }, + warn = { + bg = "#ffaa00", + fg = "#000000", + timeout = 10, + }, +} + +ret.config.defaults = { + timeout = 5, + text = "", + screen = nil, + ontop = true, + margin = dpi(5), + border_width = dpi(1), + position = "top_right" +} + +ret.notification_closed_reason = { + silent = -1, + expired = 1, + 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, + undefined = 4 +} + +-- Legacy --TODO v5 remove this alias +ret.notificationClosedReason = ret.notification_closed_reason + +return ret diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index 24398b4b0..ea3827407 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -9,67 +9,43 @@ --luacheck: no max line length -- Package environment -local pairs = pairs -local table = table -local type = type -local string = string -local pcall = pcall -local capi = { screen = screen, - awesome = awesome } -local timer = require("gears.timer") -local button = require("awful.button") +local capi = { screen = screen } +local gdebug = require("gears.debug") local screen = require("awful.screen") -local util = require("awful.util") local gtable = require("gears.table") -local gfs = require("gears.filesystem") -local beautiful = require("beautiful") -local wibox = require("wibox") -local surface = require("gears.surface") -local cairo = require("lgi").cairo -local dpi = beautiful.xresources.apply_dpi - -local function get_screen(s) - return s and capi.screen[s] -end local naughty = {} ---[[-- -Naughty configuration - a table containing common popup settings. - -@table naughty.config -@tfield[opt=apply_dpi(4)] int padding Space between popups and edge of the - workarea. -@tfield[opt=apply_dpi(1)] int spacing Spacing between popups. -@tfield[opt={"/usr/share/pixmaps/"}] table icon_dirs List of directories - that will be checked by `getIcon()`. -@tfield[opt={ "png", "gif" }] table icon_formats List of formats that will be - checked by `getIcon()`. -@tfield[opt] function notify_callback Callback used to modify or reject -notifications, e.g. - naughty.config.notify_callback = function(args) - args.text = 'prefix: ' .. args.text - return args - end - To reject a notification return `nil` from the callback. - If the notification is a freedesktop notification received via DBUS, you can - access the freedesktop hints via `args.freedesktop_hints` if any where - specified. - -@tfield table presets Notification presets. See `config.presets`. - -@tfield table defaults Default values for the params to `notify()`. These can - optionally be overridden by specifying a preset. See `config.defaults`. - ---]] +--- Naughty configuration - a table containing common popup settings. -- -naughty.config = { - padding = dpi(4), - spacing = dpi(1), - icon_dirs = { "/usr/share/pixmaps/", }, - icon_formats = { "png", "gif" }, - notify_callback = nil, -} +-- @table naughty.config +-- @tfield[opt=apply_dpi(4)] int padding Space between popups and edge of the +-- workarea. +-- @tfield[opt=apply_dpi(1)] int spacing Spacing between popups. +-- @tfield[opt={"/usr/share/pixmaps/"}] table icon_dirs List of directories +-- that will be checked by `getIcon()`. +-- @tfield[opt={ "png", "gif" }] table icon_formats List of formats that will be +-- checked by `getIcon()`. +-- @tfield[opt] function notify_callback Callback used to modify or reject +-- notifications, e.g. +-- naughty.config.notify_callback = function(args) +-- args.text = 'prefix: ' .. args.text +-- return args +-- end +-- To reject a notification return `nil` from the callback. +-- If the notification is a freedesktop notification received via DBUS, you can +-- access the freedesktop hints via `args.freedesktop_hints` if any where +-- specified. +-- +-- @tfield table presets Notification presets. See `config.presets`. +-- +-- @tfield table defaults Default values for the params to `notify()`. These can +-- optionally be overridden by specifying a preset. See `config.defaults`. + +-- It's done that way to preserve compatibility with Awesome 4.0 while allowing +-- the naughty submodules to use the contants without creating a circular +-- dependency. +gtable.crush(naughty, require("naughty.constants")) --- Notification presets for `naughty.notify`. -- This holds presets for different purposes. A preset is a table of any @@ -92,32 +68,6 @@ naughty.config = { -- @tfield[opt="#ff0000"] string critical.bg -- @tfield[opt="#ffffff"] string critical.fg -- @tfield[opt=0] string critical.timeout -naughty.config.presets = { - low = { - timeout = 5 - }, - normal = {}, - critical = { - bg = "#ff0000", - fg = "#ffffff", - timeout = 0, - }, - ok = { - bg = "#00bb00", - fg = "#ffffff", - timeout = 5, - }, - info = { - bg = "#0000ff", - fg = "#ffffff", - timeout = 5, - }, - warn = { - bg = "#ffaa00", - fg = "#000000", - timeout = 10, - }, -} --- Defaults for `naughty.notify`. -- @@ -129,78 +79,29 @@ naughty.config.presets = { -- @tfield[opt=apply_dpi(5)] int margin -- @tfield[opt=apply_dpi(1)] int border_width -- @tfield[opt="top_right"] string position -naughty.config.defaults = { - timeout = 5, - text = "", - screen = nil, - ontop = true, - margin = dpi(5), - border_width = dpi(1), - position = "top_right" -} -naughty.notificationClosedReason = { - silent = -1, - expired = 1, - dismissedByUser = 2, - dismissedByCommand = 3, - undefined = 4 -} +--- The reason why a notification is to be closed. +-- See [the specification](https://developer.gnome.org/notification-spec/#signals) +-- for more details. +-- @tfield number silent +-- @tfield number expired +-- @tfield number dismissed_by_user +-- @tfield number dismissed_by_command +-- @tfield number undefined +-- @table notification_closed_reason +--- The global suspension state. +-- +-- When suspended, no notification widget should interrupt the user. This is +-- useful when watching movies or doing presentations. +-- +-- @property suspended +-- @param boolean ---- Notifications font. --- @beautiful beautiful.notification_font --- @tparam string|lgi.Pango.FontDescription notification_font - ---- Notifications background color. --- @beautiful beautiful.notification_bg --- @tparam color notification_bg - ---- Notifications foreground color. --- @beautiful beautiful.notification_fg --- @tparam color notification_fg - ---- Notifications border width. --- @beautiful beautiful.notification_border_width --- @tparam int notification_border_width - ---- Notifications border color. --- @beautiful beautiful.notification_border_color --- @tparam color notification_border_color - ---- Notifications shape. --- @beautiful beautiful.notification_shape --- @tparam[opt] gears.shape notification_shape --- @see gears.shape - ---- Notifications opacity. --- @beautiful beautiful.notification_opacity --- @tparam[opt] int notification_opacity - ---- Notifications margin. --- @beautiful beautiful.notification_margin --- @tparam int notification_margin - ---- Notifications width. --- @beautiful beautiful.notification_width --- @tparam int notification_width - ---- Notifications height. --- @beautiful beautiful.notification_height --- @tparam int notification_height - ---- Notifications icon size. --- @beautiful beautiful.notification_icon_size --- @tparam int notification_icon_size - - --- Counter for the notifications --- Required for later access via DBUS -local counter = 1 - --- True if notifying is suspended local suspended = false +--TODO v5 Deprecate the public `naughty.notifications` (to make it private) + --- Index of notifications per screen and position. -- See config table for valid 'position' values. -- Each element is a table consisting of: @@ -229,13 +130,23 @@ capi.screen.connect_signal("removed", function(scr) naughty.notifications[scr] = nil end) ---- Notification state +--- Notification state. +-- +-- This function is deprecated, use `naughty.suspended`. +-- +-- @deprecated naughty.is_suspended function naughty.is_suspended() + gdebug.deprecate("Use naughty.suspended", {deprecated_in=5}) return suspended end ---- Suspend notifications +--- Suspend notifications. +-- +-- This function is deprecated, use `naughty.suspended = true`. +-- +-- @deprecated naughty.suspend function naughty.suspend() + gdebug.deprecate("Use naughty.suspended = true", {deprecated_in=5}) suspended = true end @@ -284,18 +195,32 @@ function naughty.disconnect_signal(name, func) return false end ---- Resume notifications -function naughty.resume() +local function resume() suspended = false for _, v in pairs(naughty.notifications.suspended) do - v.box.visible = true + v:emit_signal("request::display") if v.timer then v.timer:start() end end naughty.notifications.suspended = { } end ---- Toggle notification state +--- Resume notifications. +-- +-- This function is deprecated, use `naughty.suspended = false`. +-- +-- @deprecated naughty.resume +function naughty.resume() + gdebug.deprecate("Use naughty.suspended = false", {deprecated_in=5}) + resume() +end + +--- Toggle notification state. +-- +-- This function is deprecated, use `naughty.suspended = not naughty.suspended`. +-- +-- @deprecated naughty.toggle function naughty.toggle() + gdebug.deprecate("Use naughty.suspended = not naughty.suspended", {deprecated_in=5}) if suspended then naughty.resume() else @@ -303,124 +228,33 @@ function naughty.toggle() end end ---- Evaluate desired position of the notification by index - internal --- --- @param s Screen to use --- @param position top_right | top_left | bottom_right | bottom_left --- | top_middle | bottom_middle --- @param idx Index of the notification --- @param[opt] width Popup width. --- @param height Popup height --- @return Absolute position and index in { x = X, y = Y, idx = I } table -local function get_offset(s, position, idx, width, height) - s = get_screen(s) - local ws = s.workarea - local v = {} - idx = idx or #naughty.notifications[s][position] + 1 - width = width or naughty.notifications[s][position][idx].width - - -- calculate x - if position:match("left") then - v.x = ws.x + naughty.config.padding - elseif position:match("middle") then - v.x = ws.x + (ws.width / 2) - (width / 2) - else - v.x = ws.x + ws.width - (width + naughty.config.padding) - end - - -- calculate existing popups' height - local existing = 0 - for i = 1, idx-1, 1 do - existing = existing + naughty.notifications[s][position][i].height + naughty.config.spacing - end - - -- calculate y - if position:match("top") then - v.y = ws.y + naughty.config.padding + existing - else - v.y = ws.y + ws.height - (naughty.config.padding + height + existing) - end - - -- Find old notification to replace in case there is not enough room. - -- This tries to skip permanent notifications (without a timeout), - -- e.g. critical ones. - local find_old_to_replace = function() - for i = 1, idx-1 do - local n = naughty.notifications[s][position][i] - if n.timeout > 0 then - return n - end - end - -- Fallback to first one. - return naughty.notifications[s][position][1] - end - - -- if positioned outside workarea, destroy oldest popup and recalculate - if v.y + height > ws.y + ws.height or v.y < ws.y then - naughty.destroy(find_old_to_replace()) - idx = idx - 1 - v = get_offset(s, position, idx, width, height) - end - if not v.idx then v.idx = idx end - - return v -end - ---- Re-arrange notifications according to their position and index - internal --- --- @return None -local function arrange(s) - for p in pairs(naughty.notifications[s]) do - for i,notification in pairs(naughty.notifications[s][p]) do - local offset = get_offset(s, p, i, notification.width, notification.height) - notification.box:geometry({ x = offset.x, y = offset.y }) - notification.idx = offset.idx - end - end -end - --- Destroy notification by notification object -- +-- This function is deprecated in favor of +-- `notification:destroy(reason, keep_visible)`. +-- -- @param notification Notification object to be destroyed --- @param reason One of the reasons from notificationClosedReason +-- @param reason One of the reasons from `notification_closed_reason` -- @param[opt=false] keep_visible If true, keep the notification visible -- @return True if the popup was successfully destroyed, nil otherwise +-- @deprecated naughty.destroy function naughty.destroy(notification, reason, keep_visible) - if notification and notification.box.visible then - if suspended then - for k, v in pairs(naughty.notifications.suspended) do - if v.box == notification.box then - table.remove(naughty.notifications.suspended, k) - break - end - end - end - local scr = notification.screen - table.remove(naughty.notifications[scr][notification.position], notification.idx) - if notification.timer then - notification.timer:stop() - end + gdebug.deprecate("Use notification:destroy(reason, keep_visible)", {deprecated_in=5}) - if not keep_visible then - notification.box.visible = false - arrange(scr) - end + if not notification then return end - if notification.destroy_cb and reason ~= naughty.notificationClosedReason.silent then - notification.destroy_cb(reason or naughty.notificationClosedReason.undefined) - end - return true - end + return notification:destroy(reason, keep_visible) end --- Destroy all notifications on given screens. -- -- @tparam table screens Table of screens on which notifications should be -- destroyed. If nil, destroy notifications on all screens. --- @tparam naughty.notificationClosedReason reason Reason for closing +-- @tparam naughty.notification_closed_reason reason Reason for closing -- notifications. -- @treturn true|nil True if all notifications were successfully destroyed, nil -- otherwise. +-- @see notification_closed_reason function naughty.destroy_all_notifications(screens, reason) if not screens then screens = {} @@ -432,7 +266,7 @@ function naughty.destroy_all_notifications(screens, reason) for _, scr in pairs(screens) do for _, list in pairs(naughty.notifications[scr]) do while #list > 0 do - ret = ret and naughty.destroy(list[1], reason) + ret = ret and list[1]:destroy(reason) end end end @@ -443,7 +277,17 @@ end -- -- @param id ID of the notification -- @return notification object if it was found, nil otherwise +-- @deprecated naughty.getById function naughty.getById(id) + gdebug.deprecate("Use naughty.get_by_id", {deprecated_in=5}) + return naughty.get_by_id(id) +end + +--- Get notification by ID +-- +-- @param id ID of the notification +-- @return notification object if it was found, nil otherwise +function naughty.get_by_id(id) -- iterate the notifications to get the notfications with the correct ID for s in pairs(naughty.notifications) do for p in pairs(naughty.notifications[s]) do @@ -456,145 +300,167 @@ function naughty.getById(id) end end ---- Install expiration timer for notification object. --- @tparam notification notification Notification object. --- @tparam number timeout Time in seconds to be set as expiration timeout. -local function set_timeout(notification, timeout) - local die = function (reason) - naughty.destroy(notification, reason) - end - if timeout > 0 then - local timer_die = timer { timeout = timeout } - timer_die:connect_signal("timeout", function() die(naughty.notificationClosedReason.expired) end) - if not suspended then - timer_die:start() - end - notification.timer = timer_die - end - notification.die = die -end - --- Set new notification timeout. +-- +-- This function is deprecated, use `notification:reset_timeout(new_timeout)`. +-- -- @tparam notification notification Notification object, which timer is to be reset. -- @tparam number new_timeout Time in seconds after which notification disappears. --- @return None. +-- @deprecated naughty.reset_timeout function naughty.reset_timeout(notification, new_timeout) - if notification.timer then notification.timer:stop() end + gdebug.deprecate("Use notification:reset_timeout(new_timeout)", {deprecated_in=5}) - local timeout = new_timeout or notification.timeout - set_timeout(notification, timeout) - notification.timeout = timeout + if not notification then return end - notification.timer:start() -end - ---- Escape and set title and text for notification object. --- @tparam notification notification Notification object. --- @tparam string title Title of notification. --- @tparam string text Main text of notification. --- @return None. -local function set_text(notification, title, text) - local escape_pattern = "[<>&]" - local escape_subs = { ['<'] = "<", ['>'] = ">", ['&'] = "&" } - - local textbox = notification.textbox - - local function setMarkup(pattern, replacements) - return textbox:set_markup_silently(string.format('%s%s', title, text:gsub(pattern, replacements))) - end - local function setText() - textbox:set_text(string.format('%s %s', title, text)) - end - - -- Since the title cannot contain markup, it must be escaped first so that - -- it is not interpreted by Pango later. - title = title:gsub(escape_pattern, escape_subs) - -- Try to set the text while only interpreting
. - if not setMarkup("", "\n") then - -- That failed, escape everything which might cause an error from pango - if not setMarkup(escape_pattern, escape_subs) then - -- Ok, just ignore all pango markup. If this fails, we got some invalid utf8 - if not pcall(setText) then - textbox:set_markup("<Invalid markup or UTF8, cannot display message>") - end - end - end -end - -local function update_size(notification) - - local n = notification - local s = n.size_info - local width = s.width - local height = s.height - local margin = s.margin - - -- calculate the width - if not width then - local w, _ = n.textbox:get_preferred_size(n.screen) - width = w + (n.iconbox and s.icon_w + 2 * margin or 0) + 2 * margin - end - - if width < s.actions_max_width then - width = s.actions_max_width - end - - -- calculate the height - if not height then - local w = width - (n.iconbox and s.icon_w + 2 * margin or 0) - 2 * margin - local h = n.textbox:get_height_for_width(w, n.screen) - if n.iconbox and s.icon_h + 2 * margin > h + 2 * margin then - height = s.icon_h + 2 * margin - else - height = h + 2 * margin - end - end - - height = height + s.actions_total_height - - -- crop to workarea size if too big - local workarea = n.screen.workarea - local border_width = s.border_width or 0 - local padding = naughty.config.padding or 0 - if width > workarea.width - 2*border_width - 2*padding then - width = workarea.width - 2*border_width - 2*padding - end - if height > workarea.height - 2*border_width - 2*padding then - height = workarea.height - 2*border_width - 2*padding - end - - -- set size in notification object - n.height = height + 2*border_width - n.width = width + 2*border_width - local offset = get_offset(n.screen, n.position, n.idx, n.width, n.height) - n.box:geometry({ - width = width, - height = height, - x = offset.x, - y = offset.y, - }) - n.idx = offset.idx - - -- update positions of other notifications - arrange(n.screen) + notification:reset_timeout(new_timeout) end --- Replace title and text of an existing notification. +-- +-- This function is deprecated, use `notification.text = new_text` and +-- `notification.title = new_title` +-- -- @tparam notification notification Notification object, which contents are to be replaced. -- @tparam string new_title New title of notification. If not specified, old title remains unchanged. -- @tparam string new_text New text of notification. If not specified, old text remains unchanged. -- @return None. +-- @deprecated naughty.replace_text function naughty.replace_text(notification, new_title, new_text) - local title = new_title + gdebug.deprecate( + "Use notification.text = new_text; notification.title = new_title", + {deprecated_in=5} + ) - if title then title = title .. "\n" else title = "" end + if not notification then return end - set_text(notification, title, new_text) - update_size(notification) + notification.title = new_title or notification.title + notification.text = new_text or notification.text +end + +-- Remove the notification from the internal list(s) +local function cleanup(self, reason) + if suspended then + for k, v in pairs(naughty.suspended) do + if v == self then + table.remove(naughty.suspended, k) + break + end + end + end + local scr = self.screen + + assert(naughty.notifications[scr][self.position][self.idx] == self) + table.remove(naughty.notifications[scr][self.position], self.idx) + + -- Update all indices + for k, n in ipairs(naughty.notifications[scr][self.position]) do + n.idx = k + end + + if self.timer then + self.timer:stop() + end + + if self.destroy_cb and reason ~= naughty.notification_closed_reason.silent then + self.destroy_cb(reason or naughty.notification_closed_reason.undefined) + end +end + +naughty.connect_signal("destroyed", cleanup) + +local function get_screen(s) + return s and capi.screen[s] +end + +-- Proxy the global suspension state on all notification objects +local function get_suspended(self) + return suspended and not self.ignore_suspend +end + +--- Emitted when a notification is created. +-- @signal added +-- @tparam naughty.notification notification The notification object + +--- Emitted when a notification is destroyed. +-- @signal destroyed +-- @tparam naughty.notification notification The notification object + +--- Emitted when a notification has to be displayed. +-- +-- To add an handler, use: +-- +-- naughty.connect_signal("request::display", function(notification, args) +-- -- do something +-- end) +-- +-- @tparam table notification The `naughty.notification` object. +-- @tparam table args Any arguments passed to the `naughty.notify` function, +-- including, but not limited to, all `naughty.notification` properties. +-- @signal request::display + +--- Emitted when a notification needs pre-display configuration. +-- +-- @tparam table notification The `naughty.notification` object. +-- @tparam table args Any arguments passed to the `naughty.notify` function, +-- including, but not limited to, all `naughty.notification` properties. +-- @signal request::preset + + + +-- 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()) + + -- insert the notification to the table + table.insert(naughty.notifications[s][notification.position], notification) + notification.idx = #naughty.notifications[s][notification.position] + notification.screen = s + + if suspended and not args.ignore_suspend then + table.insert(naughty.notifications.suspended, notification) + end + + naughty.emit_signal("added", notification, args) + + assert(rawget(notification, "preset")) + + -- return the notification + return notification +end + +naughty.connect_signal("new", register) + +local function index_miss(_, key) + if key == "suspended" then + return suspended + else + return nil + end +end + +local function set_index_miss(_, key, value) + if key == "suspended" then + assert(type(value) == "boolean") + suspended = value + if value then + resume() + end + else + rawset(naughty, key, value) + end end --- Create a notification. -- +-- This function is deprecated, create notification objects instead: +-- +-- local notif = naughty.notification(args) +-- -- @tab args The argument table containing any of the arguments below. -- @string[opt=""] args.text Text of the notification. -- @string[opt] args.title Title of the notification. @@ -621,7 +487,7 @@ end -- @tparam[opt] func args.run Function to run on left click. The notification -- object will be passed to it as an argument. -- You need to call e.g. --- `notification.die(naughty.notificationClosedReason.dismissedByUser)` from +-- `notification.die(naughty.notification_closed_reason.dismissedByUser)` from -- there to dismiss the notification yourself. -- @tparam[opt] func args.destroy Function to run when notification is destroyed. -- @tparam[opt] table args.preset Table with any of the above parameters. @@ -638,250 +504,22 @@ end -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 }) -- @treturn ?table The notification object, or nil in case a notification was -- not displayed. +-- @deprecated naughty.notify + +local nnotif = nil + function naughty.notify(args) - if naughty.config.notify_callback then - args = naughty.config.notify_callback(args) - if not args then return end - end + gdebug.deprecate( + "Use local notif = naughty.notification(args)", + {deprecated_in=5} + ) - -- gather variables together - local preset = gtable.join(naughty.config.defaults or {}, - args.preset or naughty.config.presets.normal or {}) - 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 text = args.text or preset.text - local title = args.title or preset.title - local s = get_screen(args.screen or preset.screen or screen.focused()) - if not s then - local err = "naughty.notify: there is no screen available to display the following notification:" - err = string.format("%s title='%s' text='%s'", err, tostring(title or ""), tostring(text or "")) - require("gears.debug").print_warning(err) - return - end - 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 + --TODO v6 remove this hack + nnotif = nnotif or require("naughty.notification") - -- 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 margin = args.margin or preset.margin or - beautiful.notification_margin - local opacity = args.opacity or preset.opacity or - beautiful.notification_opacity - local notification = { screen = s, destroy_cb = destroy_cb, timeout = timeout } - - -- replace notification if needed - local reuse_box - if args.replaces_id then - local obj = naughty.getById(args.replaces_id) - if obj then - -- destroy this and ... - naughty.destroy(obj, naughty.notificationClosedReason.silent, true) - reuse_box = obj.box - end - -- ... may use its ID - if args.replaces_id <= counter then - notification.id = args.replaces_id - else - counter = counter + 1 - notification.id = counter - end - else - -- get a brand new ID - counter = counter + 1 - notification.id = counter - end - - notification.position = position - - if title then title = title .. "\n" else title = "" end - - -- hook destroy - set_timeout(notification, timeout) - local die = notification.die - - local run = function () - if args.run then - args.run(notification) - else - die(naughty.notificationClosedReason.dismissedByUser) - end - end - - local hover_destroy = function () - if hover_timeout == 0 then - die(naughty.notificationClosedReason.expired) - else - if notification.timer then notification.timer:stop() end - notification.timer = timer { timeout = hover_timeout } - notification.timer:connect_signal("timeout", function() die(naughty.notificationClosedReason.expired) end) - notification.timer:start() - end - end - - -- create textbox - local textbox = wibox.widget.textbox() - local marginbox = wibox.container.margin() - marginbox:set_margins(margin) - marginbox:set_widget(textbox) - textbox:set_valign("middle") - textbox:set_font(font) - - notification.textbox = textbox - - set_text(notification, title, text) - - local actionslayout = wibox.layout.fixed.vertical() - local actions_max_width = 0 - local actions_total_height = 0 - if actions then - for action, callback in pairs(actions) do - local actiontextbox = wibox.widget.textbox() - local actionmarginbox = wibox.container.margin() - actionmarginbox:set_margins(margin) - actionmarginbox:set_widget(actiontextbox) - actiontextbox:set_valign("middle") - actiontextbox:set_font(font) - actiontextbox:set_markup(string.format('☛ %s', action)) - -- 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) - )) - actionslayout:add(actionmarginbox) - - actions_total_height = actions_total_height + action_height - if actions_max_width < action_width then - actions_max_width = action_width - end - end - end - - -- create iconbox - local iconbox = nil - local iconmargin = nil - local icon_w, icon_h = 0, 0 - if icon then - -- Is this really an URI instead of a path? - if type(icon) == "string" and string.sub(icon, 1, 7) == "file://" then - icon = string.sub(icon, 8) - -- urldecode URI path - icon = string.gsub(icon, "%%(%x%x)", function(x) return string.char(tonumber(x, 16)) end ) - end - -- try to guess icon if the provided one is non-existent/readable - if type(icon) == "string" and not gfs.file_readable(icon) then - icon = util.geticonpath(icon, naughty.config.icon_formats, naughty.config.icon_dirs, icon_size) or icon - end - - -- is the icon file readable? - icon = surface.load_uncached(icon) - - -- if we have an icon, use it - if icon then - iconbox = wibox.widget.imagebox() - iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin) - if icon_size then - local scaled = cairo.ImageSurface(cairo.Format.ARGB32, icon_size, icon_size) - local cr = cairo.Context(scaled) - cr:scale(icon_size / icon:get_height(), icon_size / icon:get_width()) - cr:set_source_surface(icon, 0, 0) - cr:paint() - icon = scaled - end - iconbox:set_resize(false) - iconbox:set_image(icon) - icon_w = icon:get_width() - icon_h = icon:get_height() - end - end - notification.iconbox = iconbox - - -- create container wibox - notification.box = wibox({ fg = fg, - bg = bg, - border_color = border_color, - border_width = border_width, - shape_border_color = shape and border_color, - shape_border_width = shape and border_width, - shape = shape, - type = "notification" }) - - if reuse_box then - notification.box = reuse_box - end - - if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end - - notification.size_info = { - width = width, - height = height, - icon_w = icon_w, - icon_h = icon_h, - margin = margin, - border_width = border_width, - actions_max_width = actions_max_width, - actions_total_height = actions_total_height, - } - - -- position the wibox - update_size(notification) - notification.box.ontop = ontop - notification.box.opacity = opacity - notification.box.visible = true - - -- populate widgets - local layout = wibox.layout.fixed.horizontal() - if iconmargin then - layout:add(iconmargin) - end - layout:add(marginbox) - - local completelayout = wibox.layout.fixed.vertical() - completelayout:add(layout) - completelayout:add(actionslayout) - notification.box:set_widget(completelayout) - - -- Setup the mouse events - layout:buttons(gtable.join(button({}, 1, nil, run), - button({}, 3, nil, function() - die(naughty.notificationClosedReason.dismissedByUser) - end))) - - -- insert the notification to the table - table.insert(naughty.notifications[s][notification.position], notification) - - if suspended and not args.ignore_suspend then - notification.box.visible = false - table.insert(naughty.notifications.suspended, notification) - end - - -- return the notification - return notification + return nnotif(args) end -return naughty +return setmetatable(naughty, {__index = index_miss, __newindex = set_index_miss}) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/naughty/dbus.lua b/lib/naughty/dbus.lua index bb9fcfc50..1d89a0d62 100644 --- a/lib/naughty/dbus.lua +++ b/lib/naughty/dbus.lua @@ -25,11 +25,14 @@ local tcat = table.concat local tins = table.insert local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) local naughty = require("naughty.core") +local cst = require("naughty.constants") +local nnotif = require("naughty.notification") --- Notification library, dbus bindings local dbus = { config = {} } -- DBUS Notification constants +-- https://developer.gnome.org/notification-spec/#urgency-levels local urgency = { low = "\0", normal = "\1", @@ -46,9 +49,9 @@ local urgency = { -- @tfield table 3 critical urgency -- @table config.mapping dbus.config.mapping = { - {{urgency = urgency.low}, naughty.config.presets.low}, - {{urgency = urgency.normal}, naughty.config.presets.normal}, - {{urgency = urgency.critical}, naughty.config.presets.critical} + {{urgency = urgency.low}, cst.config.presets.low}, + {{urgency = urgency.normal}, cst.config.presets.normal}, + {{urgency = urgency.critical}, cst.config.presets.critical} } local function sendActionInvoked(notificationId, action) @@ -140,7 +143,7 @@ capi.dbus.connect_signal("org.freedesktop.Notifications", args.preset = gtable.join(args.preset, preset) end end - local preset = args.preset or naughty.config.defaults + local preset = args.preset or cst.config.defaults local notification if actions then args.actions = {} @@ -152,12 +155,12 @@ capi.dbus.connect_signal("org.freedesktop.Notifications", if action_id == "default" then args.run = function() sendActionInvoked(notification.id, "default") - naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser) + notification:destroy(cst.notification_closed_reason.dismissed_by_user) end elseif action_id ~= nil and action_text ~= nil then args.actions[action_text] = function() sendActionInvoked(notification.id, action_id) - naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser) + notification:destroy(cst.notification_closed_reason.dismissed_by_user) end end end @@ -190,14 +193,14 @@ capi.dbus.connect_signal("org.freedesktop.Notifications", args.timeout = expire / 1000 end args.freedesktop_hints = hints - notification = naughty.notify(args) + notification = nnotif(args) return "u", notification.id end return "u", "0" elseif data.member == "CloseNotification" then - local obj = naughty.getById(appname) + local obj = naughty.get_by_id(appname) if obj then - naughty.destroy(obj, naughty.notificationClosedReason.dismissedByCommand) + obj:destroy(cst.notification_closed_reason.dismissed_by_command) end elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then -- name of notification app, name of vender, version, specification version diff --git a/lib/naughty/init.lua b/lib/naughty/init.lua index 71dee9fcf..7b7325542 100644 --- a/lib/naughty/init.lua +++ b/lib/naughty/init.lua @@ -9,6 +9,9 @@ if dbus then naughty.dbus = require("naughty.dbus") end +naughty.layout = require("naughty.layout") +naughty.notification = require("naughty.notification") + return naughty -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/naughty/layout/init.lua b/lib/naughty/layout/init.lua new file mode 100644 index 000000000..c6df968a1 --- /dev/null +++ b/lib/naughty/layout/init.lua @@ -0,0 +1,9 @@ +--------------------------------------------------------------------------- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2017 Emmanuel Lepage Vallee +-- @module naughty.layout +--------------------------------------------------------------------------- + +return { + legacy = require("naughty.layout.legacy") +} diff --git a/lib/naughty/layout/legacy.lua b/lib/naughty/layout/legacy.lua new file mode 100644 index 000000000..a757ff3a7 --- /dev/null +++ b/lib/naughty/layout/legacy.lua @@ -0,0 +1,544 @@ +---------------------------------------------------------------------------- +--- A notification popup widget. +-- +-- This is the legacy notification widget. It was the default until Awesome +-- v4.3 but is now being deprecated in favor of a more flexible widget. +-- +-- The reason for this is/was that this widget is inflexible and mutate the +-- state of the notification object in a way that hinder other notification +-- widgets. +-- +-- If no other notification widget is specified, Awesome fallback to this +-- widget. +-- +--@DOC_naughty_actions_EXAMPLE@ +-- +-- @author koniu <gkusnierz@gmail.com> +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2008 koniu +-- @copyright 2017 Emmanuel Lepage Vallee +-- @classmod naughty.layout.legacy +---------------------------------------------------------------------------- + +local capi = { screen = screen, awesome = awesome } +local naughty = require("naughty.core") +local screen = require("awful.screen") +local button = require("awful.button") +local beautiful = require("beautiful") +local surface = require("gears.surface") +local gtable = require("gears.table") +local wibox = require("wibox") +local gfs = require("gears.filesystem") +local timer = require("gears.timer") +local gmath = require("gears.math") +local cairo = require("lgi").cairo +local util = require("awful.util") + +local function get_screen(s) + return s and capi.screen[s] +end + +-- This is a copy of the table found in `naughty.core`. The reason the copy +-- exists is to make sure there is only unidirectional coupling between the +-- legacy widget (this class) and `naughty.core`. Exposing the "raw" +-- notification list is also a bad design and might cause indices and position +-- corruption. While it cannot be removed from the public API (yet), it can at +-- least be blacklisted internally. +local current_notifications = setmetatable({}, {__mode = "k"}) + +screen.connect_for_each_screen(function(s) + current_notifications[s] = { + top_left = {}, + top_middle = {}, + top_right = {}, + bottom_left = {}, + bottom_middle = {}, + bottom_right = {}, + } +end) + +-- Counter for the notifications +-- Required for later access via DBUS +local counter = 1 + +--- Evaluate desired position of the notification by index - internal +-- +-- @param s Screen to use +-- @param position top_right | top_left | bottom_right | bottom_left +-- | top_middle | bottom_middle +-- @param idx Index of the notification +-- @param[opt] width Popup width. +-- @param height Popup height +-- @return Absolute position and index in { x = X, y = Y, idx = I } table +local function get_offset(s, position, idx, width, height) + s = get_screen(s) + local ws = s.workarea + local v = {} + idx = idx or #current_notifications[s][position] + 1 + width = width or current_notifications[s][position][idx].width + + -- calculate x + if position:match("left") then + v.x = ws.x + naughty.config.padding + elseif position:match("middle") then + v.x = ws.x + (ws.width / 2) - (width / 2) + else + v.x = ws.x + ws.width - (width + naughty.config.padding) + end + + -- calculate existing popups' height + local existing = 0 + for i = 1, idx-1, 1 do + local n = current_notifications[s][position][i] + + -- `n` will not nil when there is too many notifications to fit in `s` + if n then + existing = existing + n.height + naughty.config.spacing + end + end + + -- calculate y + if position:match("top") then + v.y = ws.y + naughty.config.padding + existing + else + v.y = ws.y + ws.height - (naughty.config.padding + height + existing) + end + + -- Find old notification to replace in case there is not enough room. + -- This tries to skip permanent notifications (without a timeout), + -- e.g. critical ones. + local find_old_to_replace = function() + for i = 1, idx-1 do + local n = current_notifications[s][position][i] + if n.timeout > 0 then + return n + end + end + -- Fallback to first one. + return current_notifications[s][position][1] + end + + -- if positioned outside workarea, destroy oldest popup and recalculate + if v.y + height > ws.y + ws.height or v.y < ws.y then + local n = find_old_to_replace() + if n then + n:destroy(naughty.notification_closed_reason.too_many_on_screen) + end + v = get_offset(s, position, idx, width, height) + end + + return v +end + +local escape_pattern = "[<>&]" +local escape_subs = { ['<'] = "<", ['>'] = ">", ['&'] = "&" } + +-- Cache the markup +local function set_escaped_text(self) + local text, title = self.text or "", self.title or "" + + if title then title = title .. "\n" else title = "" end + + local textbox = self.textbox + + local function set_markup(pattern, replacements) + return textbox:set_markup_silently(string.format('%s%s', title, text:gsub(pattern, replacements))) + end + + local function set_text() + textbox:set_text(string.format('%s %s', title, text)) + end + + -- Since the title cannot contain markup, it must be escaped first so that + -- it is not interpreted by Pango later. + title = title:gsub(escape_pattern, escape_subs) + -- Try to set the text while only interpreting
. + if not set_markup("", "\n") then + -- That failed, escape everything which might cause an error from pango + if not set_markup(escape_pattern, escape_subs) then + -- Ok, just ignore all pango markup. If this fails, we got some invalid utf8 + if not pcall(set_text) then + textbox:set_markup("<Invalid markup or UTF8, cannot display message>") + end + end + end +end + +naughty.connect_signal("property::text" ,set_escaped_text) +naughty.connect_signal("property::title",set_escaped_text) + + +--- Re-arrange notifications according to their position and index - internal +-- +-- @return None +local function arrange(s) + -- {} in case the screen has been deleted + for p in pairs(current_notifications[s] or {}) do + for i,notification in pairs(current_notifications[s][p]) do + local offset = get_offset(s, p, i, notification.width, notification.height) + notification.box:geometry({ x = offset.x, y = offset.y }) + end + end +end + +local function update_size(notification) + local n = notification + local s = n.size_info + local width = s.width + local height = s.height + local margin = s.margin + + -- calculate the width + if not width then + local w, _ = n.textbox:get_preferred_size(n.screen) + width = w + (n.iconbox and s.icon_w + 2 * margin or 0) + 2 * margin + end + + if width < s.actions_max_width then + width = s.actions_max_width + end + + if s.max_width then + width = math.min(width, s.max_width) + end + + + -- calculate the height + if not height then + local w = width - (n.iconbox and s.icon_w + 2 * margin or 0) - 2 * margin + local h = n.textbox:get_height_for_width(w, n.screen) + if n.iconbox and s.icon_h + 2 * margin > h + 2 * margin then + height = s.icon_h + 2 * margin + else + height = h + 2 * margin + end + end + + height = height + s.actions_total_height + + if s.max_height then + height = math.min(height, s.max_height) + end + + -- crop to workarea size if too big + local workarea = n.screen.workarea + local border_width = s.border_width or 0 + local padding = naughty.config.padding or 0 + if width > workarea.width - 2*border_width - 2*padding then + width = workarea.width - 2*border_width - 2*padding + end + if height > workarea.height - 2*border_width - 2*padding then + height = workarea.height - 2*border_width - 2*padding + end + + -- set size in notification object + n.height = height + 2*border_width + n.width = width + 2*border_width + local offset = get_offset(n.screen, n.position, n.idx, n.width, n.height) + n.box:geometry({ + width = width, + height = height, + x = offset.x, + y = offset.y, + }) + + -- update positions of other notifications + arrange(n.screen) +end + + +local function cleanup(self, _ --[[reason]], keep_visible) + -- It is not a legacy notification + if not self.box then return end + + local scr = self.screen + + assert(current_notifications[scr][self.position][self.idx] == self) + table.remove(current_notifications[scr][self.position], self.idx) + + if (not keep_visible) or (not scr) then + self.box.visible = false + end + + arrange(scr) +end + +naughty.connect_signal("destroyed", cleanup) + +--- The default notification GUI handler. +-- +-- To disable this handler, use: +-- +-- naughty.disconnect_signal( +-- "request::display", naughty.default_notification_handler +-- ) +-- +-- It looks like: +-- +--@DOC_naughty_actions_EXAMPLE@ +-- +-- @tparam table notification The `naughty.notification` object. +-- @tparam table args Any arguments passed to the `naughty.notify` function, +-- including, but not limited to all `naughty.notification` properties. +-- @signalhandler naughty.default_notification_handler +function naughty.default_notification_handler(notification, args) + + -- If request::display is called more than once, simply make sure the wibox + -- is visible. + if notification.box then + notification.box.visible = true + return + end + + local preset = notification.preset + local text = args.text or preset.text + local title = args.title or preset.title + local s = get_screen(args.screen or preset.screen or screen.focused()) + + if not s then + local err = "naughty.notify: there is no screen available to display the following notification:" + err = string.format("%s title='%s' text='%s'", err, tostring(title or ""), tostring(text or "")) + require("gears.debug").print_warning(err) + 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 + + 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 + + -- replace notification if needed + local reuse_box + if args.replaces_id then + local obj = naughty.get_by_id(args.replaces_id) + if obj then + -- destroy this and ... + naughty.destroy(obj, naughty.notification_closed_reason.silent, true) + reuse_box = obj.box + end + -- ... may use its ID + if args.replaces_id <= counter then + notification.id = args.replaces_id + else + counter = counter + 1 + notification.id = counter + end + else + -- get a brand new ID + counter = counter + 1 + notification.id = counter + end + + notification.position = position + + -- hook destroy + notification.timeout = timeout + local die = notification.die + + local run = function () + if args.run then + args.run(notification) + else + die(naughty.notification_closed_reason.dismissed_by_user) + end + end + + local hover_destroy = function () + if hover_timeout == 0 then + die(naughty.notification_closed_reason.expired) + else + if notification.timer then notification.timer:stop() end + notification.timer = timer { timeout = hover_timeout } + notification.timer:connect_signal("timeout", function() die(naughty.notification_closed_reason.expired) end) + notification.timer:start() + end + end + + -- create textbox + local textbox = wibox.widget.textbox() + local marginbox = wibox.container.margin() + marginbox:set_margins(margin) + marginbox:set_widget(textbox) + textbox:set_valign("middle") + textbox:set_font(font) + + notification.textbox = textbox + set_escaped_text(notification) + + local actionslayout = wibox.layout.fixed.vertical() + local actions_max_width = 0 + local actions_total_height = 0 + if actions then + for action, callback in pairs(actions) do + local actiontextbox = wibox.widget.textbox() + local actionmarginbox = wibox.container.margin() + actionmarginbox:set_margins(margin) + actionmarginbox:set_widget(actiontextbox) + actiontextbox:set_valign("middle") + actiontextbox:set_font(font) + actiontextbox:set_markup(string.format('☛ %s', action)) + -- 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) + )) + actionslayout:add(actionmarginbox) + + actions_total_height = actions_total_height + action_height + if actions_max_width < action_width then + actions_max_width = action_width + end + end + end + + local size_info = { + width = width, + height = height, + max_width = max_width, + max_height = max_height, + margin = margin, + border_width = border_width, + actions_max_width = actions_max_width, + actions_total_height = actions_total_height, + } + + -- create iconbox + local iconbox = nil + local iconmargin = nil + if icon then + -- Is this really an URI instead of a path? + if type(icon) == "string" and string.sub(icon, 1, 7) == "file://" then + icon = string.sub(icon, 8) + -- urldecode URI path + icon = string.gsub(icon, "%%(%x%x)", function(x) return string.char(tonumber(x, 16)) end ) + end + -- try to guess icon if the provided one is non-existent/readable + if type(icon) == "string" and not gfs.file_readable(icon) then + icon = util.geticonpath(icon, naughty.config.icon_formats, naughty.config.icon_dirs, icon_size) or icon + end + + -- is the icon file readable? + icon = surface.load_uncached(icon) + + -- if we have an icon, use it + if icon then + iconbox = wibox.widget.imagebox() + iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin) + + if icon_size and (icon:get_height() > icon_size or icon:get_width() > icon_size) then + size_info.icon_scale_factor = icon_size / math.max(icon:get_height(), + icon:get_width()) + + size_info.icon_w = icon:get_width () * size_info.icon_scale_factor + size_info.icon_h = icon:get_height() * size_info.icon_scale_factor + + local scaled = + cairo.ImageSurface(cairo.Format.ARGB32, + gmath.round(size_info.icon_w), + gmath.round(size_info.icon_h)) + + local cr = cairo.Context(scaled) + cr:scale(size_info.icon_scale_factor, size_info.icon_scale_factor) + cr:set_source_surface(icon, 0, 0) + cr:paint() + icon = scaled + else + size_info.icon_w = icon:get_width () + size_info.icon_h = icon:get_height() + end + iconbox:set_resize(false) + iconbox:set_image(icon) + end + end + notification.iconbox = iconbox + + -- create container wibox + notification.box = wibox({ fg = fg, + bg = bg, + border_color = border_color, + border_width = border_width, + shape_border_color = shape and border_color, + shape_border_width = shape and border_width, + shape = shape, + type = "notification" }) + + if reuse_box then + notification.box = reuse_box + end + + if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end + + notification.size_info = size_info + + -- position the wibox + update_size(notification) + notification.box.ontop = ontop + notification.box.opacity = opacity + notification.box.visible = true + + -- populate widgets + local layout = wibox.layout.fixed.horizontal() + if iconmargin then + layout:add(iconmargin) + end + layout:add(marginbox) + + local completelayout = wibox.layout.fixed.vertical() + completelayout:add(layout) + completelayout:add(actionslayout) + notification.box:set_widget(completelayout) + + -- Setup the mouse events + layout:buttons(gtable.join(button({}, 1, nil, run), + button({}, 3, nil, function() + die(naughty.notification_closed_reason.dismissed_by_user) + end))) + + -- insert the notification to the table + table.insert(current_notifications[s][notification.position], notification) + + if naughty.suspended and not args.ignore_suspend then + notification.box.visible = false + end +end + +naughty.connect_signal("request::display", naughty.default_notification_handler) diff --git a/lib/naughty/notification.lua b/lib/naughty/notification.lua new file mode 100644 index 000000000..138a99150 --- /dev/null +++ b/lib/naughty/notification.lua @@ -0,0 +1,416 @@ +--------------------------------------------------------------------------- +--- A notification object. +-- +-- This class creates individual notification objects that can be manipulated +-- to extend the default behavior. +-- +-- This class doesn't define the actual widget, but is rather intended as a data +-- object to hold the properties. All examples assume the default widgets, but +-- the whole implementation can be replaced. +-- +--@DOC_naughty_actions_EXAMPLE@ +-- +-- @author Emmanuel Lepage Vallee +-- @copyright 2008 koniu +-- @copyright 2017 Emmanuel Lepage Vallee +-- @classmod 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 notification = {} + +--- Notifications font. +-- @beautiful beautiful.notification_font +-- @tparam string|lgi.Pango.FontDescription notification_font + +--- Notifications background color. +-- @beautiful beautiful.notification_bg +-- @tparam color notification_bg + +--- Notifications foreground color. +-- @beautiful beautiful.notification_fg +-- @tparam color notification_fg + +--- Notifications border width. +-- @beautiful beautiful.notification_border_width +-- @tparam int notification_border_width + +--- Notifications border color. +-- @beautiful beautiful.notification_border_color +-- @tparam color notification_border_color + +--- Notifications shape. +-- @beautiful beautiful.notification_shape +-- @tparam[opt] gears.shape notification_shape +-- @see gears.shape + +--- Notifications opacity. +-- @beautiful beautiful.notification_opacity +-- @tparam[opt] int notification_opacity + +--- Notifications margin. +-- @beautiful beautiful.notification_margin +-- @tparam int notification_margin + +--- Notifications width. +-- @beautiful beautiful.notification_width +-- @tparam int notification_width + +--- Notifications height. +-- @beautiful beautiful.notification_height +-- @tparam int notification_height + +--- Unique identifier of the notification. +-- This is the equivalent to a PID as allows external applications to select +-- notifications. +-- @property text +-- @param string +-- @see title + +--- Text of the notification. +-- @property text +-- @param string +-- @see title + +--- Title of the notification. +--@DOC_naughty_helloworld_EXAMPLE@ +-- @property title +-- @param string + +--- Time in seconds after which popup expires. +-- Set 0 for no timeout. +-- @property timeout +-- @param number + +--- Delay in seconds after which hovered popup disappears. +-- @property hover_timeout +-- @param number + +--- Target screen for the notification. +-- @property screen +-- @param screen + +--- Corner of the workarea displaying the popups. +-- +-- The possible values are: +-- +-- * *top_right* +-- * *top_left* +-- * *bottom_left* +-- * *bottom_right* +-- * *top_middle* +-- * *bottom_middle* +-- +--@DOC_awful_notification_corner_EXAMPLE@ +-- +-- @property position +-- @param string + +--- Boolean forcing popups to display on top. +-- @property ontop +-- @param boolean + +--- Popup height. +-- @property height +-- @param number + +--- Popup width. +-- @property width +-- @param number + +--- Notification font. +--@DOC_naughty_colors_EXAMPLE@ +-- @property font +-- @param string + +--- Path to icon. +-- @property icon +-- @tparam string|surface icon + +--- Desired icon size in px. +-- @property icon_size +-- @param number + +--- Foreground color. +-- @property fg +-- @tparam string|color|pattern fg +-- @see title +-- @see gears.color + +--- Background color. +-- @property bg +-- @tparam string|color|pattern bg +-- @see title +-- @see gears.color + +--- Border width. +-- @property border_width +-- @param number +-- @see title + +--- Border color. +-- @property border_color +-- @param string +-- @see title +-- @see gears.color + +--- Widget shape. +--@DOC_naughty_shape_EXAMPLE@ +-- @property shape + +--- Widget opacity. +-- @property opacity +-- @param number From 0 to 1 + +--- Widget margin. +-- @property margin +-- @tparam number|table margin +-- @see shape + +--- Function to run on left click. +-- @property run +-- @param function + +--- Function to run when notification is destroyed. +-- @property destroy +-- @param function + +--- Table with any of the above parameters. +-- args will override ones defined +-- in the preset. +-- @property preset +-- @param table + +--- Replace the notification with the given ID. +-- @property replaces_id +-- @param number + +--- 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. +-- @property callback +-- @param function + +--- A table containing strings that represents actions to buttons. +-- +-- The table key (a number) is used by DBus to set map the action. +-- +-- @property actions +-- @param table + +--- Ignore this notification, do not display. +-- +-- Note that this property has to be set in a `preset` or in a `request::preset` +-- handler. +-- +-- @property ignore +-- @param boolean + +--- Tell if the notification is currently suspended (read only). +-- +-- This is always equal to `naughty.suspended` +--@property suspended +--@param boolean + +--- Emitted when the notification is destroyed. +-- @signal destroyed + +-- . --FIXME needs a description +-- @property ignore_suspend If set to true this notification +-- will be shown even if notifications are suspended via `naughty.suspend`. + +--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 +-- +-- @tparam string reason One of the reasons from `notification_closed_reason` +-- @tparam[opt=false] boolean keep_visible If true, keep the notification visible +-- @return True if the popup was successfully destroyed, nil otherwise +function notification:destroy(reason, keep_visible) + self:emit_signal("destroyed") + + return true +end + +--- Set new notification timeout. +-- @tparam number new_timeout Time in seconds after which notification disappears. +function notification:reset_timeout(new_timeout) + if self.timer then self.timer:stop() end + + local timeout = new_timeout or self.timeout + self:set_timeout(self, timeout) + self.timeout = timeout + + self.timer:start() +end + +function notification:set_id(new_id) + assert(self._private.id == nil, "Notification identifier can only be set once") + self._private.id = new_id + self:emit_signal("property::id", new_id) +end + +function notification:set_timeout(timeout) + local die = function (reason) + self:destroy(reason) + end + + if self.timer and self._private.timeout == timeout then return end + + if timeout > 0 then + local timer_die = timer { timeout = timeout } + timer_die:connect_signal("timeout", function() die(cst.notification_closed_reason.expired) end) + if not self.suspended then --FIXME there's still a dependency loop to fix before it works + timer_die:start() + end + + -- Prevent a memory leak and the accumulation of active timers + if self.timer and self.timer.started then + self.timer:stop() + end + + self.timer = timer_die + end + self.die = die + self._private.timeout = timeout +end + +local properties = { + "text" , "title" , "timeout" , "hover_timeout" , + "screen" , "position", "ontop" , "border_width" , + "width" , "font" , "icon" , "icon_size" , + "fg" , "bg" , "height" , "border_color" , + "shape" , "opacity" , "margin" , "ignore_suspend", + "destroy" , "preset" , "callback", "replaces_id" , + "actions" , "run" , "id" , "ignore" , +} + +for _, prop in ipairs(properties) do + notification["get_"..prop] = notification["get_"..prop] or function(self) + -- It's possible this could be called from the `request::preset` handler. + -- `rawget()` is necessary to avoid a stack overflow. + local preset = rawget(self, "preset") + + return self._private[prop] + or (preset and preset[prop]) + or cst.config.defaults[prop] + end + + notification["set_"..prop] = notification["set_"..prop] or function(self, value) + self._private[prop] = value + self:emit_signal("property::"..prop, value) + return + end + +end + +--- Create a notification. +-- +-- @tab args The argument table containing any of the arguments below. +-- @string[opt=""] args.text Text of the notification. +-- @string[opt] args.title Title of the notification. +-- @int[opt=5] args.timeout Time in seconds after which popup expires. +-- Set 0 for no timeout. +-- @int[opt] args.hover_timeout Delay in seconds after which hovered popup disappears. +-- @tparam[opt=focused] integer|screen args.screen Target screen for the notification. +-- @string[opt="top_right"] args.position Corner of the workarea displaying the popups. +-- Values: `"top_right"`, `"top_left"`, `"bottom_left"`, +-- `"bottom_right"`, `"top_middle"`, `"bottom_middle"`. +-- @bool[opt=true] args.ontop Boolean forcing popups to display on top. +-- @int[opt=`beautiful.notification_height` or auto] args.height Popup height. +-- @int[opt=`beautiful.notification_width` or auto] args.width Popup width. +-- @string[opt=`beautiful.notification_font` or `beautiful.font` or `awesome.font`] args.font Notification font. +-- @string[opt] args.icon Path to icon. +-- @int[opt] args.icon_size Desired icon size in px. +-- @string[opt=`beautiful.notification_fg` or `beautiful.fg_focus` or `'#ffffff'`] args.fg Foreground color. +-- @string[opt=`beautiful.notification_fg` or `beautiful.bg_focus` or `'#535d6c'`] args.bg Background color. +-- @int[opt=`beautiful.notification_border_width` or 1] args.border_width Border width. +-- @string[opt=`beautiful.notification_border_color` or +-- `beautiful.border_focus` or `'#535d6c'`] args.border_color Border color. +-- @tparam[opt=`beautiful.notification_shape`] gears.shape args.shape Widget shape. +-- @tparam[opt=`beautiful.notification_opacity`] gears.opacity args.opacity Widget opacity. +-- @tparam[opt=`beautiful.notification_margin`] gears.margin args.margin Widget margin. +-- @tparam[opt] func args.run Function to run on left click. The notification +-- object will be passed to it as an argument. +-- You need to call e.g. +-- `notification.die(naughty.notification_closed_reason.dismissedByUser)` from +-- there to dismiss the notification yourself. +-- @tparam[opt] func args.destroy Function to run when notification is destroyed. +-- @tparam[opt] table args.preset Table with any of the above parameters. +-- Note: Any parameters specified directly in args will override ones defined +-- in the preset. +-- @tparam[opt] int args.replaces_id Replace the notification with the given ID. +-- @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. +-- @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 }) +-- @treturn ?table The notification object, or nil in case a notification was +-- not displayed. +-- @function naughty.notification +local function create(args) + if cst.config.notify_callback then + args = cst.config.notify_callback(args) + if not args then return end + end + + local n = gobject { + enable_properties = true, + } + + assert(naughty.emit_signal) + -- Make sure all signals bubble up + n:_connect_everything(naughty.emit_signal) + + -- Avoid modifying the original table + local 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 {} + )) + + for k, v in pairs(n.preset) do + private[k] = v + end + + for k, v in pairs(args) do + private[k] = v + end + + rawset(n, "_private", private) + + gtable.crush(n, notification, true) + + -- Allow extensions to create override the preset with custom data + naughty.emit_signal("request::preset", n, args) + + -- Register the notification before requesting a widget + n:emit_signal("new", args) + + -- Let all listeners handle the actual visual aspects + if (not n.ignore) and (not n.preset.ignore) then + naughty.emit_signal("request::display", 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) + end + + return n +end + +return setmetatable(notification, {__call = function(_, ...) return create(...) end}) diff --git a/tests/examples/text/awful/keygrabber/autostart.lua b/tests/examples/text/awful/keygrabber/autostart.lua index ac4e5d58f..d8814c665 100644 --- a/tests/examples/text/awful/keygrabber/autostart.lua +++ b/tests/examples/text/awful/keygrabber/autostart.lua @@ -1,7 +1,7 @@ local awful = { keygrabber = require("awful.keygrabber") } --DOC_HIDE -local naughty = { notify = function() end } --DOC_HIDE +local naughty = { notification = function() end } --DOC_HIDE local autostart_works = false --DOC_HIDE @@ -11,7 +11,7 @@ awful.keygrabber { stop_callback = function(_, _, _, sequence) autostart_works = true --DOC_HIDE assert(sequence == "abc") --DOC_HIDE - naughty.notify{text="The keys were:"..sequence} + naughty.notification {text="The keys were:"..sequence} end, } diff --git a/tests/examples/wibox/awidget/prompt/keypress.lua b/tests/examples/wibox/awidget/prompt/keypress.lua index 50fc56346..232540427 100644 --- a/tests/examples/wibox/awidget/prompt/keypress.lua +++ b/tests/examples/wibox/awidget/prompt/keypress.lua @@ -15,7 +15,7 @@ local naughty = {} --DOC_HIDE prompt = "Run: ", keypressed_callback = function(mod, key, cmd) --luacheck: no unused args if key == "Shift_L" then - notif = naughty.notify { text = "Shift pressed" } + notif = naughty.notification { text = "Shift pressed" } end end, keyreleased_callback = function(mod, key, cmd) --luacheck: no unused args diff --git a/tests/examples/wibox/awidget/prompt/simple.lua b/tests/examples/wibox/awidget/prompt/simple.lua index bdf98a365..8e64d1fc5 100644 --- a/tests/examples/wibox/awidget/prompt/simple.lua +++ b/tests/examples/wibox/awidget/prompt/simple.lua @@ -18,7 +18,7 @@ local naughty = {} --DOC_HIDE textbox = atextbox, exe_callback = function(input) if not input or #input == 0 then return end - naughty.notify{ text = "The input was: "..input } + naughty.notification { text = "The input was: "..input } end } end diff --git a/tests/test-screen-changes.lua b/tests/test-screen-changes.lua index 12146c44b..c12bc9662 100644 --- a/tests/test-screen-changes.lua +++ b/tests/test-screen-changes.lua @@ -31,7 +31,7 @@ local steps = { fake_screen.selected_tag.layout = max -- Display a notification on the screen-to-be-removed - naughty.notify{ text = "test", screen = fake_screen } + naughty.notification { text = "test", screen = fake_screen } return true end From c691a0842b28ee03a81f2dbcd8b38477362b5544 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Thu, 3 Jan 2019 01:17:22 -0500 Subject: [PATCH 08/21] notification: Prevent Lua errors from causing unlimited timer events. It now runs user defined code, so it can happen. Extra safety is required. --- lib/naughty/notification.lua | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/naughty/notification.lua b/lib/naughty/notification.lua index 138a99150..fd76ff8a9 100644 --- a/lib/naughty/notification.lua +++ b/lib/naughty/notification.lua @@ -244,11 +244,11 @@ end function notification:reset_timeout(new_timeout) if self.timer then self.timer:stop() end - local timeout = new_timeout or self.timeout - self:set_timeout(self, timeout) - self.timeout = timeout + self.timeout = new_timeout or self.timeout - self.timer:start() + if not self.timer.started then + self.timer:start() + end end function notification:set_id(new_id) @@ -264,10 +264,21 @@ function notification:set_timeout(timeout) if self.timer and self._private.timeout == timeout then return end + -- 0 == never if timeout > 0 then local timer_die = timer { timeout = timeout } - timer_die:connect_signal("timeout", function() die(cst.notification_closed_reason.expired) end) - if not self.suspended then --FIXME there's still a dependency loop to fix before it works + + timer_die:connect_signal("timeout", function() + pcall(die, cst.notification_closed_reason.expired) + + -- Prevent infinite timers events on errors. + if timer_die.started then + timer_die:stop() + end + end) + + --FIXME there's still a dependency loop to fix before it works + if not self.suspended then timer_die:start() end From 29cef2d6158a87f7e559becda725b77290e722c8 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Thu, 3 Jan 2019 03:09:42 -0500 Subject: [PATCH 09/21] notification: Add a new destroyed reason for "lack of space". they were previously silently dismissed. --- lib/naughty/constants.lua | 1 + lib/naughty/notification.lua | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/naughty/constants.lua b/lib/naughty/constants.lua index aa7906bc1..f7c883a75 100644 --- a/lib/naughty/constants.lua +++ b/lib/naughty/constants.lua @@ -59,6 +59,7 @@ ret.config.defaults = { } ret.notification_closed_reason = { + too_many_on_screen = -2, silent = -1, expired = 1, dismissedByUser = 2, --TODO v5 remove this undocumented legacy constant diff --git a/lib/naughty/notification.lua b/lib/naughty/notification.lua index fd76ff8a9..f85cf621a 100644 --- a/lib/naughty/notification.lua +++ b/lib/naughty/notification.lua @@ -218,6 +218,9 @@ local notification = {} --- Emitted when the notification is destroyed. -- @signal destroyed +-- @tparam number reason Why it was destroyed +-- @tparam boolean keep_visible If it was kept visible. +-- @see naughty.notification_closed_reason -- . --FIXME needs a description -- @property ignore_suspend If set to true this notification @@ -234,7 +237,7 @@ local notification = {} -- @tparam[opt=false] boolean keep_visible If true, keep the notification visible -- @return True if the popup was successfully destroyed, nil otherwise function notification:destroy(reason, keep_visible) - self:emit_signal("destroyed") + self:emit_signal("destroyed", reason, keep_visible) return true end From bfda6f64bb6ece8c48ef771d607f5605b92e3648 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 4 Jan 2019 00:05:05 -0500 Subject: [PATCH 10/21] naughty: Display the notification with invalid icons. They previously caused an error. --- lib/naughty/layout/legacy.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/naughty/layout/legacy.lua b/lib/naughty/layout/legacy.lua index a757ff3a7..ff5140bbe 100644 --- a/lib/naughty/layout/legacy.lua +++ b/lib/naughty/layout/legacy.lua @@ -457,7 +457,8 @@ function naughty.default_notification_handler(notification, args) end -- is the icon file readable? - icon = surface.load_uncached(icon) + local had_icon = type(icon) == "string" + icon = surface.load_uncached_silently(icon) -- if we have an icon, use it if icon then @@ -487,6 +488,9 @@ function naughty.default_notification_handler(notification, args) end iconbox:set_resize(false) iconbox:set_image(icon) + elseif had_icon then + require("gears.debug").print_warning("Naughty: failed to load icon ".. + (args.icon or preset.icon).. "(title: "..title..")") end end notification.iconbox = iconbox From e70822a6a4468dd486e09fbd44d0608b42141f7c Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 4 Jan 2019 01:05:36 -0500 Subject: [PATCH 11/21] naughty: Make sure the icon cannot be bigger then the box. Without this change, parts of the icon were hidden. --- lib/naughty/layout/legacy.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/naughty/layout/legacy.lua b/lib/naughty/layout/legacy.lua index ff5140bbe..796a32b75 100644 --- a/lib/naughty/layout/legacy.lua +++ b/lib/naughty/layout/legacy.lua @@ -465,6 +465,14 @@ function naughty.default_notification_handler(notification, args) iconbox = wibox.widget.imagebox() iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin) + if max_height and icon:get_height() > max_height then + icon_size = icon_size and math.min(max_height, icon_size) or max_height + end + + if max_width and icon:get_width() > max_width then + icon_size = icon_size and math.min(max_width, icon_size) or max_width + end + if icon_size and (icon:get_height() > icon_size or icon:get_width() > icon_size) then size_info.icon_scale_factor = icon_size / math.max(icon:get_height(), icon:get_width()) From 6d5d016a2ad77e13d244cbaddcd4148d495fc2e4 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 4 Jan 2019 02:52:02 -0500 Subject: [PATCH 12/21] 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. --- lib/naughty/action.lua | 127 ++++++++++++++++++++++++++++++++++ lib/naughty/core.lua | 3 +- lib/naughty/dbus.lua | 12 +++- lib/naughty/layout/legacy.lua | 12 ++-- lib/naughty/notification.lua | 54 ++++++++++++++- 5 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 lib/naughty/action.lua diff --git a/lib/naughty/action.lua b/lib/naughty/action.lua new file mode 100644 index 000000000..a16a95706 --- /dev/null +++ b/lib/naughty/action.lua @@ -0,0 +1,127 @@ +--------------------------------------------------------------------------- +--- A notification action. +-- +-- A notification can have multiple actions to chose from. This module allows +-- to manage such actions. +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2019 Emmanuel Lepage Vallee +-- @classmod naughty.action +--------------------------------------------------------------------------- +local gtable = require("gears.table" ) +local gobject = require("gears.object") + +local action = {} + +--- Create a new action. +-- @function naughty.action +-- @tparam table args The arguments. +-- @tparam string args.name The name. +-- @tparam string args.position The position. +-- @tparam string args.icon The icon. +-- @tparam naughty.notification args.notification The notification object. +-- @tparam boolean args.selected If this action is currently selected. +-- @return A new action. + +-- The action name. +-- @property name +-- @tparam string name The name. + +-- If the action is selected. +-- +-- Only a single action can be selected per notification. It will be applied +-- when `my_notification:apply()` is called. +-- +-- @property selected +-- @param boolean + +--- The action position (index). +-- @property position +-- @param number + +--- The action icon. +-- @property icon +-- @param gears.surface + +--- The notification. +-- @property notification +-- @tparam naughty.notification notification + +--- When a notification is invoked. +-- @signal invoked + +function action:get_selected() + return self._private.selected +end + +function action:set_selected(value) + self._private.selected = value + self:emit_signal("property::selected", value) + + if self._private.notification then + self._private.notification:emit_signal("property::actions") + end + + --TODO deselect other actions from the same notification +end + +function action:get_position() + return self._private.position +end + +function action:set_position(value) + self._private.position = value + self:emit_signal("property::position", value) + + if self._private.notification then + self._private.notification:emit_signal("property::actions") + end + + --TODO make sure the position is unique +end + +for _, prop in ipairs { "name", "icon", "notification" } do + action["get_"..prop] = function(self) + return self._private[prop] + end + + action["set_"..prop] = function(self, value) + self._private[prop] = value + self:emit_signal("property::"..prop, value) + + -- Make sure widgets with as an actionlist is updated. + if self._private.notification then + self._private.notification:emit_signal("property::actions") + end + end +end + +--- Execute this action. +function action:invoke() + assert(self._private.notification, + "Cannot invoke an action without a notification") + + self:emit_signal("invoked") +end + +local function new(_, args) + args = args or {} + local ret = gobject { enable_properties = true } + + gtable.crush(ret, action, true) + + local default = { + -- See "table 1" of the spec about the default name + name = args.name or "default", + selected = args.selected == true, + position = args.position, + icon = args.icon, + notification = args.notification, + } + + rawset(ret, "_private", default) + + return ret +end + +return setmetatable(action, {__call = new}) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index ea3827407..d7fee714d 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -497,8 +497,7 @@ end -- @tparam[opt] func args.callback Function that will be called with all arguments. -- The notification will only be displayed if the function returns true. -- Note: this function is only relevant to notifications sent via dbus. --- @tparam[opt] table args.actions Mapping that maps a string to a callback when this --- action is selected. +-- @tparam[opt] table args.actions A list of `naughty.action`s. -- @bool[opt=false] args.ignore_suspend If set to true this notification -- will be shown even if notifications are suspended via `naughty.suspend`. -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 }) diff --git a/lib/naughty/dbus.lua b/lib/naughty/dbus.lua index 1d89a0d62..89ff9b94c 100644 --- a/lib/naughty/dbus.lua +++ b/lib/naughty/dbus.lua @@ -27,6 +27,7 @@ local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility local naughty = require("naughty.core") local cst = require("naughty.constants") local nnotif = require("naughty.notification") +local naction = require("naughty.action") --- Notification library, dbus bindings local dbus = { config = {} } @@ -158,10 +159,17 @@ capi.dbus.connect_signal("org.freedesktop.Notifications", notification:destroy(cst.notification_closed_reason.dismissed_by_user) end elseif action_id ~= nil and action_text ~= nil then - args.actions[action_text] = function() + local a = naction { + name = action_text, + position = action_id, + } + + a:connect_signal("invoked", function() sendActionInvoked(notification.id, action_id) notification:destroy(cst.notification_closed_reason.dismissed_by_user) - end + end) + + table.insert(args.actions, a) end end end diff --git a/lib/naughty/layout/legacy.lua b/lib/naughty/layout/legacy.lua index 796a32b75..c46c96eb1 100644 --- a/lib/naughty/layout/legacy.lua +++ b/lib/naughty/layout/legacy.lua @@ -404,23 +404,25 @@ function naughty.default_notification_handler(notification, args) local actions_max_width = 0 local actions_total_height = 0 if actions then - for action, callback in pairs(actions) do + for _, action in ipairs(actions) do + assert(type(action) == "table") + assert(action.name ~= nil) local actiontextbox = wibox.widget.textbox() local actionmarginbox = wibox.container.margin() actionmarginbox:set_margins(margin) actionmarginbox:set_widget(actiontextbox) actiontextbox:set_valign("middle") actiontextbox:set_font(font) - actiontextbox:set_markup(string.format('☛ %s', action)) + actiontextbox:set_markup(string.format('☛ %s', action.name)) -- calculate the height and width local w, h = actiontextbox:get_preferred_size(s) local action_height = h + 2 * margin local action_width = w + 2 * margin actionmarginbox:buttons(gtable.join( - button({ }, 1, callback), - button({ }, 3, callback) - )) + button({ }, 1, function() action:trigger() end), + button({ }, 3, function() action:trigger() end) + )) actionslayout:add(actionmarginbox) actions_total_height = actions_total_height + action_height diff --git a/lib/naughty/notification.lua b/lib/naughty/notification.lua index f85cf621a..402e4257f 100644 --- a/lib/naughty/notification.lua +++ b/lib/naughty/notification.lua @@ -325,6 +325,43 @@ for _, prop in ipairs(properties) do end +--TODO v6: remove this +local function convert_actions(actions) + gdebug.deprecate( + "The notification actions should now be of type `naughty.action`, ".. + "not strings or callback functions", + {deprecated_in=5} + ) + + local naction = require("naughty.action") + + -- Does not attempt to handle when there is a mix of strings and objects + for idx, name in pairs(actions) do + local cb = nil + + if type(name) == "function" then + cb = name + end + + if type(idx) == "string" then + name, idx = idx, nil + end + + local a = naction { + position = idx, + name = name, + } + + if cb then + a:connect_signal("invoked", cb) + end + + -- Yes, it modifies `args`, this is legacy code, cloning the args + -- just for this isn't worth it. + actions[idx] = a + end +end + --- Create a notification. -- -- @tab args The argument table containing any of the arguments below. @@ -364,8 +401,7 @@ end -- @tparam[opt] func args.callback Function that will be called with all arguments. -- The notification will only be displayed if the function returns true. -- Note: this function is only relevant to notifications sent via dbus. --- @tparam[opt] table args.actions Mapping that maps a string to a callback when this --- action is selected. +-- @tparam[opt] table args.actions A list of `naughty.action`s. -- @bool[opt=false] args.ignore_suspend If set to true this notification -- will be shown even if notifications are suspended via `naughty.suspend`. -- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 }) @@ -378,6 +414,16 @@ local function create(args) if not args then return end end + args = args or {} + + -- Old actions usually have callbacks and names. But this isn't non + -- compliant with the spec. The spec has explicit ordering and optional + -- icons. The old format doesn't allow these metadata to be stored. + local is_old_action = args.actions and ( + (args.actions[1] and type(args.actions[1]) == "string") or + (type(next(args.actions)) == "string") + ) + local n = gobject { enable_properties = true, } @@ -396,6 +442,10 @@ local function create(args) rawget(n, "preset") or {} )) + if is_old_action then + convert_actions(args.actions) + end + for k, v in pairs(n.preset) do private[k] = v end From faa553e47cc94e7a8b76d4f1fce9d17d4234790e Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Thu, 3 Jan 2019 01:52:13 -0500 Subject: [PATCH 13/21] notification: Allow to pause automatic expiration. When the mouse is over or a keyboard driven menu is open, avoid unexpected expiration to mess with the current notifications. This commit also improve the `suspended` behavior to correctly emit some signals. --- lib/naughty/core.lua | 73 ++++++++++++++++++++++++++---------- lib/naughty/notification.lua | 16 ++++++++ 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index d7fee714d..668310158 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -98,7 +98,19 @@ gtable.crush(naughty, require("naughty.constants")) -- @property suspended -- @param boolean -local suspended = false +--- Do not allow notifications to auto-expire. +-- +-- When navigating the notifications, for example on mouse over or when +-- keyboard navigation is enabled, it is very annoying when notifications +-- just vanish. +-- +-- @property expiration_paused +-- @param[opt=false] boolean + +local properties = { + suspended = false, + expiration_paused = false +} --TODO v5 Deprecate the public `naughty.notifications` (to make it private) @@ -112,7 +124,7 @@ local suspended = false -- @field die Function to be executed on timeout -- @field id Unique notification id based on a counter -- @table notifications -naughty.notifications = { suspended = { } } +naughty.notifications = { suspended = { }, _expired = {{}} } screen.connect_for_each_screen(function(s) naughty.notifications[s] = { top_left = {}, @@ -137,7 +149,7 @@ end) -- @deprecated naughty.is_suspended function naughty.is_suspended() gdebug.deprecate("Use naughty.suspended", {deprecated_in=5}) - return suspended + return properties.suspended end --- Suspend notifications. @@ -147,7 +159,7 @@ end -- @deprecated naughty.suspend function naughty.suspend() gdebug.deprecate("Use naughty.suspended = true", {deprecated_in=5}) - suspended = true + properties.suspended = true end local conns = {} @@ -196,9 +208,14 @@ function naughty.disconnect_signal(name, func) end local function resume() - suspended = false + properties.suspended = false for _, v in pairs(naughty.notifications.suspended) do - v:emit_signal("request::display") + local args = v._private.args + assert(args) + v._private.args = nil + + naughty.emit_signal("added", v, args) + naughty.emit_signal("request::display", v, args) if v.timer then v.timer:start() end end naughty.notifications.suspended = { } @@ -221,7 +238,7 @@ end -- @deprecated naughty.toggle function naughty.toggle() gdebug.deprecate("Use naughty.suspended = not naughty.suspended", {deprecated_in=5}) - if suspended then + if properties.suspended then naughty.resume() else naughty.suspend() @@ -339,10 +356,10 @@ end -- Remove the notification from the internal list(s) local function cleanup(self, reason) - if suspended then - for k, v in pairs(naughty.suspended) do + if properties.suspended then + for k, v in pairs(naughty.notifications.suspended) do if v == self then - table.remove(naughty.suspended, k) + table.remove(naughty.notifications.suspended, k) break end end @@ -357,7 +374,8 @@ local function cleanup(self, reason) n.idx = k end - if self.timer then + -- `self.timer.started` will be false if the expiration was paused. + if self.timer and self.timer.started then self.timer:stop() end @@ -374,7 +392,17 @@ end -- Proxy the global suspension state on all notification objects local function get_suspended(self) - return suspended and not self.ignore_suspend + return properties.suspended and not self.ignore_suspend +end + +function naughty.set_expiration_paused(p) + properties.expiration_paused = p + + if not p then + for _, n in ipairs(naughty.notifications._expired[1]) do + n:destroy(naughty.notification_closed_reason.expired) + end + end end --- Emitted when a notification is created. @@ -421,12 +449,13 @@ local function register(notification, args) notification.idx = #naughty.notifications[s][notification.position] notification.screen = s - if suspended and not args.ignore_suspend then + if properties.suspended and not args.ignore_suspend then + notification._private.args = args table.insert(naughty.notifications.suspended, notification) + else + naughty.emit_signal("added", notification, args) end - naughty.emit_signal("added", notification, args) - assert(rawget(notification, "preset")) -- return the notification @@ -436,18 +465,22 @@ end naughty.connect_signal("new", register) local function index_miss(_, key) - if key == "suspended" then - return suspended + if rawget(naughty, "get_"..key) then + return rawget(naughty, "get_"..key)() + elseif properties[key] ~= nil then + return properties[key] else return nil end end local function set_index_miss(_, key, value) - if key == "suspended" then + if rawget(naughty, "set_"..key) then + return rawget(naughty, "set_"..key)(value) + elseif properties[key] ~= nil then assert(type(value) == "boolean") - suspended = value - if value then + properties[key] = value + if not value then resume() end else diff --git a/lib/naughty/notification.lua b/lib/naughty/notification.lua index 402e4257f..83841e75f 100644 --- a/lib/naughty/notification.lua +++ b/lib/naughty/notification.lua @@ -216,6 +216,11 @@ local notification = {} --@property suspended --@param boolean +--- If the notification is expired. +-- @property is_expired +-- @param boolean +-- @see naughty.expiration_paused + --- Emitted when the notification is destroyed. -- @signal destroyed -- @tparam number reason Why it was destroyed @@ -262,6 +267,14 @@ end function notification:set_timeout(timeout) local die = function (reason) + if reason == cst.notification_closed_reason.expired then + self.is_expired = true + if naughty.expiration_paused then + table.insert(naughty.notifications._expired[1], self) + return + end + end + self:destroy(reason) end @@ -454,6 +467,9 @@ local function create(args) private[k] = v end + -- It's an automatic property + n.is_expired = false + rawset(n, "_private", private) gtable.crush(n, notification, true) From 1b567cc06a7cae4ed5b005e1c089a03dfa154778 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 4 Jan 2019 04:39:07 -0500 Subject: [PATCH 14/21] notification: Rename text to message There is many "text" and the default text is the title, not the body. --- awesomerc.lua | 12 ++++---- lib/awful/spawn.lua | 6 ++-- lib/naughty/core.lua | 4 +-- lib/naughty/dbus.lua | 4 +-- lib/naughty/layout/legacy.lua | 4 +-- lib/naughty/notification.lua | 29 +++++++++++++++++-- .../text/awful/keygrabber/autostart.lua | 2 +- .../wibox/awidget/prompt/keypress.lua | 2 +- .../examples/wibox/awidget/prompt/simple.lua | 2 +- tests/test-screen-changes.lua | 3 +- 10 files changed, 46 insertions(+), 22 deletions(-) diff --git a/awesomerc.lua b/awesomerc.lua index 0bc0432cb..fa584b8a8 100644 --- a/awesomerc.lua +++ b/awesomerc.lua @@ -25,9 +25,9 @@ require("awful.hotkeys_popup.keys") -- another config (This code will only ever execute for the fallback config) if awesome.startup_errors then naughty.notification { - preset = naughty.config.presets.critical, - title = "Oops, there were errors during startup!", - text = awesome.startup_errors + preset = naughty.config.presets.critical, + title = "Oops, there were errors during startup!", + message = awesome.startup_errors } end @@ -40,9 +40,9 @@ do in_error = true naughty.notification { - preset = naughty.config.presets.critical, - title = "Oops, an error happened!", - text = tostring(err) + preset = naughty.config.presets.critical, + title = "Oops, an error happened!", + message = tostring(err) } in_error = false diff --git a/lib/awful/spawn.lua b/lib/awful/spawn.lua index 37b634c1b..6474d6286 100644 --- a/lib/awful/spawn.lua +++ b/lib/awful/spawn.lua @@ -132,17 +132,17 @@ -- -- awful.spawn.with_line_callback(noisy, { -- stdout = function(line) --- naughty.notification { text = "LINE:"..line } +-- naughty.notification { message = "LINE:"..line } -- end, -- stderr = function(line) --- naughty.notification { text = "ERR:"..line} +-- naughty.notification { message = "ERR:"..line} -- end, -- }) -- -- If only the full output is needed, then `easy_async` is the right choice: -- -- awful.spawn.easy_async(noisy, function(stdout, stderr, reason, exit_code) --- naughty.notification { text = stdout } +-- naughty.notification { message = stdout } -- end) -- -- **Default applications**: diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index 668310158..9eae64c34 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -334,7 +334,7 @@ end --- Replace title and text of an existing notification. -- --- This function is deprecated, use `notification.text = new_text` and +-- This function is deprecated, use `notification.message = new_text` and -- `notification.title = new_title` -- -- @tparam notification notification Notification object, which contents are to be replaced. @@ -533,7 +533,7 @@ end -- @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 }) +-- @usage naughty.notify({ title = "Achtung!", message = "You're idling", timeout = 0 }) -- @treturn ?table The notification object, or nil in case a notification was -- not displayed. -- @deprecated naughty.notify diff --git a/lib/naughty/dbus.lua b/lib/naughty/dbus.lua index 89ff9b94c..8b86cd1b0 100644 --- a/lib/naughty/dbus.lua +++ b/lib/naughty/dbus.lua @@ -122,13 +122,13 @@ capi.dbus.connect_signal("org.freedesktop.Notifications", local args = { } if data.member == "Notify" then if text ~= "" then - args.text = text + args.message = text if title ~= "" then args.title = title end else if title ~= "" then - args.text = title + args.message = title else return end diff --git a/lib/naughty/layout/legacy.lua b/lib/naughty/layout/legacy.lua index c46c96eb1..0e20f0662 100644 --- a/lib/naughty/layout/legacy.lua +++ b/lib/naughty/layout/legacy.lua @@ -135,7 +135,7 @@ local escape_subs = { ['<'] = "<", ['>'] = ">", ['&'] = "&" } -- Cache the markup local function set_escaped_text(self) - local text, title = self.text or "", self.title or "" + local text, title = self.message or "", self.title or "" if title then title = title .. "\n" else title = "" end @@ -291,7 +291,7 @@ function naughty.default_notification_handler(notification, args) end local preset = notification.preset - local text = args.text or preset.text + 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()) diff --git a/lib/naughty/notification.lua b/lib/naughty/notification.lua index 83841e75f..ac229e40a 100644 --- a/lib/naughty/notification.lua +++ b/lib/naughty/notification.lua @@ -20,6 +20,7 @@ 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 notification = {} @@ -309,8 +310,24 @@ function notification:set_timeout(timeout) self._private.timeout = timeout end +function notification:set_text(txt) + gdebug.deprecate( + "The `text` attribute is deprecated, use `message`", + {deprecated_in=5} + ) + self:set_message(txt) +end + +function notification:get_text() + gdebug.deprecate( + "The `text` attribute is deprecated, use `message`", + {deprecated_in=5} + ) + return self:get_message() +end + local properties = { - "text" , "title" , "timeout" , "hover_timeout" , + "message" , "title" , "timeout" , "hover_timeout" , "screen" , "position", "ontop" , "border_width" , "width" , "font" , "icon" , "icon_size" , "fg" , "bg" , "height" , "border_color" , @@ -417,7 +434,7 @@ end -- @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 }) +-- @usage naughty.notify({ title = "Achtung!", message = "You're idling", timeout = 0 }) -- @treturn ?table The notification object, or nil in case a notification was -- not displayed. -- @function naughty.notification @@ -441,6 +458,14 @@ local function create(args) enable_properties = true, } + if args.text then + gdebug.deprecate( + "The `text` attribute is deprecated, use `message`", + {deprecated_in=5} + ) + args.message = args.text + end + assert(naughty.emit_signal) -- Make sure all signals bubble up n:_connect_everything(naughty.emit_signal) diff --git a/tests/examples/text/awful/keygrabber/autostart.lua b/tests/examples/text/awful/keygrabber/autostart.lua index d8814c665..f490b3e92 100644 --- a/tests/examples/text/awful/keygrabber/autostart.lua +++ b/tests/examples/text/awful/keygrabber/autostart.lua @@ -11,7 +11,7 @@ awful.keygrabber { stop_callback = function(_, _, _, sequence) autostart_works = true --DOC_HIDE assert(sequence == "abc") --DOC_HIDE - naughty.notification {text="The keys were:"..sequence} + naughty.notification {message="The keys were:"..sequence} end, } diff --git a/tests/examples/wibox/awidget/prompt/keypress.lua b/tests/examples/wibox/awidget/prompt/keypress.lua index 232540427..9b8943a37 100644 --- a/tests/examples/wibox/awidget/prompt/keypress.lua +++ b/tests/examples/wibox/awidget/prompt/keypress.lua @@ -15,7 +15,7 @@ local naughty = {} --DOC_HIDE prompt = "Run: ", keypressed_callback = function(mod, key, cmd) --luacheck: no unused args if key == "Shift_L" then - notif = naughty.notification { text = "Shift pressed" } + notif = naughty.notification { message = "Shift pressed" } end end, keyreleased_callback = function(mod, key, cmd) --luacheck: no unused args diff --git a/tests/examples/wibox/awidget/prompt/simple.lua b/tests/examples/wibox/awidget/prompt/simple.lua index 8e64d1fc5..df5287c57 100644 --- a/tests/examples/wibox/awidget/prompt/simple.lua +++ b/tests/examples/wibox/awidget/prompt/simple.lua @@ -18,7 +18,7 @@ local naughty = {} --DOC_HIDE textbox = atextbox, exe_callback = function(input) if not input or #input == 0 then return end - naughty.notification { text = "The input was: "..input } + naughty.notification { message = "The input was: "..input } end } end diff --git a/tests/test-screen-changes.lua b/tests/test-screen-changes.lua index c12bc9662..7e24e25f0 100644 --- a/tests/test-screen-changes.lua +++ b/tests/test-screen-changes.lua @@ -31,8 +31,7 @@ local steps = { fake_screen.selected_tag.layout = max -- Display a notification on the screen-to-be-removed - naughty.notification { text = "test", screen = fake_screen } - + naughty.notification { message = "test", screen = fake_screen } return true end end, From b11582ad1555993065b14d5e86dd192c141f979d Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 4 Jan 2019 19:23:44 -0500 Subject: [PATCH 15/21] build: Install `notify-send` We could use a `dbus-send` too, but this would be a little more error prone given the dbus notification call is quite large. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 26c3052f4..70ae1559d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ install: # Install build dependencies. # See also `apt-cache showsrc awesome | grep -E '^(Version|Build-Depends)'`. - - sudo apt-get install -y libcairo2-dev gir1.2-gtk-3.0 libpango1.0-dev libxcb-xtest0-dev libxcb-icccm4-dev libxcb-randr0-dev libxcb-keysyms1-dev libxcb-xinerama0-dev libdbus-1-dev libxdg-basedir-dev libstartup-notification0-dev imagemagick libxcb1-dev libxcb-shape0-dev libxcb-util0-dev libx11-xcb-dev libxcb-cursor-dev libxcb-xkb-dev libxcb-xfixes0-dev libxkbcommon-dev libxkbcommon-x11-dev + - sudo apt-get install -y libnotify-bin libcairo2-dev gir1.2-gtk-3.0 libpango1.0-dev libxcb-xtest0-dev libxcb-icccm4-dev libxcb-randr0-dev libxcb-keysyms1-dev libxcb-xinerama0-dev libdbus-1-dev libxdg-basedir-dev libstartup-notification0-dev imagemagick libxcb1-dev libxcb-shape0-dev libxcb-util0-dev libx11-xcb-dev libxcb-cursor-dev libxcb-xkb-dev libxcb-xfixes0-dev libxkbcommon-dev libxkbcommon-x11-dev - sudo gem install asciidoctor # Deps for tests. From e13b9a48fffa5fdd99b6f676a2389a0b3ff15f45 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 13 Aug 2017 21:21:01 -0400 Subject: [PATCH 16/21] naughty: Use same object when the remote source request a replacement Previously, it would create a new object. The leaves the old `replaces_id` logic mostly intact for now to keep the full backward compatibility. I don't think anybody would have noticed the changed, by time and time again we had proof that some silent users have some amazing and advanced code hidden somewhere. It could be cleaned later when breaking compatibility isn't an issue. --- lib/naughty/dbus.lua | 21 +++++++++++++++++++-- lib/naughty/layout/legacy.lua | 30 ------------------------------ 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/lib/naughty/dbus.lua b/lib/naughty/dbus.lua index 8b86cd1b0..639b0dbc4 100644 --- a/lib/naughty/dbus.lua +++ b/lib/naughty/dbus.lua @@ -73,6 +73,9 @@ local function sendNotificationClosed(notificationId, reason) end end +-- This allow notification to be upadated later. +local counter = 1 + local function convert_icon(w, h, rowstride, channels, data) -- Do the arguments look sane? (e.g. we have enough data) local expected_length = rowstride * (h - 1) + w * channels @@ -201,10 +204,24 @@ capi.dbus.connect_signal("org.freedesktop.Notifications", args.timeout = expire / 1000 end args.freedesktop_hints = hints - notification = nnotif(args) + + -- Try to update existing objects when possible + notification = naughty.get_by_id(replaces_id) + + if notification then + for k, v in pairs(args) do + notification[k] = v + end + else + counter = counter+1 + args.id = counter + notification = nnotif(args) + end + return "u", notification.id end - return "u", "0" + counter = counter+1 + return "u", counter elseif data.member == "CloseNotification" then local obj = naughty.get_by_id(appname) if obj then diff --git a/lib/naughty/layout/legacy.lua b/lib/naughty/layout/legacy.lua index 0e20f0662..b8d1be906 100644 --- a/lib/naughty/layout/legacy.lua +++ b/lib/naughty/layout/legacy.lua @@ -57,10 +57,6 @@ screen.connect_for_each_screen(function(s) } end) --- Counter for the notifications --- Required for later access via DBUS -local counter = 1 - --- Evaluate desired position of the notification by index - internal -- -- @param s Screen to use @@ -342,28 +338,6 @@ function naughty.default_notification_handler(notification, args) local opacity = args.opacity or preset.opacity or beautiful.notification_opacity - -- replace notification if needed - local reuse_box - if args.replaces_id then - local obj = naughty.get_by_id(args.replaces_id) - if obj then - -- destroy this and ... - naughty.destroy(obj, naughty.notification_closed_reason.silent, true) - reuse_box = obj.box - end - -- ... may use its ID - if args.replaces_id <= counter then - notification.id = args.replaces_id - else - counter = counter + 1 - notification.id = counter - end - else - -- get a brand new ID - counter = counter + 1 - notification.id = counter - end - notification.position = position -- hook destroy @@ -515,10 +489,6 @@ function naughty.default_notification_handler(notification, args) shape = shape, type = "notification" }) - if reuse_box then - notification.box = reuse_box - end - if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end notification.size_info = size_info From 66b39aee35e27aa82d763d872cfea1274ad18ff0 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 6 Aug 2017 01:28:21 -0400 Subject: [PATCH 17/21] doc: Add a link to the base naughty documentation. --- lib/naughty/core.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index 9eae64c34..b239a5dec 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -1,5 +1,9 @@ ---------------------------------------------------------------------------- ---- Notification library +--- Notification library. +-- +-- For more details on how to create notifications, see `naughty.notification`. +-- +-- To send notifications from the terminal, use `notify-send`. -- -- @author koniu <gkusnierz@gmail.com> -- @copyright 2008 koniu From d859f671a1c8dabe975c12214639baee549b2dfa Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 4 Jan 2019 17:39:14 -0500 Subject: [PATCH 18/21] naughty: Add hicolor to the default icon path. Ref #2533 --- lib/naughty/constants.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/naughty/constants.lua b/lib/naughty/constants.lua index f7c883a75..693c08a78 100644 --- a/lib/naughty/constants.lua +++ b/lib/naughty/constants.lua @@ -16,7 +16,7 @@ local ret = {} ret.config = { padding = dpi(4), spacing = dpi(1), - icon_dirs = { "/usr/share/pixmaps/", }, + icon_dirs = { "/usr/share/pixmaps/", "/usr/share/icons/hicolor" }, icon_formats = { "png", "gif" }, notify_callback = nil, } From 7c96a98a0d77b436f987310418577f2c484194c6 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 9 Feb 2019 17:14:38 -0500 Subject: [PATCH 19/21] naughty: Correctly update the content of the legacy popup --- lib/naughty/layout/legacy.lua | 77 +++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/lib/naughty/layout/legacy.lua b/lib/naughty/layout/legacy.lua index b8d1be906..bf1a231f1 100644 --- a/lib/naughty/layout/legacy.lua +++ b/lib/naughty/layout/legacy.lua @@ -374,6 +374,10 @@ function naughty.default_notification_handler(notification, args) notification.textbox = textbox set_escaped_text(notification) + -- Update the content if it changes + notification:connect_signal("property::message", set_escaped_text) + notification:connect_signal("property::title" , set_escaped_text) + local actionslayout = wibox.layout.fixed.vertical() local actions_max_width = 0 local actions_total_height = 0 @@ -435,47 +439,58 @@ function naughty.default_notification_handler(notification, args) -- is the icon file readable? local had_icon = type(icon) == "string" icon = surface.load_uncached_silently(icon) - - -- if we have an icon, use it if icon then iconbox = wibox.widget.imagebox() iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin) + end - if max_height and icon:get_height() > max_height then - icon_size = icon_size and math.min(max_height, icon_size) or max_height + -- if we have an icon, use it + local function update_icon(icn) + if icn then + if max_height and icn:get_height() > max_height then + icon_size = icon_size and math.min(max_height, icon_size) or max_height + end + + if max_width and icn:get_width() > max_width then + icon_size = icon_size and math.min(max_width, icon_size) or max_width + end + + if icon_size and (icn:get_height() > icon_size or icn:get_width() > icon_size) then + size_info.icon_scale_factor = icon_size / math.max(icn:get_height(), + icn:get_width()) + + size_info.icon_w = icn:get_width () * size_info.icon_scale_factor + size_info.icon_h = icn:get_height() * size_info.icon_scale_factor + + local scaled = + cairo.ImageSurface(cairo.Format.ARGB32, + gmath.round(size_info.icon_w), + gmath.round(size_info.icon_h)) + + local cr = cairo.Context(scaled) + cr:scale(size_info.icon_scale_factor, size_info.icon_scale_factor) + cr:set_source_surface(icn, 0, 0) + cr:paint() + icn = scaled + else + size_info.icon_w = icn:get_width () + size_info.icon_h = icn:get_height() + end + iconbox:set_resize(false) + iconbox:set_image(icn) end + end - if max_width and icon:get_width() > max_width then - icon_size = icon_size and math.min(max_width, icon_size) or max_width - end - - if icon_size and (icon:get_height() > icon_size or icon:get_width() > icon_size) then - size_info.icon_scale_factor = icon_size / math.max(icon:get_height(), - icon:get_width()) - - size_info.icon_w = icon:get_width () * size_info.icon_scale_factor - size_info.icon_h = icon:get_height() * size_info.icon_scale_factor - - local scaled = - cairo.ImageSurface(cairo.Format.ARGB32, - gmath.round(size_info.icon_w), - gmath.round(size_info.icon_h)) - - local cr = cairo.Context(scaled) - cr:scale(size_info.icon_scale_factor, size_info.icon_scale_factor) - cr:set_source_surface(icon, 0, 0) - cr:paint() - icon = scaled - else - size_info.icon_w = icon:get_width () - size_info.icon_h = icon:get_height() - end - iconbox:set_resize(false) - iconbox:set_image(icon) + if icon then + notification:connect_signal("property::icon", function() + update_icon(surface.load_uncached_silently(notification.icon)) + end) + update_icon(icon) elseif had_icon then require("gears.debug").print_warning("Naughty: failed to load icon ".. (args.icon or preset.icon).. "(title: "..title..")") end + end notification.iconbox = iconbox From db591b50a45511bc32fa38315dc96e578dae1de7 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 4 Jan 2019 03:24:28 -0500 Subject: [PATCH 20/21] tests: Add extensive tests for the existing notification features. This new test suit add a rather extensive coverage to the "legacy" notification popups. A few minor bugs have been found and fixed and we can rest assured that the new spec 1.2 support and extended manipulation API wont regress existing configs. --- tests/test-naughty-legacy.lua | 669 ++++++++++++++++++++++++++++++++++ 1 file changed, 669 insertions(+) create mode 100644 tests/test-naughty-legacy.lua diff --git a/tests/test-naughty-legacy.lua b/tests/test-naughty-legacy.lua new file mode 100644 index 000000000..8e77a61c3 --- /dev/null +++ b/tests/test-naughty-legacy.lua @@ -0,0 +1,669 @@ +-- This test suite tries to prevent the legacy notification popups from +-- regressing as the new notification API is improving. +local spawn = require("awful.spawn") +local naughty = require("naughty" ) +local cairo = require("lgi" ).cairo +local beautiful = require("beautiful") + +-- This module test deprecated APIs +require("gears.debug").deprecate = function() end + +local steps = {} + +local has_cmd_notify, has_cmd_send = false + +-- Use `notify-send` instead of the shimmed version to better test the dbus +-- to notification code. +local function check_cmd() + local path = os.getenv("PATH") + local pos = 1 + while path:find(":", pos) do + local np = path:find(":", pos) + local p = path:sub(pos, np-1).."/" + + pos = np+1 + + local f = io.open(p.."notify-send") + if f then + f:close() + has_cmd_notify = true + end + + f = io.open(p.."dbus-send") + if f then + f:close() + has_cmd_send = true + end + + if has_cmd_notify and has_cmd_send then return end + end +end + +check_cmd() + +-- Can't test anything of value the documentation example tests don't already +-- hit. +if not has_cmd_send then require("_runner").run_steps {}; return end + +local active, destroyed, reasons, counter = {}, {}, {}, 0 + +local default_width, default_height = 0, 0 + +naughty.connect_signal("added", function(n) + table.insert(active, n) + counter = counter + 1 +end) + +naughty.connect_signal("destroyed", function(n, reason) + local found = false + + for k, n2 in ipairs(active) do + if n2 == n then + found = true + table.remove(active, k) + end + end + + assert(found) + + if reason then + reasons[reason] = reasons[reason] and reasons[reason] + 1 or 1 + end + + table.insert(destroyed, n) +end) + +table.insert(steps, function() + if not has_cmd_notify then return true end + + spawn('notify-send title message -t 25000') + + return true +end) + +table.insert(steps, function() + if not has_cmd_notify then return true end + if #active ~= 1 then return end + + local n = active[1] + + assert(n.box) + local offset = 2*n.box.border_width + default_width = n.box.width+offset + default_height = n.box.height + offset + naughty.config.spacing + + assert(default_width > 0) + assert(default_height > 0) + + -- Make sure the expiration timer is started + assert(n.timer) + assert(n.timer.started) + assert(n.is_expired == false) + + n:destroy() + + assert(#active == 0) + + return true +end) + +-- Test pausing incoming notifications. +table.insert(steps, function() + assert(not naughty.suspended) + + naughty.suspended = true + + -- There is some magic behind this, check it works + assert(naughty.suspended) + + spawn('notify-send title message -t 25000') + + return true +end) + +-- Test resuming incoming notifications. +table.insert(steps, function(count) + if count ~= 4 then return end + + assert(#active == 0) + assert(#naughty.notifications.suspended == 1) + assert(naughty.notifications.suspended[1]:get_suspended()) + + naughty.resume() + + assert(not naughty.suspended) + assert(#naughty.notifications.suspended == 0) + assert(#active == 1) + + active[1]:destroy() + assert(#active == 0) + + spawn('notify-send title message -t 1') + + return true +end) + +-- Test automatic expiration. +table.insert(steps, function() + if counter ~= 3 then return end + + return true +end) + +table.insert(steps, function() + if #active > 0 then return end + + -- It expired after one milliseconds, so it should be gone as soon as + -- it is registered. + assert(#active == 0) + + assert(not naughty.expiration_paused) + naughty.expiration_paused = true + + -- There is some magic behind this, make sure it works + assert(naughty.expiration_paused) + + spawn('notify-send title message -t 1') + + return true +end) + +-- Test disabling automatic expiration. +table.insert(steps, function() + if counter ~= 4 then return end + + -- It should not expire by itself, so that should always be true + assert(#active == 1) + + return true +end) + +-- Wait long enough to avoid races. +table.insert(steps, function(count) + if count ~= 4 then return end + + assert(#active == 1) + assert(active[1].is_expired) + + naughty.expiration_paused = false + assert(not naughty.expiration_paused) + + return true +end) + +-- Make sure enabling expiration process the expired queue. +table.insert(steps, function() + -- Right now this doesn't require a step for itself, but this could change + -- so better not "document" the instantaneous clearing of the queue. + if #active > 0 then return end + + spawn('notify-send low message -t 25000 -u low') + spawn('notify-send normal message -t 25000 -u normal') + spawn('notify-send critical message -t 25000 -u critical') + + return true +end) + +-- Test the urgency level and default preset. +table.insert(steps, function() + if counter ~= 7 then return end + + while #active > 0 do + active[1]:destroy() + end + + return true +end) + +-- Test what happens when the screen has the maximum number of notification it +-- can display at one. +table.insert(steps, function() + local wa = mouse.screen.workarea + local max_notif = math.floor(wa.height/default_height) + + -- Everything should fit, otherwise the math is wrong in + -- `neughty.layout.legacy` and its a regression. + for i=1, max_notif do + spawn('notify-send "notif '..i..'" message -t 25000 -u low') + end + + return true +end) + +-- Test vertical overlapping +local function test_overlap() + local wa = mouse.screen.workarea + + for _, n1 in ipairs(active) do + assert(n1.box) + + -- Check for overlapping the workarea + assert(n1.box.y+default_height < wa.y+wa.height) + assert(n1.box.y >= wa.y) + + -- Check for overlapping each other + for _, n2 in ipairs(active) do + assert(n2.box) + if n1 ~= n2 then + local geo1, geo2 = n1.box:geometry(), n2.box:geometry() + assert(geo1.height == geo2.height) + assert(geo1.height + 2*n1.box.border_width + naughty.config.spacing + == default_height) + + if n1.position == n2.position then + assert( + geo1.y >= geo2.y+default_height or + geo2.y >= geo1.y+default_height + ) + end + end + end + end +end + +-- Check the lack of overlapping and the presence of the expected content. +table.insert(steps, function() + local wa = mouse.screen.workarea + local max_notif = math.floor(wa.height/default_height) + if counter ~= 7 + max_notif then return end + + assert(#active == max_notif) + + test_overlap() + + -- Now add even more! + for i=1, 5 do + spawn('notify-send "notif '..i..'" message -t 25000 -u low') + end + + return true +end) + +-- Test the code to hide the older notifications when there is too many for the +-- screen. +table.insert(steps, function() + local wa = mouse.screen.workarea + local max_notif = math.floor(wa.height/default_height) + if counter ~= 7 + max_notif + 5 then return end + + -- The other should have been hidden + assert(#active == max_notif) + + assert(reasons[naughty.notification_closed_reason.too_many_on_screen] == 5) + + test_overlap() + + while #active > 0 do + active[1]:destroy() + end + + return true +end) + +local positions = { + "top_left" , "top_middle" , "top_right" , + "bottom_left" , "bottom_middle" , "bottom_right" , +} + +-- Test each corners. +table.insert(steps, function() + for _, pos in ipairs(positions) do + for i=1, 3 do + -- Skip dbus for this one. + naughty.notification { + position = pos, + title = "At "..pos.." "..i, + message = "some message", + timeout = 25000, + } + end + end + + return true +end) + +table.insert(steps, function() + if #active ~= #positions*3 then return end + + test_overlap() + + while #active > 0 do + active[1]:destroy() + end + + return true +end) + +local big_icon = cairo.ImageSurface(cairo.Format.ARGB32, 256, 256) +local cr = cairo.Context(big_icon) +local small_icon = cairo.ImageSurface(cairo.Format.ARGB32, 32 , 32 ) +local cr2 = cairo.Context(small_icon) +local wierd_ratio1 = cairo.ImageSurface(cairo.Format.ARGB32, 256, 128) +local cr3 = cairo.Context(wierd_ratio1) +local wierd_ratio2 = cairo.ImageSurface(cairo.Format.ARGB32, 128, 256) +local cr4 = cairo.Context(wierd_ratio2) + +-- Checkboard shirt pattern icon! +for i=1, 5 do + for j=1, 5 do + cr:set_source_rgb( + i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1 + ) + cr:rectangle( (i-1)*48, (j-1)*48, 48, 48 ) + cr:fill() + + cr2:set_source_rgb( + i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1 + ) + cr2:rectangle( (i-1)*6, (j-1)*6, 6, 6 ) + cr2:fill() + + cr3:set_source_rgb( + i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1 + ) + cr3:rectangle( (i-1)*48, (j-1)*24, 48, 24 ) + cr3:fill() + + cr4:set_source_rgb( + i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1 + ) + cr4:rectangle( (i-1)*24, (j-1)*48, 24, 48 ) + cr4:fill() + end +end + +-- Test the icon size constraints. +table.insert(steps, function() + beautiful.notification_icon_size = 64 + + -- Icons that are too large (they should be downscaled) + local n1 = naughty.notification { + icon = big_icon, + title = "Has a nice icon!", + message = "big", + timeout = 25000, + } + + assert(n1.iconbox) + assert(n1.iconbox._private.image:get_width () == beautiful.notification_icon_size) + assert(n1.iconbox._private.image:get_height() == beautiful.notification_icon_size) + assert(n1.iconbox._private.image:get_width () == n1.size_info.icon_w) + assert(n1.iconbox._private.image:get_height() == n1.size_info.icon_h) + assert(n1.size_info.icon_scale_factor == 1/4) + + -- Icons that are too small (they should not be upscaled) + local n2 = naughty.notification { + icon = small_icon, + title = "Has a nice icon!", + message = "small", + timeout = 25000, + } + + assert(n2.iconbox) + assert(n2.iconbox._private.image:get_width () == 32) + assert(n2.iconbox._private.image:get_height() == 32) + assert(n2.iconbox._private.image:get_width () == n2.size_info.icon_w) + assert(n2.iconbox._private.image:get_height() == n2.size_info.icon_h) + assert(not n2.size_info.icon_scale_factor) + + -- Downscaled non square icons (aspect ratio should be kept). + local n3 = naughty.notification { + icon = wierd_ratio1, + title = "Has a nice icon!", + message = "big", + timeout = 25000, + } + + local n4 = naughty.notification { + icon = wierd_ratio2, + title = "Has a nice icon!", + message = "big", + timeout = 25000, + } + + assert(n3.iconbox) + assert(n3.iconbox._private.image:get_width () == beautiful.notification_icon_size) + assert(n3.iconbox._private.image:get_height() == beautiful.notification_icon_size/2) + assert(n3.iconbox._private.image:get_width () == n3.size_info.icon_w) + assert(n3.iconbox._private.image:get_height() == n3.size_info.icon_h) + assert(n3.size_info.icon_scale_factor == 1/4) + + assert(n4.iconbox) + assert(n4.iconbox._private.image:get_width () == beautiful.notification_icon_size/2) + assert(n4.iconbox._private.image:get_height() == beautiful.notification_icon_size) + assert(n4.iconbox._private.image:get_width () == n4.size_info.icon_w) + assert(n4.iconbox._private.image:get_height() == n4.size_info.icon_h) + assert(n4.size_info.icon_scale_factor == 1/4) + + -- The notification size should change proportionally to the icon size. + assert(n1.box.width == n2.box.width + 32) + assert(n1.box.height == n2.box.height + 32) + assert(n1.box.height == n3.box.height + 32) + assert(n1.box.width == n4.box.width + 32) + assert(n1.box.height == n4.box.height) + assert(n1.box.width == n3.box.width ) + + -- Make sure unconstrained icons work as expected. + beautiful.notification_icon_size = nil + + local n5 = naughty.notification { + icon = big_icon, + title = "Has a nice icon!", + message = "big", + timeout = 25000, + } + + assert(n5.iconbox) + assert(n5.iconbox._private.image:get_width () == 256) + assert(n5.iconbox._private.image:get_height() == 256) + assert(n5.iconbox._private.image:get_width () == n5.size_info.icon_w) + assert(n5.iconbox._private.image:get_height() == n5.size_info.icon_h) + assert(not n5.size_info.icon_scale_factor) + + -- Make sure invalid icons don't prevent the message from being shown. + local n6 = naughty.notification { + icon = "this/is/an/invlid/path", + title = "Has a nice icon!", + message = "Very important life saving advice", + timeout = 25000, + } + + n1:destroy() + n2:destroy() + n3:destroy() + n4:destroy() + n5:destroy() + n6:destroy() + assert(#active == 0) + + return true +end) + +-- Test notifications with size constraints. +table.insert(steps, function() + local str = "foobar! " + assert(#active == 0) + + -- 2^9 foobars is a lot of foobars. + for _=1, 10 do + str = str .. str + end + + -- First, see what happen without any constraint and enormous messages. + -- This also test notifications larger than the workarea. + + local n1 = naughty.notification { + title = str, + message = str, + timeout = 25000, + } + + -- Same, but with an icon and larger borders. + local n2 = naughty.notification { + icon = big_icon, + title = str, + message = str, + timeout = 25000, + border_width = 40, + } + + local wa = mouse.screen.workarea + 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() + + -- Now set a maximum size and try again. + beautiful.notification_max_width = 256 + beautiful.notification_max_height = 96 + + local n3 = naughty.notification { + title = str, + message = str, + timeout = 25000, + } + + assert(n3.box.width <= 256) + assert(n3.box.height <= 96 ) + + -- Now test when the icon is larger than the maximum. + local n4 = naughty.notification { + icon = big_icon, + title = str, + message = str, + timeout = 25000, + } + + assert(n4.box.width <= 256) + assert(n4.box.height <= 96 ) + assert(n4.iconbox._private.image:get_width () == n4.size_info.icon_w) + assert(n4.iconbox._private.image:get_height() == n4.size_info.icon_h) + assert(n4.size_info.icon_w <= 256) + assert(n4.size_info.icon_h <= 96 ) + + n3:destroy() + n4:destroy() + assert(#active == 0) + + return true +end) + +-- Test more advanced features than what notify-send can provide. +if has_cmd_send then + + local cmd = [[dbus-send \ + --type=method_call \ + --print-reply=literal \ + --dest=org.freedesktop.Notifications \ + /org/freedesktop/Notifications \ + org.freedesktop.Notifications.Notify \ + string:"Awesome test" \ + uint32:0 \ + string:"" \ + string:"title" \ + string:"message body" \ + array:string:1,"one",2,"two",3,"three" \ + dict:string:string:"","" \ + int32:25000]] + + -- Test the actions. + table.insert(steps, function() + + assert(#active == 0) + + spawn(cmd) + + return true + end) + + table.insert(steps, function() + if #active == 0 then return end + + assert(#active == 1) + local n = active[1] + + assert(n.box) + assert(#n.actions == 3) + assert(n.actions[1].name == "one" ) + assert(n.actions[2].name == "two" ) + assert(n.actions[3].name == "three") + + n:destroy() + + return true + end) + + --TODO Test too many actions. + + --TODO Test action with long names. + + local nid, name_u, message_u, actions_u = nil + + -- Test updating a notification. + table.insert(steps, function() + spawn.easy_async(cmd, function(out) + nid = tonumber(out:match(" [0-9]+"):match("[0-9]+")) + end) + + return true + end) + + table.insert(steps, function() + if #active == 0 or not nid then return end + + local n = active[1] + + n:connect_signal("property::title" , function() name_u = true end) + n:connect_signal("property::message", function() message_u = true end) + n:connect_signal("property::actions", function() actions_u = true end) + + local update = [[dbus-send \ + --type=method_call \ + --print-reply=literal \ + --dest=org.freedesktop.Notifications \ + /org/freedesktop/Notifications \ + org.freedesktop.Notifications.Notify \ + string:"Awesome test" \ + uint32:]].. nid ..[[ \ + string:"" \ + string:"updated title" \ + string:"updated message body" \ + array:string:1,"four",2,"five",3,"six" \ + dict:string:string:"","" \ + int32:25000]] + + spawn(update) + + return true + end) + + -- Test if all properties have been updated. + table.insert(steps, function() + if not name_u then return end + if not message_u then return end + if not actions_u then return end + + -- No new notification should have been created. + assert(#active == 1) + + local n = active[1] + + assert(n.title == "updated title" ) + assert(n.message == "updated message body") + + assert(#n.actions == 3) + assert(n.actions[1].name == "four" ) + assert(n.actions[2].name == "five" ) + assert(n.actions[3].name == "six" ) + + return true + end) + +end + +-- Test many screens. + +require("_runner").run_steps(steps) From 1b4c1e9f1c6b695cbb5a9d2780fb07c49435ff8a Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 16 Feb 2019 15:18:33 -0500 Subject: [PATCH 21/21] tests: Test the naughty deprecated APIs --- tests/test-naughty-legacy.lua | 92 +++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/tests/test-naughty-legacy.lua b/tests/test-naughty-legacy.lua index 8e77a61c3..5a0ad5f3b 100644 --- a/tests/test-naughty-legacy.lua +++ b/tests/test-naughty-legacy.lua @@ -2,6 +2,7 @@ -- regressing as the new notification API is improving. local spawn = require("awful.spawn") local naughty = require("naughty" ) +local gdebug = require("gears.debug") local cairo = require("lgi" ).cairo local beautiful = require("beautiful") @@ -49,12 +50,14 @@ local active, destroyed, reasons, counter = {}, {}, {}, 0 local default_width, default_height = 0, 0 -naughty.connect_signal("added", function(n) +local function added_callback(n) table.insert(active, n) counter = counter + 1 -end) +end -naughty.connect_signal("destroyed", function(n, reason) +naughty.connect_signal("added", added_callback) + +local function destroyed_callback(n, reason) local found = false for k, n2 in ipairs(active) do @@ -71,7 +74,9 @@ naughty.connect_signal("destroyed", function(n, reason) end table.insert(destroyed, n) -end) +end + +naughty.connect_signal("destroyed", destroyed_callback) table.insert(steps, function() if not has_cmd_notify then return true end @@ -664,6 +669,85 @@ if has_cmd_send then 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 + gdebug.deprecate = function() end + + local n = naughty.notification { + title = "foo", + message = "bar", + timeout = 25000, + } + + -- Make sure the suspension don't cause errors + assert(not naughty.is_suspended()) + assert(not naughty.suspended) + naughty.suspend() + assert(naughty.is_suspended()) + assert(naughty.suspended) + naughty.resume() + assert(not naughty.is_suspended()) + assert(not naughty.suspended) + naughty.toggle() + assert(naughty.is_suspended()) + assert(naughty.suspended) + naughty.toggle() + assert(not naughty.is_suspended()) + assert(not naughty.suspended) + naughty.suspended = not naughty.suspended + assert(naughty.is_suspended()) + assert(naughty.suspended) + naughty.suspended = not naughty.suspended + assert(not naughty.is_suspended()) + assert(not naughty.suspended) + + -- Replace the text + assert(n.message == "bar") + assert(n.text == "bar") + assert(n.title == "foo") + naughty.replace_text(n, "foobar", "baz") + assert(n.title == "foobar") + assert(n.message == "baz") + assert(n.text == "baz") + + -- Test the ID system + n.id = 1337 + assert(n.id == 1337) + assert(naughty.getById(1337) == n) + assert(naughty.get_by_id(1337) == n) + assert(naughty.getById(42) ~= n) + assert(naughty.get_by_id(42) ~= n) + + -- The timeout + naughty.reset_timeout(n, 1337) + + -- Destroy using the old API + local old_count = #destroyed + naughty.destroy(n) + assert(old_count == #destroyed - 1) + + -- Destroy using the old API, while suspended + local n2 = naughty.notification { + title = "foo", + message = "bar", + timeout = 25000, + } + naughty.suspended = true + naughty.destroy(n2) + assert(old_count == #destroyed - 2) + naughty.suspended = false + + -- The old notify function and "text" instead of "message" + naughty.notify { text = "foo" } + + -- Finish by testing disconnect_signal + naughty.disconnect_signal("destroyed", destroyed_callback) + naughty.disconnect_signal("added", added_callback) + + return true +end) + -- Test many screens. require("_runner").run_steps(steps)