naughty: Move the notification object into its own file.

Mostly for the documentation, but also as the new base upon which
to build the modular notification GUI.
This commit is contained in:
Emmanuel Lepage Vallee 2017-07-01 22:43:26 -04:00
parent 14eab7890f
commit 9df77e5c76
14 changed files with 1334 additions and 639 deletions

View File

@ -24,9 +24,11 @@ require("awful.hotkeys_popup.keys")
-- Check if awesome encountered an error during startup and fell back to -- Check if awesome encountered an error during startup and fell back to
-- another config (This code will only ever execute for the fallback config) -- another config (This code will only ever execute for the fallback config)
if awesome.startup_errors then if awesome.startup_errors then
naughty.notify({ preset = naughty.config.presets.critical, naughty.notification {
title = "Oops, there were errors during startup!", preset = naughty.config.presets.critical,
text = awesome.startup_errors }) title = "Oops, there were errors during startup!",
text = awesome.startup_errors
}
end end
-- Handle runtime errors after startup -- Handle runtime errors after startup
@ -37,9 +39,12 @@ do
if in_error then return end if in_error then return end
in_error = true in_error = true
naughty.notify({ preset = naughty.config.presets.critical, naughty.notification {
title = "Oops, an error happened!", preset = naughty.config.presets.critical,
text = tostring(err) }) title = "Oops, an error happened!",
text = tostring(err)
}
in_error = false in_error = false
end) end)
end end

View File

@ -121,9 +121,12 @@ file = {
'../lib/gears/init.lua', '../lib/gears/init.lua',
'../lib/wibox/layout/init.lua', '../lib/wibox/layout/init.lua',
'../lib/wibox/container/init.lua', '../lib/wibox/container/init.lua',
'../lib/naughty/constants.lua',
'../lib/naughty/dbus.lua',
-- Ignore some parts of the widget library -- Ignore some parts of the widget library
'../lib/awful/widget/init.lua', '../lib/awful/widget/init.lua',
'../lib/naughty/layout/init.lua',
-- Deprecated classes for one years or more don't deserve entries -- Deprecated classes for one years or more don't deserve entries
-- in the index -- in the index

View File

@ -132,17 +132,17 @@
-- --
-- awful.spawn.with_line_callback(noisy, { -- awful.spawn.with_line_callback(noisy, {
-- stdout = function(line) -- stdout = function(line)
-- naughty.notify { text = "LINE:"..line } -- naughty.notification { text = "LINE:"..line }
-- end, -- end,
-- stderr = function(line) -- stderr = function(line)
-- naughty.notify { text = "ERR:"..line} -- naughty.notification { text = "ERR:"..line}
-- end, -- end,
-- }) -- })
-- --
-- If only the full output is needed, then `easy_async` is the right choice: -- 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) -- awful.spawn.easy_async(noisy, function(stdout, stderr, reason, exit_code)
-- naughty.notify { text = stdout } -- naughty.notification { text = stdout }
-- end) -- end)
-- --
-- **Default applications**: -- **Default applications**:

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

@ -0,0 +1,74 @@
----------------------------------------------------------------------------
--- This file hosts the shared constants used by the notification subsystem.
--
-- [[documented in core.lua]]
--
-- @author koniu <gkusnierz@gmail.com>
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
-- @copyright 2008 koniu
-- @copyright 2017 Emmanuel Lepage Vallee
----------------------------------------------------------------------------
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi
local ret = {}
ret.config = {
padding = dpi(4),
spacing = dpi(1),
icon_dirs = { "/usr/share/pixmaps/", },
icon_formats = { "png", "gif" },
notify_callback = nil,
}
ret.config.presets = {
low = {
timeout = 5
},
normal = {},
critical = {
bg = "#ff0000",
fg = "#ffffff",
timeout = 0,
},
ok = {
bg = "#00bb00",
fg = "#ffffff",
timeout = 5,
},
info = {
bg = "#0000ff",
fg = "#ffffff",
timeout = 5,
},
warn = {
bg = "#ffaa00",
fg = "#000000",
timeout = 10,
},
}
ret.config.defaults = {
timeout = 5,
text = "",
screen = nil,
ontop = true,
margin = dpi(5),
border_width = dpi(1),
position = "top_right"
}
ret.notification_closed_reason = {
silent = -1,
expired = 1,
dismissedByUser = 2, --TODO v5 remove this undocumented legacy constant
dismissed_by_user = 2,
dismissedByCommand = 3, --TODO v5 remove this undocumented legacy constant
dismissed_by_vommand = 3,
undefined = 4
}
-- Legacy --TODO v5 remove this alias
ret.notificationClosedReason = ret.notification_closed_reason
return ret

