Merge pull request #2541 from Elv13/xmas_2k18_9

Split naughty along model/view lines and add an extensive test suite
This commit is contained in:
Emmanuel Lepage Vallée 2019-02-16 16:12:39 -05:00 committed by GitHub
commit 698fce9b4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2442 additions and 695 deletions

View File

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

View File

@ -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!",
message = 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!",
message = tostring(err)
}
in_error = false
end)
end

View File

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

View File

@ -132,17 +132,17 @@
--
-- awful.spawn.with_line_callback(noisy, {
-- stdout = function(line)
-- naughty.notify { text = "LINE:"..line }
-- naughty.notification { message = "LINE:"..line }
-- end,
-- stderr = function(line)
-- naughty.notify { 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.notify { text = stdout }
-- naughty.notification { message = stdout }
-- end)
--
-- **Default applications**:

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

@ -0,0 +1,127 @@
---------------------------------------------------------------------------
--- A notification action.
--
-- A notification can have multiple actions to chose from. This module allows
-- to manage such actions.
--
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
-- @copyright 2019 Emmanuel Lepage Vallee
-- @classmod naughty.action
---------------------------------------------------------------------------
local gtable = require("gears.table" )
local gobject = require("gears.object")
local action = {}
--- Create a new action.
-- @function naughty.action
-- @tparam table args The arguments.
-- @tparam string args.name The name.
-- @tparam string args.position The position.
-- @tparam string args.icon The icon.
-- @tparam naughty.notification args.notification The notification object.
-- @tparam boolean args.selected If this action is currently selected.
-- @return A new action.
-- The action name.
-- @property name
-- @tparam string name The name.
-- If the action is selected.
--
-- Only a single action can be selected per notification. It will be applied
-- when `my_notification:apply()` is called.
--
-- @property selected
-- @param boolean
--- The action position (index).
-- @property position
-- @param number
--- The action icon.
-- @property icon
-- @param gears.surface
--- The notification.
-- @property notification
-- @tparam naughty.notification notification
--- When a notification is invoked.
-- @signal invoked
function action:get_selected()
return self._private.selected
end
function action:set_selected(value)
self._private.selected = value
self:emit_signal("property::selected", value)
if self._private.notification then
self._private.notification:emit_signal("property::actions")
end
--TODO deselect other actions from the same notification
end
function action:get_position()
return self._private.position
end
function action:set_position(value)
self._private.position = value
self:emit_signal("property::position", value)
if self._private.notification then
self._private.notification:emit_signal("property::actions")
end
--TODO make sure the position is unique
end
for _, prop in ipairs { "name", "icon", "notification" } do
action["get_"..prop] = function(self)
return self._private[prop]
end
action["set_"..prop] = function(self, value)
self._private[prop] = value
self:emit_signal("property::"..prop, value)
-- Make sure widgets with as an actionlist is updated.
if self._private.notification then
self._private.notification:emit_signal("property::actions")
end
end
end
--- Execute this action.
function action:invoke()
assert(self._private.notification,
"Cannot invoke an action without a notification")
self:emit_signal("invoked")
end
local function new(_, args)
args = args or {}
local ret = gobject { enable_properties = true }
gtable.crush(ret, action, true)
local default = {
-- See "table 1" of the spec about the default name
name = args.name or "default",
selected = args.selected == true,
position = args.position,
icon = args.icon,
notification = args.notification,
}
rawset(ret, "_private", default)
return ret
end
return setmetatable(action, {__call = new})

75
lib/naughty/constants.lua Normal file
View File

@ -0,0 +1,75 @@
----------------------------------------------------------------------------
--- 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/", "/usr/share/icons/hicolor" },
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 = {
too_many_on_screen = -2,
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

File diff suppressed because it is too large Load Diff

View File

@ -25,11 +25,15 @@ 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")
local naction = require("naughty.action")
--- 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 +50,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)
@ -69,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
@ -118,13 +125,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
@ -140,7 +147,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,13 +159,20 @@ 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()
local a = naction {
name = action_text,
position = action_id,
}
a:connect_signal("invoked", function()
sendActionInvoked(notification.id, action_id)
naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser)
end
notification:destroy(cst.notification_closed_reason.dismissed_by_user)
end)
table.insert(args.actions, a)
end
end
end
@ -190,16 +204,28 @@ capi.dbus.connect_signal("org.freedesktop.Notifications",
args.timeout = expire / 1000
end
args.freedesktop_hints = hints
notification = naughty.notify(args)
if notification ~= nil then
return "u", notification.id
-- 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", naughty.get_next_notification_id()
counter = counter+1
return "u", counter
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

View File

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

View File

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

View File

@ -0,0 +1,543 @@
----------------------------------------------------------------------------
--- 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)
--- 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 = { ['<'] = "&lt;", ['>'] = "&gt;", ['&'] = "&amp;" }
-- Cache the markup
local function set_escaped_text(self)
local text, title = self.message 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('<b>%s</b>%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 <br>.
if not set_markup("<br.->", "\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("<i>&lt;Invalid markup or UTF8, cannot display message&gt;</i>")
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.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())
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
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)
-- 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
if actions then
for _, action in ipairs(actions) do
assert(type(action) == "table")
assert(action.name ~= nil)
local actiontextbox = wibox.widget.textbox()
local actionmarginbox = wibox.container.margin()
actionmarginbox:set_margins(margin)
actionmarginbox:set_widget(actiontextbox)
actiontextbox:set_valign("middle")
actiontextbox:set_font(font)
actiontextbox:set_markup(string.format('☛ <u>%s</u>', action.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, function() action:trigger() end),
button({ }, 3, function() action:trigger() end)
))
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?
local had_icon = type(icon) == "string"
icon = surface.load_uncached_silently(icon)
if icon then
iconbox = wibox.widget.imagebox()
iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin)
end
-- 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 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
-- 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 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)

View File

@ -0,0 +1,521 @@
---------------------------------------------------------------------------
--- 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 gdebug = require("gears.debug")
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
--- 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
-- @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
-- 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", reason, keep_visible)
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
self.timeout = new_timeout or self.timeout
if not self.timer.started then
self.timer:start()
end
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)
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
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()
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
-- 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
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 = {
"message" , "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
--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.
-- @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 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!", message = "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
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,
}
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)
-- 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 {}
))
if is_old_action then
convert_actions(args.actions)
end
for k, v in pairs(n.preset) do
private[k] = v
end
for k, v in pairs(args) do
private[k] = v
end
-- It's an automatic property
n.is_expired = false
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})

View File

@ -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 {message="The keys were:"..sequence}
end,
}

View File

@ -15,7 +15,7 @@ local naughty = {} --DOC_HIDE
prompt = "<b>Run: </b>",
keypressed_callback = function(mod, key, cmd) --luacheck: no unused args
if key == "Shift_L" then
notif = naughty.notify { text = "Shift pressed" }
notif = naughty.notification { message = "Shift pressed" }
end
end,
keyreleased_callback = function(mod, key, cmd) --luacheck: no unused args

View File

@ -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 { message = "The input was: "..input }
end
}
end

View File

@ -0,0 +1,753 @@
-- 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 gdebug = require("gears.debug")
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
local function added_callback(n)
table.insert(active, n)
counter = counter + 1
end
naughty.connect_signal("added", added_callback)
local function destroyed_callback(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
naughty.connect_signal("destroyed", destroyed_callback)
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
-- 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)

View File

@ -31,8 +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 { message = "test", screen = fake_screen }
return true
end
end,