awesome/lib/naughty/notification.lua

559 lines
16 KiB
Lua

---------------------------------------------------------------------------
--- 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
-- @coreclassmod naughty.notification
---------------------------------------------------------------------------
local gobject = require("gears.object")
local gtable = require("gears.table")
local timer = require("gears.timer")
local cst = require("naughty.constants")
local naughty = require("naughty.core")
local gdebug = require("gears.debug")
local 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 id
-- @param string
-- @see title
-- Text of the notification [[deprecated]]
-- @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*
-- * *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
-- @param gears.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.
--
-- @method destroy
-- @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, false otherwise
function notification:destroy(reason, keep_visible)
if self._private.is_destroyed then
gdebug.print_warning("Trying to destroy the same notification twice. It"..
" was destroyed because: "..self._private.destroy_reason)
return false
end
reason = reason or cst.notification_closed_reason.dismissed_by_user
self:emit_signal("destroyed", reason, keep_visible)
self._private.is_destroyed = true
self._private.destroy_reason = reason
return true
end
--- Set new notification timeout.
-- @method reset_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 self.timer and 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")
local new_actions = {}
-- Does not attempt to handle when there is a mix of strings and objects
for idx, name in pairs(actions) do
local cb, old_idx = nil, idx
if type(name) == "function" then
cb = name
end
if type(idx) == "string" then
name, idx = idx, #actions+1
end
local a = naction {
position = idx,
name = name,
}
if cb then
a:connect_signal("invoked", cb)
end
new_actions[old_idx] = a
end
-- Yes, it modifies `args`, this is legacy code, cloning the args
-- just for this isn't worth it.
for old_idx, a in pairs(new_actions) do
actions[a.position] = a
actions[ old_idx ] = nil
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"`, `"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.
-- @constructorfct 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
assert(not args.id, "Identifiers cannot be specified externally")
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
n.id = notification._gen_next_id()
return n
end
-- This allows notification to be updated later.
local counter = 1
-- Identifier support.
function notification._gen_next_id()
counter = counter+1
return counter
end
--@DOC_object_COMMON@
return setmetatable(notification, {__call = function(_, ...) return create(...) end})