File diff suppressed because it is too large Load Diff

View File

@ -25,11 +25,14 @@ local tcat = table.concat
local tins = table.insert local tins = table.insert
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local naughty = require("naughty.core") local naughty = require("naughty.core")
local cst = require("naughty.constants")
local nnotif = require("naughty.notification")
--- Notification library, dbus bindings --- Notification library, dbus bindings
local dbus = { config = {} } local dbus = { config = {} }
-- DBUS Notification constants -- DBUS Notification constants
-- https://developer.gnome.org/notification-spec/#urgency-levels
local urgency = { local urgency = {
low = "\0", low = "\0",
normal = "\1", normal = "\1",
@ -46,9 +49,9 @@ local urgency = {
-- @tfield table 3 critical urgency -- @tfield table 3 critical urgency
-- @table config.mapping -- @table config.mapping
dbus.config.mapping = { dbus.config.mapping = {
{{urgency = urgency.low}, naughty.config.presets.low}, {{urgency = urgency.low}, cst.config.presets.low},
{{urgency = urgency.normal}, naughty.config.presets.normal}, {{urgency = urgency.normal}, cst.config.presets.normal},
{{urgency = urgency.critical}, naughty.config.presets.critical} {{urgency = urgency.critical}, cst.config.presets.critical}
} }
local function sendActionInvoked(notificationId, action) local function sendActionInvoked(notificationId, action)
@ -140,7 +143,7 @@ capi.dbus.connect_signal("org.freedesktop.Notifications",
args.preset = gtable.join(args.preset, preset) args.preset = gtable.join(args.preset, preset)
end end
end end
local preset = args.preset or naughty.config.defaults local preset = args.preset or cst.config.defaults
local notification local notification
if actions then if actions then
args.actions = {} args.actions = {}
@ -152,12 +155,12 @@ capi.dbus.connect_signal("org.freedesktop.Notifications",
if action_id == "default" then if action_id == "default" then
args.run = function() args.run = function()
sendActionInvoked(notification.id, "default") sendActionInvoked(notification.id, "default")
naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser) notification:destroy(cst.notification_closed_reason.dismissed_by_user)
end end
elseif action_id ~= nil and action_text ~= nil then elseif action_id ~= nil and action_text ~= nil then
args.actions[action_text] = function() args.actions[action_text] = function()
sendActionInvoked(notification.id, action_id) sendActionInvoked(notification.id, action_id)
naughty.destroy(notification, naughty.notificationClosedReason.dismissedByUser) notification:destroy(cst.notification_closed_reason.dismissed_by_user)
end end
end end
end end
@ -190,14 +193,14 @@ capi.dbus.connect_signal("org.freedesktop.Notifications",
args.timeout = expire / 1000 args.timeout = expire / 1000
end end
args.freedesktop_hints = hints args.freedesktop_hints = hints
notification = naughty.notify(args) notification = nnotif(args)
return "u", notification.id return "u", notification.id
end end
return "u", "0" return "u", "0"
elseif data.member == "CloseNotification" then elseif data.member == "CloseNotification" then
local obj = naughty.getById(appname) local obj = naughty.get_by_id(appname)
if obj then if obj then
naughty.destroy(obj, naughty.notificationClosedReason.dismissedByCommand) obj:destroy(cst.notification_closed_reason.dismissed_by_command)
end end
elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then
-- name of notification app, name of vender, version, specification version -- name of notification app, name of vender, version, specification version

View File

@ -9,6 +9,9 @@ if dbus then
naughty.dbus = require("naughty.dbus") naughty.dbus = require("naughty.dbus")
end end
naughty.layout = require("naughty.layout")
naughty.notification = require("naughty.notification")
return naughty return naughty
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 -- 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,544 @@
----------------------------------------------------------------------------
--- A notification popup widget.
--
-- This is the legacy notification widget. It was the default until Awesome
-- v4.3 but is now being deprecated in favor of a more flexible widget.
--
-- The reason for this is/was that this widget is inflexible and mutate the
-- state of the notification object in a way that hinder other notification
-- widgets.
--
-- If no other notification widget is specified, Awesome fallback to this
-- widget.
--
--@DOC_naughty_actions_EXAMPLE@
--
-- @author koniu <gkusnierz@gmail.com>
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
-- @copyright 2008 koniu
-- @copyright 2017 Emmanuel Lepage Vallee
-- @classmod naughty.layout.legacy
----------------------------------------------------------------------------
local capi = { screen = screen, awesome = awesome }
local naughty = require("naughty.core")
local screen = require("awful.screen")
local button = require("awful.button")
local beautiful = require("beautiful")
local surface = require("gears.surface")
local gtable = require("gears.table")
local wibox = require("wibox")
local gfs = require("gears.filesystem")
local timer = require("gears.timer")
local gmath = require("gears.math")
local cairo = require("lgi").cairo
local util = require("awful.util")
local function get_screen(s)
return s and capi.screen[s]
end
-- This is a copy of the table found in `naughty.core`. The reason the copy
-- exists is to make sure there is only unidirectional coupling between the
-- legacy widget (this class) and `naughty.core`. Exposing the "raw"
-- notification list is also a bad design and might cause indices and position
-- corruption. While it cannot be removed from the public API (yet), it can at
-- least be blacklisted internally.
local current_notifications = setmetatable({}, {__mode = "k"})
screen.connect_for_each_screen(function(s)
current_notifications[s] = {
top_left = {},
top_middle = {},
top_right = {},
bottom_left = {},
bottom_middle = {},
bottom_right = {},
}
end)
-- Counter for the notifications
-- Required for later access via DBUS
local counter = 1
--- Evaluate desired position of the notification by index - internal
--
-- @param s Screen to use
-- @param position top_right | top_left | bottom_right | bottom_left
-- | top_middle | bottom_middle
-- @param idx Index of the notification
-- @param[opt] width Popup width.
-- @param height Popup height
-- @return Absolute position and index in { x = X, y = Y, idx = I } table
local function get_offset(s, position, idx, width, height)
s = get_screen(s)
local ws = s.workarea
local v = {}
idx = idx or #current_notifications[s][position] + 1
width = width or current_notifications[s][position][idx].width
-- calculate x
if position:match("left") then
v.x = ws.x + naughty.config.padding
elseif position:match("middle") then
v.x = ws.x + (ws.width / 2) - (width / 2)
else
v.x = ws.x + ws.width - (width + naughty.config.padding)
end
-- calculate existing popups' height
local existing = 0
for i = 1, idx-1, 1 do
local n = current_notifications[s][position][i]
-- `n` will not nil when there is too many notifications to fit in `s`
if n then
existing = existing + n.height + naughty.config.spacing
end
end
-- calculate y
if position:match("top") then
v.y = ws.y + naughty.config.padding + existing
else
v.y = ws.y + ws.height - (naughty.config.padding + height + existing)
end
-- Find old notification to replace in case there is not enough room.
-- This tries to skip permanent notifications (without a timeout),
-- e.g. critical ones.
local find_old_to_replace = function()
for i = 1, idx-1 do
local n = current_notifications[s][position][i]
if n.timeout > 0 then
return n
end
end
-- Fallback to first one.
return current_notifications[s][position][1]
end
-- if positioned outside workarea, destroy oldest popup and recalculate
if v.y + height > ws.y + ws.height or v.y < ws.y then
local n = find_old_to_replace()
if n then
n:destroy(naughty.notification_closed_reason.too_many_on_screen)
end
v = get_offset(s, position, idx, width, height)
end
return v
end
local escape_pattern = "[<>&]"
local escape_subs = { ['<'] = "&lt;", ['>'] = "&gt;", ['&'] = "&amp;" }
-- Cache the markup
local function set_escaped_text(self)
local text, title = self.text or "", self.title or ""
if title then title = title .. "\n" else title = "" end
local textbox = self.textbox
local function set_markup(pattern, replacements)
return textbox:set_markup_silently(string.format('<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.text or preset.text
local title = args.title or preset.title
local s = get_screen(args.screen or preset.screen or screen.focused())
if not s then
local err = "naughty.notify: there is no screen available to display the following notification:"
err = string.format("%s title='%s' text='%s'", err, tostring(title or ""), tostring(text or ""))
require("gears.debug").print_warning(err)
return
end
local timeout = args.timeout or preset.timeout
local icon = args.icon or preset.icon
local icon_size = args.icon_size or preset.icon_size
or beautiful.notification_icon_size
local ontop = args.ontop or preset.ontop
local hover_timeout = args.hover_timeout or preset.hover_timeout
local position = args.position or preset.position
local actions = args.actions
local destroy_cb = args.destroy
notification.screen = s
notification.destroy_cb = destroy_cb
notification.timeout = timeout
-- beautiful
local font = args.font or preset.font or beautiful.notification_font or
beautiful.font or capi.awesome.font
local fg = args.fg or preset.fg or
beautiful.notification_fg or beautiful.fg_normal or '#ffffff'
local bg = args.bg or preset.bg or
beautiful.notification_bg or beautiful.bg_normal or '#535d6c'
local border_color = args.border_color or preset.border_color or
beautiful.notification_border_color or beautiful.bg_focus or '#535d6c'
local border_width = args.border_width or preset.border_width or
beautiful.notification_border_width
local shape = args.shape or preset.shape or
beautiful.notification_shape
local width = args.width or preset.width or
beautiful.notification_width
local height = args.height or preset.height or
beautiful.notification_height
local max_width = args.max_width or preset.max_width or
beautiful.notification_max_width
local max_height = args.max_height or preset.max_height or
beautiful.notification_max_height
local margin = args.margin or preset.margin or
beautiful.notification_margin
local opacity = args.opacity or preset.opacity or
beautiful.notification_opacity
-- replace notification if needed
local reuse_box
if args.replaces_id then
local obj = naughty.get_by_id(args.replaces_id)
if obj then
-- destroy this and ...
naughty.destroy(obj, naughty.notification_closed_reason.silent, true)
reuse_box = obj.box
end
-- ... may use its ID
if args.replaces_id <= counter then
notification.id = args.replaces_id
else
counter = counter + 1
notification.id = counter
end
else
-- get a brand new ID
counter = counter + 1
notification.id = counter
end
notification.position = position
-- hook destroy
notification.timeout = timeout
local die = notification.die
local run = function ()
if args.run then
args.run(notification)
else
die(naughty.notification_closed_reason.dismissed_by_user)
end
end
local hover_destroy = function ()
if hover_timeout == 0 then
die(naughty.notification_closed_reason.expired)
else
if notification.timer then notification.timer:stop() end
notification.timer = timer { timeout = hover_timeout }
notification.timer:connect_signal("timeout", function() die(naughty.notification_closed_reason.expired) end)
notification.timer:start()
end
end
-- create textbox
local textbox = wibox.widget.textbox()
local marginbox = wibox.container.margin()
marginbox:set_margins(margin)
marginbox:set_widget(textbox)
textbox:set_valign("middle")
textbox:set_font(font)
notification.textbox = textbox
set_escaped_text(notification)
local actionslayout = wibox.layout.fixed.vertical()
local actions_max_width = 0
local actions_total_height = 0
if actions then
for action, callback in pairs(actions) do
local actiontextbox = wibox.widget.textbox()
local actionmarginbox = wibox.container.margin()
actionmarginbox:set_margins(margin)
actionmarginbox:set_widget(actiontextbox)
actiontextbox:set_valign("middle")
actiontextbox:set_font(font)
actiontextbox:set_markup(string.format('☛ <u>%s</u>', action))
-- calculate the height and width
local w, h = actiontextbox:get_preferred_size(s)
local action_height = h + 2 * margin
local action_width = w + 2 * margin
actionmarginbox:buttons(gtable.join(
button({ }, 1, callback),
button({ }, 3, callback)
))
actionslayout:add(actionmarginbox)
actions_total_height = actions_total_height + action_height
if actions_max_width < action_width then
actions_max_width = action_width
end
end
end
local size_info = {
width = width,
height = height,
max_width = max_width,
max_height = max_height,
margin = margin,
border_width = border_width,
actions_max_width = actions_max_width,
actions_total_height = actions_total_height,
}
-- create iconbox
local iconbox = nil
local iconmargin = nil
if icon then
-- Is this really an URI instead of a path?
if type(icon) == "string" and string.sub(icon, 1, 7) == "file://" then
icon = string.sub(icon, 8)
-- urldecode URI path
icon = string.gsub(icon, "%%(%x%x)", function(x) return string.char(tonumber(x, 16)) end )
end
-- try to guess icon if the provided one is non-existent/readable
if type(icon) == "string" and not gfs.file_readable(icon) then
icon = util.geticonpath(icon, naughty.config.icon_formats, naughty.config.icon_dirs, icon_size) or icon
end
-- is the icon file readable?
icon = surface.load_uncached(icon)
-- if we have an icon, use it
if icon then
iconbox = wibox.widget.imagebox()
iconmargin = wibox.container.margin(iconbox, margin, margin, margin, margin)
if icon_size and (icon:get_height() > icon_size or icon:get_width() > icon_size) then
size_info.icon_scale_factor = icon_size / math.max(icon:get_height(),
icon:get_width())
size_info.icon_w = icon:get_width () * size_info.icon_scale_factor
size_info.icon_h = icon:get_height() * size_info.icon_scale_factor
local scaled =
cairo.ImageSurface(cairo.Format.ARGB32,
gmath.round(size_info.icon_w),
gmath.round(size_info.icon_h))
local cr = cairo.Context(scaled)
cr:scale(size_info.icon_scale_factor, size_info.icon_scale_factor)
cr:set_source_surface(icon, 0, 0)
cr:paint()
icon = scaled
else
size_info.icon_w = icon:get_width ()
size_info.icon_h = icon:get_height()
end
iconbox:set_resize(false)
iconbox:set_image(icon)
end
end
notification.iconbox = iconbox
-- create container wibox
notification.box = wibox({ fg = fg,
bg = bg,
border_color = border_color,
border_width = border_width,
shape_border_color = shape and border_color,
shape_border_width = shape and border_width,
shape = shape,
type = "notification" })
if reuse_box then
notification.box = reuse_box
end
if hover_timeout then notification.box:connect_signal("mouse::enter", hover_destroy) end
notification.size_info = size_info
-- position the wibox
update_size(notification)
notification.box.ontop = ontop
notification.box.opacity = opacity
notification.box.visible = true
-- populate widgets
local layout = wibox.layout.fixed.horizontal()
if iconmargin then
layout:add(iconmargin)
end
layout:add(marginbox)
local completelayout = wibox.layout.fixed.vertical()
completelayout:add(layout)
completelayout:add(actionslayout)
notification.box:set_widget(completelayout)
-- Setup the mouse events
layout:buttons(gtable.join(button({}, 1, nil, run),
button({}, 3, nil, function()
die(naughty.notification_closed_reason.dismissed_by_user)
end)))
-- insert the notification to the table
table.insert(current_notifications[s][notification.position], notification)
if naughty.suspended and not args.ignore_suspend then
notification.box.visible = false
end
end
naughty.connect_signal("request::display", naughty.default_notification_handler)

View File

@ -0,0 +1,416 @@
---------------------------------------------------------------------------
--- A notification object.
--
-- This class creates individual notification objects that can be manipulated
-- to extend the default behavior.
--
-- This class doesn't define the actual widget, but is rather intended as a data
-- object to hold the properties. All examples assume the default widgets, but
-- the whole implementation can be replaced.
--
--@DOC_naughty_actions_EXAMPLE@
--
-- @author Emmanuel Lepage Vallee
-- @copyright 2008 koniu
-- @copyright 2017 Emmanuel Lepage Vallee
-- @classmod naughty.notification
---------------------------------------------------------------------------
local gobject = require("gears.object")
local gtable = require("gears.table")
local timer = require("gears.timer")
local cst = require("naughty.constants")
local naughty = require("naughty.core")
local notification = {}
--- Notifications font.
-- @beautiful beautiful.notification_font
-- @tparam string|lgi.Pango.FontDescription notification_font
--- Notifications background color.
-- @beautiful beautiful.notification_bg
-- @tparam color notification_bg
--- Notifications foreground color.
-- @beautiful beautiful.notification_fg
-- @tparam color notification_fg
--- Notifications border width.
-- @beautiful beautiful.notification_border_width
-- @tparam int notification_border_width
--- Notifications border color.
-- @beautiful beautiful.notification_border_color
-- @tparam color notification_border_color
--- Notifications shape.
-- @beautiful beautiful.notification_shape
-- @tparam[opt] gears.shape notification_shape
-- @see gears.shape
--- Notifications opacity.
-- @beautiful beautiful.notification_opacity
-- @tparam[opt] int notification_opacity
--- Notifications margin.
-- @beautiful beautiful.notification_margin
-- @tparam int notification_margin
--- Notifications width.
-- @beautiful beautiful.notification_width
-- @tparam int notification_width
--- Notifications height.
-- @beautiful beautiful.notification_height
-- @tparam int notification_height
--- Unique identifier of the notification.
-- This is the equivalent to a PID as allows external applications to select
-- notifications.
-- @property text
-- @param string
-- @see title
--- Text of the notification.
-- @property text
-- @param string
-- @see title
--- Title of the notification.
--@DOC_naughty_helloworld_EXAMPLE@
-- @property title
-- @param string
--- Time in seconds after which popup expires.
-- Set 0 for no timeout.
-- @property timeout
-- @param number
--- Delay in seconds after which hovered popup disappears.
-- @property hover_timeout
-- @param number
--- Target screen for the notification.
-- @property screen
-- @param screen
--- Corner of the workarea displaying the popups.
--
-- The possible values are:
--
-- * *top_right*
-- * *top_left*
-- * *bottom_left*
-- * *bottom_right*
-- * *top_middle*
-- * *bottom_middle*
--
--@DOC_awful_notification_corner_EXAMPLE@
--
-- @property position
-- @param string
--- Boolean forcing popups to display on top.
-- @property ontop
-- @param boolean
--- Popup height.
-- @property height
-- @param number
--- Popup width.
-- @property width
-- @param number
--- Notification font.
--@DOC_naughty_colors_EXAMPLE@
-- @property font
-- @param string
--- Path to icon.
-- @property icon
-- @tparam string|surface icon
--- Desired icon size in px.
-- @property icon_size
-- @param number
--- Foreground color.
-- @property fg
-- @tparam string|color|pattern fg
-- @see title
-- @see gears.color
--- Background color.
-- @property bg
-- @tparam string|color|pattern bg
-- @see title
-- @see gears.color
--- Border width.
-- @property border_width
-- @param number
-- @see title
--- Border color.
-- @property border_color
-- @param string
-- @see title
-- @see gears.color
--- Widget shape.
--@DOC_naughty_shape_EXAMPLE@
-- @property shape
--- Widget opacity.
-- @property opacity
-- @param number From 0 to 1
--- Widget margin.
-- @property margin
-- @tparam number|table margin
-- @see shape
--- Function to run on left click.
-- @property run
-- @param function
--- Function to run when notification is destroyed.
-- @property destroy
-- @param function
--- Table with any of the above parameters.
-- args will override ones defined
-- in the preset.
-- @property preset
-- @param table
--- Replace the notification with the given ID.
-- @property replaces_id
-- @param number
--- Function that will be called with all arguments.
-- The notification will only be displayed if the function returns true.
-- Note: this function is only relevant to notifications sent via dbus.
-- @property callback
-- @param function
--- A table containing strings that represents actions to buttons.
--
-- The table key (a number) is used by DBus to set map the action.
--
-- @property actions
-- @param table
--- Ignore this notification, do not display.
--
-- Note that this property has to be set in a `preset` or in a `request::preset`
-- handler.
--
-- @property ignore
-- @param boolean
--- Tell if the notification is currently suspended (read only).
--
-- This is always equal to `naughty.suspended`
--@property suspended
--@param boolean
--- Emitted when the notification is destroyed.
-- @signal destroyed
-- . --FIXME needs a description
-- @property ignore_suspend If set to true this notification
-- will be shown even if notifications are suspended via `naughty.suspend`.
--FIXME remove the screen attribute, let the handlers decide
-- document all handler extra properties
--FIXME add methods such as persist
--- Destroy notification by notification object
--
-- @tparam string reason One of the reasons from `notification_closed_reason`
-- @tparam[opt=false] boolean keep_visible If true, keep the notification visible
-- @return True if the popup was successfully destroyed, nil otherwise
function notification:destroy(reason, keep_visible)
self:emit_signal("destroyed")
return true
end
--- Set new notification timeout.
-- @tparam number new_timeout Time in seconds after which notification disappears.
function notification:reset_timeout(new_timeout)
if self.timer then self.timer:stop() end
local timeout = new_timeout or self.timeout
self:set_timeout(self, timeout)
self.timeout = timeout
self.timer:start()
end
function notification:set_id(new_id)
assert(self._private.id == nil, "Notification identifier can only be set once")
self._private.id = new_id
self:emit_signal("property::id", new_id)
end
function notification:set_timeout(timeout)
local die = function (reason)
self:destroy(reason)
end
if self.timer and self._private.timeout == timeout then return end
if timeout > 0 then
local timer_die = timer { timeout = timeout }
timer_die:connect_signal("timeout", function() die(cst.notification_closed_reason.expired) end)
if not self.suspended then --FIXME there's still a dependency loop to fix before it works
timer_die:start()
end
-- Prevent a memory leak and the accumulation of active timers
if self.timer and self.timer.started then
self.timer:stop()
end
self.timer = timer_die
end
self.die = die
self._private.timeout = timeout
end
local properties = {
"text" , "title" , "timeout" , "hover_timeout" ,
"screen" , "position", "ontop" , "border_width" ,
"width" , "font" , "icon" , "icon_size" ,
"fg" , "bg" , "height" , "border_color" ,
"shape" , "opacity" , "margin" , "ignore_suspend",
"destroy" , "preset" , "callback", "replaces_id" ,
"actions" , "run" , "id" , "ignore" ,
}
for _, prop in ipairs(properties) do
notification["get_"..prop] = notification["get_"..prop] or function(self)
-- It's possible this could be called from the `request::preset` handler.
-- `rawget()` is necessary to avoid a stack overflow.
local preset = rawget(self, "preset")
return self._private[prop]
or (preset and preset[prop])
or cst.config.defaults[prop]
end
notification["set_"..prop] = notification["set_"..prop] or function(self, value)
self._private[prop] = value
self:emit_signal("property::"..prop, value)
return
end
end
--- Create a notification.
--
-- @tab args The argument table containing any of the arguments below.
-- @string[opt=""] args.text Text of the notification.
-- @string[opt] args.title Title of the notification.
-- @int[opt=5] args.timeout Time in seconds after which popup expires.
-- Set 0 for no timeout.
-- @int[opt] args.hover_timeout Delay in seconds after which hovered popup disappears.
-- @tparam[opt=focused] integer|screen args.screen Target screen for the notification.
-- @string[opt="top_right"] args.position Corner of the workarea displaying the popups.
-- Values: `"top_right"`, `"top_left"`, `"bottom_left"`,
-- `"bottom_right"`, `"top_middle"`, `"bottom_middle"`.
-- @bool[opt=true] args.ontop Boolean forcing popups to display on top.
-- @int[opt=`beautiful.notification_height` or auto] args.height Popup height.
-- @int[opt=`beautiful.notification_width` or auto] args.width Popup width.
-- @string[opt=`beautiful.notification_font` or `beautiful.font` or `awesome.font`] args.font Notification font.
-- @string[opt] args.icon Path to icon.
-- @int[opt] args.icon_size Desired icon size in px.
-- @string[opt=`beautiful.notification_fg` or `beautiful.fg_focus` or `'#ffffff'`] args.fg Foreground color.
-- @string[opt=`beautiful.notification_fg` or `beautiful.bg_focus` or `'#535d6c'`] args.bg Background color.
-- @int[opt=`beautiful.notification_border_width` or 1] args.border_width Border width.
-- @string[opt=`beautiful.notification_border_color` or
-- `beautiful.border_focus` or `'#535d6c'`] args.border_color Border color.
-- @tparam[opt=`beautiful.notification_shape`] gears.shape args.shape Widget shape.
-- @tparam[opt=`beautiful.notification_opacity`] gears.opacity args.opacity Widget opacity.
-- @tparam[opt=`beautiful.notification_margin`] gears.margin args.margin Widget margin.
-- @tparam[opt] func args.run Function to run on left click. The notification
-- object will be passed to it as an argument.
-- You need to call e.g.
-- `notification.die(naughty.notification_closed_reason.dismissedByUser)` from
-- there to dismiss the notification yourself.
-- @tparam[opt] func args.destroy Function to run when notification is destroyed.
-- @tparam[opt] table args.preset Table with any of the above parameters.
-- Note: Any parameters specified directly in args will override ones defined
-- in the preset.
-- @tparam[opt] int args.replaces_id Replace the notification with the given ID.
-- @tparam[opt] func args.callback Function that will be called with all arguments.
-- The notification will only be displayed if the function returns true.
-- Note: this function is only relevant to notifications sent via dbus.
-- @tparam[opt] table args.actions Mapping that maps a string to a callback when this
-- action is selected.
-- @bool[opt=false] args.ignore_suspend If set to true this notification
-- will be shown even if notifications are suspended via `naughty.suspend`.
-- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 })
-- @treturn ?table The notification object, or nil in case a notification was
-- not displayed.
-- @function naughty.notification
local function create(args)
if cst.config.notify_callback then
args = cst.config.notify_callback(args)
if not args then return end
end
local n = gobject {
enable_properties = true,
}
assert(naughty.emit_signal)
-- Make sure all signals bubble up
n:_connect_everything(naughty.emit_signal)
-- Avoid modifying the original table
local private = {}
-- gather variables together
rawset(n, "preset", gtable.join(
cst.config.defaults or {},
args.preset or cst.config.presets.normal or {},
rawget(n, "preset") or {}
))
for k, v in pairs(n.preset) do
private[k] = v
end
for k, v in pairs(args) do
private[k] = v
end
rawset(n, "_private", private)
gtable.crush(n, notification, true)
-- Allow extensions to create override the preset with custom data
naughty.emit_signal("request::preset", n, args)
-- Register the notification before requesting a widget
n:emit_signal("new", args)
-- Let all listeners handle the actual visual aspects
if (not n.ignore) and (not n.preset.ignore) then
naughty.emit_signal("request::display", n, args)
end
-- Because otherwise the setter logic would not be executed
if n._private.timeout then
n:set_timeout(n._private.timeout or n.preset.timeout)
end
return n
end
return setmetatable(notification, {__call = function(_, ...) return create(...) end})

View File

@ -1,7 +1,7 @@
local awful = { keygrabber = require("awful.keygrabber") } --DOC_HIDE 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 local autostart_works = false --DOC_HIDE
@ -11,7 +11,7 @@ awful.keygrabber {
stop_callback = function(_, _, _, sequence) stop_callback = function(_, _, _, sequence)
autostart_works = true --DOC_HIDE autostart_works = true --DOC_HIDE
assert(sequence == "abc") --DOC_HIDE assert(sequence == "abc") --DOC_HIDE
naughty.notify{text="The keys were:"..sequence} naughty.notification {text="The keys were:"..sequence}
end, end,
} }

View File

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

View File

@ -18,7 +18,7 @@ local naughty = {} --DOC_HIDE
textbox = atextbox, textbox = atextbox,
exe_callback = function(input) exe_callback = function(input)
if not input or #input == 0 then return end if not input or #input == 0 then return end
naughty.notify{ text = "The input was: "..input } naughty.notification { text = "The input was: "..input }
end end
} }
end end

View File

@ -31,7 +31,7 @@ local steps = {
fake_screen.selected_tag.layout = max fake_screen.selected_tag.layout = max
-- Display a notification on the screen-to-be-removed -- Display a notification on the screen-to-be-removed
naughty.notify{ text = "test", screen = fake_screen } naughty.notification { text = "test", screen = fake_screen }
return true return true
end end