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:
commit
698fce9b4e
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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**:
|
||||
|
|
|
@ -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})
|
|
@ -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
|
1009
lib/naughty/core.lua
1009
lib/naughty/core.lua
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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 = { ['<'] = "<", ['>'] = ">", ['&'] = "&" }
|
||||
|
||||
-- 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><Invalid markup or UTF8, cannot display message></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)
|
|
@ -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})
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue