diff --git a/lib/awful/placement.lua b/lib/awful/placement.lua index fc79d5b9b..c67de4e95 100644 --- a/lib/awful/placement.lua +++ b/lib/awful/placement.lua @@ -1533,7 +1533,7 @@ function placement.next_to(d, args) geo.width, geo.height = dgeo.width, dgeo.height - fit = fit_in_bounding(pos.region.screen, geo, args) + fit = fit_in_bounding(pos.region, geo, args) if fit then break end end diff --git a/lib/awful/popup.lua b/lib/awful/popup.lua index a66a157b7..3309fca86 100644 --- a/lib/awful/popup.lua +++ b/lib/awful/popup.lua @@ -21,7 +21,7 @@ -- @popupmod awful.popup --------------------------------------------------------------------------- local wibox = require( "wibox" ) -local util = require( "awful.util" ) +local gtable = require( "gears.table" ) local placement = require( "awful.placement" ) local xresources= require("beautiful.xresources") local timer = require( "gears.timer" ) @@ -441,7 +441,7 @@ local function create_popup(_, args) enable_properties = true }) - util.table.crush(ii, main_widget, true) + gtable.crush(ii, main_widget, true) -- Create a wibox to host the widget local w = wibox(args or {}) @@ -453,7 +453,7 @@ local function create_popup(_, args) widget = child_widget }) - util.table.crush(w, popup) + gtable.crush(w, popup) ii:set_widget(child_widget) diff --git a/lib/gears/geometry.lua b/lib/gears/geometry.lua index 0ad73234f..d9a203039 100644 --- a/lib/gears/geometry.lua +++ b/lib/gears/geometry.lua @@ -80,9 +80,17 @@ end --- Return true whether rectangle B is in the right direction -- compared to rectangle A. --- @param dir The direction. --- @param gA The geometric specification for rectangle A. --- @param gB The geometric specification for rectangle B. +-- +-- The valid `dir` are: +-- +-- * up +-- * down +-- * left +-- * right +-- +-- @tparam string dir The direction. +-- @tparam table gA The geometric specification for rectangle A. +-- @tparam table gB The geometric specification for rectangle B. -- @return True if B is in the direction of A. local function is_in_direction(dir, gA, gB) if dir == "up" then @@ -100,9 +108,17 @@ end --- Calculate distance between two points. -- i.e: if we want to move to the right, we will take the right border -- of the currently focused screen and the left side of the checked screen. --- @param dir The direction. --- @param _gA The first rectangle. --- @param _gB The second rectangle. +-- +-- The valid `dir` are: +-- +-- * up +-- * down +-- * left +-- * right +-- +-- @tparam string dir The direction. +-- @tparam table _gA The first rectangle. +-- @tparam table _gB The second rectangle. -- @return The distance between the screens. local function calculate_distance(dir, _gA, _gB) local gAx = _gA.x @@ -158,6 +174,7 @@ end -- @tparam table a The area. -- @tparam table b The other area. -- @treturn boolean If the areas are identical. +-- @staticfct gears.geometry.rectangle.are_equal function gears.geometry.rectangle.are_equal(a, b) for _, v in ipairs {"x", "y", "width", "height"} do if a[v] ~= b[v] then return false end @@ -165,9 +182,28 @@ function gears.geometry.rectangle.are_equal(a, b) return true end +--- Return if rectangle `a` is within rectangle `b`. +-- +-- This includes the edges. 100% of `a` area has to be within `b` for this +-- function to return true. If you wish to know if any part of `a` intersect +-- with `b`, use `gears.geometry.rectangle.get_intersection`. +-- +-- @tparam table a The smaller area. +-- @tparam table b The larger area. +-- @treturn boolean If the areas are identical. +-- @staticfct gears.geometry.rectangle.is_inside +-- @see gears.geometry.rectangle.get_intersection +function gears.geometry.rectangle.is_inside(a, b) + return (a.x >= b.x + and a.y >= b.y + and a.x+a.width <= b.x + b.width + and a.y+a.height <= b.y + b.height + ) +end + --- Check if an area intersect another area. --- @param a The area. --- @param b The other area. +-- @tparam table a The area. +-- @tparam table b The other area. -- @return True if they intersect, false otherwise. -- @staticfct gears.geometry.rectangle.area_intersect_area function gears.geometry.rectangle.area_intersect_area(a, b) @@ -190,6 +226,7 @@ end -- @tparam number b.height The rectangle height -- @treturn table The intersect area. -- @staticfct gears.geometry.rectangle.get_intersection +-- @see gears.geometry.rectangle.is_inside function gears.geometry.rectangle.get_intersection(a, b) local g = {} g.x = math.max(a.x, b.x) diff --git a/lib/naughty/action.lua b/lib/naughty/action.lua index 3ed71114e..7a8755521 100644 --- a/lib/naughty/action.lua +++ b/lib/naughty/action.lua @@ -27,6 +27,7 @@ local action = {} -- The action name. -- @property name -- @tparam string name The name. +-- @propemits true false -- If the action is selected. -- @@ -35,14 +36,17 @@ local action = {} -- -- @property selected -- @param boolean +-- @propemits true false --- The action position (index). -- @property position -- @param number +-- @propemits true false --- The action icon. -- @property icon -- @tparam gears.surface|string icon +-- @propemits true false --- If the action should hide the label and only display the icon. -- @@ -50,6 +54,7 @@ local action = {} -- -- @property icon_only -- @param[opt=false] boolean +-- @propemits true false --- When a notification is invoked. -- @signal invoked diff --git a/lib/naughty/container/background.lua b/lib/naughty/container/background.lua index b6896b307..2d4796349 100644 --- a/lib/naughty/container/background.lua +++ b/lib/naughty/container/background.lua @@ -36,13 +36,14 @@ local function update_background(notif, wdg) wdg:set_bg(bg) wdg:set_shape(shape) -- otherwise there's no borders - wdg:set_shape_border_width(bw) - wdg:set_shape_border_color(bc) + wdg:set_border_width(bw) + wdg:set_border_color(bc) end --- The attached notification. -- @property notification -- @tparam naughty.notification notification +-- @propemits true false function background:set_notification(notif) if self._private.notification == notif then return end @@ -66,12 +67,16 @@ function background:set_notification(notif) notif:connect_signal("property::border_width", self._private.background_changed_callback) notif:connect_signal("property::border_color", self._private.background_changed_callback) notif:connect_signal("property::shape" , self._private.background_changed_callback) + self:emit_signal("property::notification", notif) end --- Create a new naughty.container.background. -- @tparam table args -- @tparam naughty.notification args.notification The notification. -- @constructorfct naughty.container.background +-- @usebeautiful beautiful.notification_border_width Fallback when the `border_width` property isn't set. +-- @usebeautiful beautiful.notification_border_color Fallback when the `border_color` property isn't set. +-- @usebeautiful beautiful.notification_shape Fallback when the `shape` property isn't set. local function new(args) args = args or {} diff --git a/lib/naughty/core.lua b/lib/naughty/core.lua index 497528790..bc10e46c8 100644 --- a/lib/naughty/core.lua +++ b/lib/naughty/core.lua @@ -102,6 +102,8 @@ gtable.crush(naughty, require("naughty.constants")) -- -- @property suspended -- @param boolean +-- @emits added +-- @propemits true false --- Do not allow notifications to auto-expire. -- @@ -111,6 +113,7 @@ gtable.crush(naughty, require("naughty.constants")) -- -- @property expiration_paused -- @param[opt=false] boolean +-- @propemits true false --- A table with all active notifications. -- @@ -122,6 +125,7 @@ gtable.crush(naughty, require("naughty.constants")) -- -- @property active -- @param table +-- @propemits false false --- True when there is a handler connected to `request::display`. -- @property has_display_handler @@ -134,6 +138,7 @@ gtable.crush(naughty, require("naughty.constants")) -- -- @property auto_reset_timeout -- @tparam[opt=true] boolean auto_reset_timeout +-- @propemits true false --- Enable or disable naughty ability to claim to support animations. -- @@ -143,6 +148,7 @@ gtable.crush(naughty, require("naughty.constants")) -- -- @property image_animations_enabled -- @param[opt=false] boolean +-- @propemits true false --- Enable or disable the persistent notifications. -- @@ -158,6 +164,7 @@ gtable.crush(naughty, require("naughty.constants")) -- -- @property persistence_enabled -- @param[opt=false] boolean +-- @propemits true false local properties = { suspended = false, @@ -196,6 +203,16 @@ screen.connect_for_each_screen(function(s) end) capi.screen.connect_signal("removed", function(scr) + -- Allow the notifications to be moved to another screen. + + for _, list in pairs(naughty.notifications[scr]) do + -- Clone the list to avoid having an iterator while mutating. + list = gtable.clone(list, false) + + for _, n in ipairs(list) do + naughty.emit_signal("request::screen", n, "removed", {}) + end + end -- Destroy all notifications on this screen naughty.destroy_all_notifications({scr}) naughty.notifications[scr] = nil @@ -210,6 +227,7 @@ local function remove_from_index(n) for _, ns in pairs(positions) do for k, n2 in ipairs(ns) do if n2 == n then + assert(ns[k+1] ~= n, "The notification index is corrupted") table.remove(ns, k) return end @@ -220,6 +238,11 @@ end -- When id or screen are set after the object is created, update the indexing. local function update_index(n) + -- Do things in the right order. + if not n._private.registered then return end + + assert(not n._private.is_destroyed, "The notification index is corrupted") + -- Find the only index and remove it (there's an useless loop, but it's small). remove_from_index(n) @@ -231,7 +254,6 @@ local function update_index(n) table.insert(naughty.notifications[s][n.position], n) end - --- Notification state. -- -- This function is deprecated, use `naughty.suspended`. @@ -357,6 +379,9 @@ function naughty.destroy_all_notifications(screens, reason) for _, scr in pairs(screens) do for _, list in pairs(naughty.notifications[scr]) do while #list > 0 do + -- Better cause an error than risk an infinite loop. + assert(not list[1]._private.is_destroyed) + ret = ret and list[1]:destroy(reason) end end @@ -406,6 +431,10 @@ function naughty.get__has_preset_handler() return conns["request::preset"] and #conns["request::preset"] > 0 or false end +function naughty._reset_display_handlers() + conns["request::display"] = nil +end + --- Set new notification timeout. -- -- This function is deprecated, use `notification:reset_timeout(new_timeout)`. @@ -500,6 +529,20 @@ function naughty.set_expiration_paused(p) end end +--- The default handler for `request::screen`. +-- +-- It selects `awful.screen.focused()`. +-- +-- @signalhandler naughty.default_screen_handler + +function naughty.default_screen_handler(n) + if n.screen and n.screen.valid then return end + + n.screen = screen.focused() +end + +naughty.connect_signal("request::screen", naughty.default_screen_handler) + --- Emitted when an error occurred and requires attention. -- @signal request::display_error -- @tparam string message The error message. @@ -546,15 +589,28 @@ end -- @tparam naughty.action action The action. -- @tparam string icon_name The icon name. +--- Emitted when the screen is not defined or being removed. +-- @signal request::screen +-- @tparam table notification The `naughty.notification` object. This is +-- currently either "new" or "removed". +-- @tparam string context Why is the signal sent. + -- Register a new notification object. local function register(notification, args) + assert(not notification._private.registered) + -- Add the some more properties rawset(notification, "get_suspended", get_suspended) - --TODO v5 uncouple the notifications and the screen - local s = get_screen(args.screen - or (notification.preset and notification.preset.screen) - or screen.focused()) + local s = get_screen(notification.screen or args.screen + or (notification.preset and notification.preset.screen)) + + if not s then + naughty.emit_signal("request::screen", notification, "new", {}) + s = notification.screen + end + + assert(s) -- insert the notification to the table table.insert(naughty._active, notification) @@ -562,6 +618,8 @@ local function register(notification, args) notification.idx = #naughty.notifications[s][notification.position] notification.screen = s + notification._private.registered = true + if properties.suspended and not args.ignore_suspend then notification._private.args = args table.insert(naughty.notifications.suspended, notification) @@ -599,7 +657,7 @@ local function set_index_miss(_, key, value) resume() end - naughty.emit_signal("property::"..key) + naughty.emit_signal("property::"..key, value) else rawset(naughty, key, value) end diff --git a/lib/naughty/layout/box.lua b/lib/naughty/layout/box.lua index 5e80dd5ca..ecbc365f8 100644 --- a/lib/naughty/layout/box.lua +++ b/lib/naughty/layout/box.lua @@ -10,13 +10,16 @@ -- @popupmod naughty.layout.box ---------------------------------------------------------------------------- +local capi = {screen=screen} local beautiful = require("beautiful") +local gtimer = require("gears.timer") local gtable = require("gears.table") local wibox = require("wibox") local popup = require("awful.popup") local awcommon = require("awful.widget.common") local placement = require("awful.placement") local abutton = require("awful.button") +local ascreen = require("awful.screen") local gpcall = require("gears.protected_call") local dpi = require("beautiful").xresources.apply_dpi @@ -26,11 +29,33 @@ local box, by_position = {}, {} -- Init the weak tables for each positions. It is done ahead of time rather -- than when notifications are added to simplify the code. -for _, pos in ipairs { "top_left" , "top_middle" , "top_right", - "bottom_left", "bottom_middle", "bottom_right" } do - by_position[pos] = setmetatable({},{__mode = "v"}) + +local function init_screen(s) + if not s.valid then return end + + if by_position[s] then return by_position[s] end + + by_position[s] = setmetatable({},{__mode = "k"}) + + for _, pos in ipairs { "top_left" , "top_middle" , "top_right", + "bottom_left", "bottom_middle", "bottom_right" } do + by_position[s][pos] = setmetatable({},{__mode = "v"}) + end + + return by_position[s] end +ascreen.connect_for_each_screen(init_screen) + +-- Manually cleanup to help the GC. +capi.screen.connect_signal("removed", function(scr) + -- By that time, all direct events should have been handled. Cleanup the + -- leftover. Being a weak table doesn't help Lua 5.1. + gtimer.delayed_call(function() + by_position[scr] = nil + end) +end) + local function get_spacing() local margin = beautiful.notification_spacing or 2 return {top = margin, bottom = margin} @@ -42,30 +67,32 @@ local function update_position(position) local align = position:match("_(.*)") :gsub("left", "front"):gsub("right", "back") - for k, wdg in ipairs(by_position[position]) do - local args = { - geometry = by_position[position][k-1], - preferred_positions = {pref }, - preferred_anchors = {align}, - margins = get_spacing(), - honor_workarea = true, - } + for _, pos in pairs(by_position) do + for k, wdg in ipairs(pos[position]) do + local args = { + geometry = pos[position][k-1], + preferred_positions = {pref }, + preferred_anchors = {align}, + margins = get_spacing(), + honor_workarea = true, + } - -- The first entry is aligned to the workarea, then the following to the - -- previous widget. - placement[k==1 and position:gsub("_middle", "") or "next_to"](wdg, args) + -- The first entry is aligned to the workarea, then the following to the + -- previous widget. + placement[k==1 and position:gsub("_middle", "") or "next_to"](wdg, args) - wdg.visible = true + wdg.visible = true + end end end local function finish(self) self.visible = false - assert(by_position[self.position]) + assert(init_screen(self.screen)[self.position]) - for k, v in ipairs(by_position[self.position]) do + for k, v in ipairs(init_screen(self.screen)[self.position]) do if v == self then - table.remove(by_position[self.position], k) + table.remove(init_screen(self.screen)[self.position], k) break end end @@ -73,6 +100,17 @@ local function finish(self) update_position(self.position) end +-- It isn't a good idea to use the `attach` `awful.placement` property. If the +-- screen is resized or the notification is moved, it causes side effects. +-- Better listen to geometry changes and reflow. +capi.screen.connect_signal("property::geometry", function(s) + for pos, notifs in pairs(by_position[s]) do + if #notifs > 0 then + update_position(pos) + end + end +end) + --- The maximum notification width. -- @beautiful beautiful.notification_max_width -- @tparam[opt=500] number notification_max_width @@ -92,8 +130,10 @@ end -- @tparam[opt="top_right"] string notification_position --- The widget notification object. +-- -- @property notification --- @param naughty.notification +-- @tparam naughty.notification notification +-- @propemits true false --- The widget template to construct the box content. -- @@ -135,6 +175,8 @@ end -- -- @property widget_template -- @param widget +-- @usebeautiful beautiful.notification_max_width The maximum width for the +-- resulting widget. local function generate_widget(args, n) local w = gpcall(wibox.widget.base.make_widget_from_value, @@ -192,12 +234,15 @@ local function init(self, notification) end end + local s = notification.screen + assert(s) + -- Add the notification to the active list - assert(by_position[position]) + assert(init_screen(s)[position], "Invalid position "..position) self:_apply_size_now() - table.insert(by_position[position], self) + table.insert(init_screen(s)[position], self) local function update() update_position(position) end @@ -220,6 +265,8 @@ function box:set_notification(notif) init(self, notif) self._private.notification = notif + + self:emit_signal("property::notification", notif) end function box:get_position() @@ -230,19 +277,41 @@ function box:get_position() return "top_right" end +--- Create a notification popup box. +-- +-- @constructorfct naughty.layout.box +-- @tparam[opt=nil] table args +-- @tparam table args.widget_template A widget definition template which will +-- be instantiated for each box. +-- @tparam naughty.notification args.notification The notification object. +-- @tparam string args.position The position. See `naughty.notification.position`. +--@DOC_wibox_constructor_COMMON@ +-- @usebeautiful beautiful.notification_position If `position` is not defined +-- in the notification object (or in this constructor). + local function new(args) + args = args or {} + -- Set the default wibox values local new_args = { ontop = true, visible = false, - bg = args and args.bg or beautiful.notification_bg, - fg = args and args.fg or beautiful.notification_fg, - shape = args and args.shape or beautiful.notification_shape, - border_width = args and args.border_width or beautiful.notification_border_width or 1, - border_color = args and args.border_color or beautiful.notification_border_color, + bg = args.bg or beautiful.notification_bg, + fg = args.fg or beautiful.notification_fg, + shape = args.shape or beautiful.notification_shape, + border_width = args.border_width or beautiful.notification_border_width or 1, + border_color = args.border_color or beautiful.notification_border_color, } - new_args = args and setmetatable(new_args, {__index = args}) or new_args + -- The C code needs `pairs` to work, so a full copy is required. + gtable.crush(new_args, args, true) + + -- Add a weak-table layer for the screen. + local weak_args = setmetatable({ + screen = args.notification and args.notification.screen or nil + }, {__mode="v"}) + + setmetatable(new_args, {__index = weak_args}) -- Generate the box before the popup is created to avoid the size changing new_args.widget = generate_widget(new_args, new_args.notification) @@ -276,6 +345,8 @@ local function new(args) abutton({ }, 3, hide) )) + gtable.crush(ret, box, false) + return ret end diff --git a/lib/naughty/list/actions.lua b/lib/naughty/list/actions.lua index ff9c8e5eb..34deaf68e 100644 --- a/lib/naughty/list/actions.lua +++ b/lib/naughty/list/actions.lua @@ -186,23 +186,47 @@ local actionlist = {} --- The actionlist parent notification. -- @property notification --- @param notification +-- @tparam naughty.notification notification +-- @propemits true false -- @see naughty.notification --- The actionlist layout. -- If no layout is specified, a `wibox.layout.fixed.horizontal` will be created -- automatically. --- @property layout --- @param widget +-- @property base_layout +-- @tparam widget base_layout +-- @propemits true false -- @see wibox.layout.fixed.horizontal --- The actionlist parent notification. -- @property widget_template --- @param table +-- @tparam table widget_template +-- @propemits true false --- A table with values to override each `beautiful.notification_action` values. -- @property style --- @param table +-- @tparam table style +-- @propemits true false +-- @usebeautiful beautiful.font Fallback when the `font` property isn't set. +-- @usebeautiful beautiful.notification_action_underline_normal Fallback. +-- @usebeautiful beautiful.notification_action_underline_selected Fallback. +-- @usebeautiful beautiful.notification_action_icon_only Fallback. +-- @usebeautiful beautiful.notification_action_label_only Fallback. +-- @usebeautiful beautiful.notification_action_shape_normal Fallback. +-- @usebeautiful beautiful.notification_action_shape_selected Fallback. +-- @usebeautiful beautiful.notification_action_shape_border_color_normal Fallback. +-- @usebeautiful beautiful.notification_action_shape_border_color_selected Fallback. +-- @usebeautiful beautiful.notification_action_shape_border_width_normal Fallback. +-- @usebeautiful beautiful.notification_action_shape_border_width_selected Fallback. +-- @usebeautiful beautiful.notification_action_icon_size_normal Fallback. +-- @usebeautiful beautiful.notification_action_icon_size_selected Fallback. +-- @usebeautiful beautiful.notification_action_bg_normal Fallback. +-- @usebeautiful beautiful.notification_action_bg_selected Fallback. +-- @usebeautiful beautiful.notification_action_fg_normal Fallback. +-- @usebeautiful beautiful.notification_action_fg_selected Fallback. +-- @usebeautiful beautiful.notification_action_bgimage_normal Fallback. +-- @usebeautiful beautiful.notification_action_bgimage_selected Fallback. + function actionlist:set_notification(notif) self._private.notification = notif @@ -215,6 +239,7 @@ function actionlist:set_notification(notif) self:emit_signal("widget::layout_changed") self:emit_signal("widget::redraw_needed") + self:emit_signal("property::notification", notif) end function actionlist:set_base_layout(layout) @@ -224,6 +249,7 @@ function actionlist:set_base_layout(layout) self:emit_signal("widget::layout_changed") self:emit_signal("widget::redraw_needed") + self:emit_signal("property::base_layout", layout) end function actionlist:set_widget_template(widget_template) @@ -236,6 +262,7 @@ function actionlist:set_widget_template(widget_template) self:emit_signal("widget::layout_changed") self:emit_signal("widget::redraw_needed") + self:emit_signal("property::widget_template", widget_template) end function actionlist:set_style(style) @@ -246,6 +273,7 @@ function actionlist:set_style(style) self:emit_signal("widget::layout_changed") self:emit_signal("widget::redraw_needed") + self:emit_signal("property::style", style) end function actionlist:get_notification() diff --git a/lib/naughty/list/notifications.lua b/lib/naughty/list/notifications.lua index 12e6d82ba..d5561fcaa 100644 --- a/lib/naughty/list/notifications.lua +++ b/lib/naughty/list/notifications.lua @@ -159,23 +159,48 @@ local notificationlist = {} --- The notificationlist parent notification. -- @property notification --- @param notification +-- @tparam naughty.notification notification +-- @propemits true false -- @see naughty.notification ---- The notificationlist layout. +--- A `wibox.layout` to be used to place the entries. +-- -- If no layout is specified, a `wibox.layout.fixed.vertical` will be created -- automatically. --- @property layout --- @param widget +-- +-- @property base_layout +-- @tparam widget base_layout +-- @propemits true false +-- @usebeautiful beautiful.notification_spacing +-- @see wibox.layout.fixed.horizontal -- @see wibox.layout.fixed.vertical +-- @see wibox.layout.flex.horizontal +-- @see wibox.layout.flex.vertical +-- @see wibox.layout.grid --- The notificationlist parent notification. -- @property widget_template --- @param table +-- @tparam table widget_template +-- @propemits true false --- A table with values to override each `beautiful.notification_action` values. -- @property style --- @param table +-- @tparam table style +-- @propemits true false +-- @usebeautiful beautiful.notification_shape_normal Fallback. +-- @usebeautiful beautiful.notification_shape_selected Fallback. +-- @usebeautiful beautiful.notification_shape_border_color_normal Fallback. +-- @usebeautiful beautiful.notification_shape_border_color_selected Fallback. +-- @usebeautiful beautiful.notification_shape_border_width_normal Fallback. +-- @usebeautiful beautiful.notification_shape_border_width_selected Fallback. +-- @usebeautiful beautiful.notification_icon_size_normal Fallback. +-- @usebeautiful beautiful.notification_icon_size_selected Fallback. +-- @usebeautiful beautiful.notification_bg_normal Fallback. +-- @usebeautiful beautiful.notification_bg_selected Fallback. +-- @usebeautiful beautiful.notification_fg_normal Fallback. +-- @usebeautiful beautiful.notification_fg_selected Fallback. +-- @usebeautiful beautiful.notification_bgimage_normal Fallback. +-- @usebeautiful beautiful.notification_bgimage_selected Fallback. function notificationlist:set_widget_template(widget_template) self._private.widget_template = widget_template @@ -187,6 +212,7 @@ function notificationlist:set_widget_template(widget_template) self:emit_signal("widget::layout_changed") self:emit_signal("widget::redraw_needed") + self:emit_signal("property::widget_template", widget_template) end function notificationlist:set_style(style) @@ -197,6 +223,7 @@ function notificationlist:set_style(style) self:emit_signal("widget::layout_changed") self:emit_signal("widget::redraw_needed") + self:emit_signal("property::style", style) end function notificationlist:layout(_, width, height) @@ -213,20 +240,12 @@ function notificationlist:fit(context, width, height) return wibox.widget.base.fit_widget(self, context, self._private.base_layout, width, height) end ---- A `wibox.layout` to be used to place the entries. --- @property base_layout --- @param widget --- @see wibox.layout.fixed.horizontal --- @see wibox.layout.fixed.vertical --- @see wibox.layout.flex.horizontal --- @see wibox.layout.flex.vertical --- @see wibox.layout.grid - --- A function to prevent some notifications from being added to the list. -- @property filter --- @param function +-- @tparam function filter +-- @propemits true false -for _, prop in ipairs { "filter", "client", "clients", "tag", "tags", "screen", "base_layout" } do +for _, prop in ipairs { "filter", "base_layout" } do notificationlist["set_"..prop] = function(self, value) self._private[prop] = value @@ -234,6 +253,7 @@ for _, prop in ipairs { "filter", "client", "clients", "tag", "tags", "screen", self:emit_signal("widget::layout_changed") self:emit_signal("widget::redraw_needed") + self:emit_signal("property::"..prop, value) end notificationlist["get_"..prop] = function(self) @@ -319,9 +339,9 @@ end module.filter = {} ---- --- @param n The notification. --- @return Always returns true because it doesn't filter anything at all. +--- All notifications. +-- @tparam naughty.notification n The notification. +-- @treturn boolean Always returns true because it doesn't filter anything at all. -- @filterfunction naughty.list.notifications.filter.all function module.filter.all(n) -- luacheck: no unused args return true @@ -333,9 +353,9 @@ end -- -- filter = function(n) return naughty.list.notifications.filter.most_recent(n, 3) end -- --- @param n The notification. +-- @tparam naughty.notification n The notification. -- @tparam[opt=1] number count The number of recent notifications to allow. --- @return Always returns true because it doesn't filter anything at all. +-- @treturn boolean Always returns true because it doesn't filter anything at all. -- @filterfunction naughty.list.notifications.filter.most_recent function module.filter.most_recent(n, count) for i=1, count or 1 do diff --git a/lib/naughty/notification.lua b/lib/naughty/notification.lua index 4d2fba6d7..f72d3227a 100644 --- a/lib/naughty/notification.lua +++ b/lib/naughty/notification.lua @@ -15,6 +15,7 @@ -- @copyright 2017 Emmanuel Lepage Vallee -- @coreclassmod naughty.notification --------------------------------------------------------------------------- +local capi = { screen = screen } local gobject = require("gears.object") local gtable = require("gears.table") local gsurface = require("gears.surface") @@ -75,7 +76,8 @@ local notification = {} -- This is the equivalent to a PID as allows external applications to select -- notifications. -- @property id --- @param number +-- @tparam number id +-- @propemits true false --- Text of the notification. -- @@ -89,12 +91,14 @@ local notification = {} --- Title of the notification. --@DOC_naughty_helloworld_EXAMPLE@ -- @property title --- @param string +-- @tparam string title +-- @propemits true false --- Time in seconds after which popup expires. -- Set 0 for no timeout. -- @property timeout --- @param number +-- @tparam number timeout +-- @propemits true false --- The notification urgency level. -- @@ -106,6 +110,7 @@ local notification = {} -- -- @property urgency -- @param string +-- @propemits true false --- The notification category. -- @@ -152,6 +157,7 @@ local notification = {} -- -- @property category -- @tparam string|nil category +-- @propemits true false --- True if the notification should be kept when an action is pressed. -- @@ -161,14 +167,17 @@ local notification = {} -- -- @property resident -- @param[opt=false] boolean +-- @propemits true false --- Delay in seconds after which hovered popup disappears. -- @property hover_timeout -- @param number +-- @propemits true false --- Target screen for the notification. -- @property screen -- @param screen +-- @propemits true false --- Corner of the workarea displaying the popups. -- @@ -186,6 +195,7 @@ local notification = {} -- -- @property position -- @param string +-- @propemits true false -- @see awful.placement.next_to --- Boolean forcing popups to display on top. @@ -198,17 +208,20 @@ local notification = {} -- -- @property height -- @param number +-- @propemits true false -- @see width --- Popup width. -- @property width -- @param number +-- @propemits true false -- @see height --- Notification font. --@DOC_naughty_colors_EXAMPLE@ -- @property font -- @param string +-- @propemits true false --- "All in one" way to access the default image or icon. -- @@ -228,12 +241,14 @@ local notification = {} -- -- @property icon -- @tparam string|surface icon +-- @propemits true false -- @see app_icon -- @see image --- Desired icon size in px. -- @property icon_size -- @param number +-- @propemits true false --- The icon provided in the `app_icon` field of the DBus notification. -- @@ -242,6 +257,7 @@ local notification = {} -- -- @property app_icon -- @param string +-- @propemits true false --- The notification image. -- @@ -251,6 +267,7 @@ local notification = {} -- -- @property image -- @tparam string|surface image +-- @propemits true false --- The notification (animated) images. -- @@ -262,6 +279,7 @@ local notification = {} -- -- @property images -- @tparam nil|table images +-- @propemits true false --- Foreground color. -- @@ -269,6 +287,7 @@ local notification = {} -- -- @property fg -- @tparam string|color|pattern fg +-- @propemits true false -- @see title -- @see gears.color @@ -278,13 +297,14 @@ local notification = {} -- -- @property bg -- @tparam string|color|pattern bg +-- @propemits true false -- @see title -- @see gears.color --- Border width. -- @property border_width -- @param number --- @see title +-- @propemits true false --- Border color. -- @@ -292,7 +312,7 @@ local notification = {} -- -- @property border_color -- @param string --- @see title +-- @propemits true false -- @see gears.color --- Widget shape. @@ -311,10 +331,12 @@ local notification = {} -- -- @property shape -- @tparam gears.shape shape +-- @propemits true false --- Widget opacity. -- @property opacity -- @tparam number opacity Between 0 to 1. +-- @propemits true false --- Widget margin. -- @@ -322,6 +344,7 @@ local notification = {} -- -- @property margin -- @tparam number|table margin +-- @propemits true false -- @see shape --- Function to run on left click. @@ -345,12 +368,14 @@ local notification = {} -- in the preset. -- @property preset -- @param table +-- @propemits true false --- 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 +-- @propemits true false --- A table containing strings that represents actions to buttons. -- @@ -358,6 +383,7 @@ local notification = {} -- -- @property actions -- @param table +-- @propemits true false --- Ignore this notification, do not display. -- @@ -366,16 +392,19 @@ local notification = {} -- -- @property ignore -- @param boolean +-- @propemits true false --- Tell if the notification is currently suspended (read only). -- -- This is always equal to `naughty.suspended` --@property suspended --@param boolean +-- @propemits true false --- If the notification is expired. -- @property is_expired -- @param boolean +-- @propemits true false -- @see naughty.expiration_paused --- If the timeout needs to be reset when a property changes. @@ -385,6 +414,7 @@ local notification = {} -- -- @property auto_reset_timeout -- @tparam[opt=true] boolean auto_reset_timeout +-- @propemits true false --- Emitted when the notification is destroyed. -- @signal destroyed @@ -415,6 +445,7 @@ local notification = {} -- -- @property[opt=500] max_width -- @param number +-- @propemits true false --- The application name specified by the notification. -- @@ -423,6 +454,7 @@ local notification = {} -- In these case, it helps to triage and detect the notification from the rules. -- @property app_name -- @param string +-- @propemits true false --- The widget template used to represent the notification. -- @@ -431,16 +463,18 @@ local notification = {} -- -- @property widget_template -- @param table - ---FIXME remove the screen attribute, let the handlers decide --- document all handler extra properties +-- @propemits true false --- 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 +-- @treturn boolean True if the popup was successfully destroyed, false otherwise. +-- @emits destroyed +-- @emitstparam destroyed integer reason The reason. +-- @emitstparam destroyed boolean keep_visible If the notification should be kept. +-- @see naughty.notification_closed_reason function notification:destroy(reason, keep_visible) if self._private.is_destroyed then gdebug.print_warning("Trying to destroy the same notification twice. It".. @@ -525,6 +559,7 @@ function notification:set_timeout(timeout) end self.die = die self._private.timeout = timeout + self:emit_signal("property::timeout", timeout) end function notification:set_text(txt) @@ -545,14 +580,14 @@ end local properties = { "message" , "title" , "timeout" , "hover_timeout" , - "screen" , "position", "ontop" , "border_width" , + "app_name", "position", "ontop" , "border_width" , "width" , "font" , "icon" , "icon_size" , "fg" , "bg" , "height" , "border_color" , "shape" , "opacity" , "margin" , "ignore_suspend", "destroy" , "preset" , "callback", "actions" , "run" , "id" , "ignore" , "auto_reset_timeout", "urgency" , "image" , "images" , "widget_template", - "max_width", "app_name", + "max_width", } for _, prop in ipairs(properties) do @@ -684,6 +719,23 @@ function notification:append_actions(new_actions) end +function notification:set_screen(s) + assert(not self._private.screen) + + s = s and capi.screen[s] or nil + + -- Avoid an infinite loop in the management code. + if s == self._private.weak_screen[1] then return end + + self._private.weak_screen = setmetatable({s}, {__mode="v"}) + + self:emit_signal("property::screen", s) +end + +function notification:get_screen() + return self._private.weak_screen[1] +end + --TODO v6: remove this local function convert_actions(actions) gdebug.deprecate( @@ -835,7 +887,7 @@ local function create(args) n:_connect_everything(naughty.emit_signal) -- Avoid modifying the original table - local private = {} + local private = {weak_screen = setmetatable({}, {__mode="v"})} rawset(n, "_private", private) -- Allow extensions to create override the preset with custom data @@ -848,7 +900,11 @@ local function create(args) end for k, v in pairs(args) do - private[k] = v + -- Don't keep a strong reference to the screen, Lua 5.1 GC wont be + -- smart enough to unwind the mess of circular weak references. + if k ~= "screen" then + private[k] = v + end end -- It's an automatic property diff --git a/lib/naughty/widget/icon.lua b/lib/naughty/widget/icon.lua index 4f7dbc400..971fb84b2 100644 --- a/lib/naughty/widget/icon.lua +++ b/lib/naughty/widget/icon.lua @@ -77,6 +77,7 @@ end --- The attached notification. -- @property notification -- @tparam naughty.notification notification +-- @propemits true false function icon:set_notification(notif) if self._private.notification == notif then return end @@ -95,6 +96,7 @@ function icon:set_notification(notif) self._private.notification = notif notif:connect_signal("property::icon", self._private.icon_changed_callback) + self:emit_signal("property::notification", notif) end local valid_strategies = { @@ -117,7 +119,11 @@ local valid_strategies = { --@DOC_wibox_nwidget_icon_strategy_EXAMPLE@ -- -- @property resize_strategy --- @param string +-- @tparam string resize_strategy +-- @propemits true false +-- @usebeautiful beautiful.notification_icon_resize_strategy The fallback when +-- there is no specified strategy. +-- @usebeautiful beautiful.notification_icon_size The size upper bound. function icon:set_resize_strategy(strategy) assert(valid_strategies[strategy], "Invalid strategy") @@ -125,6 +131,7 @@ function icon:set_resize_strategy(strategy) self._private.resize_strategy = strategy self:emit_signal("widget::redraw_needed") + self:emit_signal("property::resize_strategy", strategy) end diff --git a/lib/naughty/widget/message.lua b/lib/naughty/widget/message.lua index 350afcc95..c9f31fb75 100644 --- a/lib/naughty/widget/message.lua +++ b/lib/naughty/widget/message.lua @@ -36,6 +36,7 @@ end --- The attached notification. -- @property notification -- @tparam naughty.notification notification +-- @propemits true false function message:set_notification(notif) if self._private.notification == notif then return end @@ -53,12 +54,15 @@ function message:set_notification(notif) notif:connect_signal("property::message", self._private.message_changed_callback) notif:connect_signal("property::fg" , self._private.message_changed_callback) + self:emit_signal("property::notification", notif) end --- Create a new naughty.widget.message. -- @tparam table args -- @tparam naughty.notification args.notification The notification. -- @constructorfct naughty.widget.message +-- @usebeautiful beautiful.notification_fg +-- @usebeautiful beautiful.notification_font local function new(args) args = args or {} diff --git a/lib/naughty/widget/title.lua b/lib/naughty/widget/title.lua index 78a804baa..38ce73270 100644 --- a/lib/naughty/widget/title.lua +++ b/lib/naughty/widget/title.lua @@ -36,6 +36,7 @@ end --- The attached notification. -- @property notification -- @tparam naughty.notification notification +-- @propemits true false function title:set_notification(notif) if self._private.notification == notif then return end @@ -54,12 +55,15 @@ function title:set_notification(notif) notif:connect_signal("property::title", self._private.title_changed_callback) notif:connect_signal("property::fg" , self._private.title_changed_callback) + self:emit_signal("property::notification", notif) end --- Create a new naughty.widget.title. -- @tparam table args -- @tparam naughty.notification args.notification The notification. -- @constructorfct naughty.widget.title +-- @usebeautiful beautiful.notification_fg +-- @usebeautiful beautiful.notification_font local function new(args) args = args or {} diff --git a/spec/gears/geometry_spec.lua b/spec/gears/geometry_spec.lua index 1c1574927..71d81878c 100644 --- a/spec/gears/geometry_spec.lua +++ b/spec/gears/geometry_spec.lua @@ -145,6 +145,79 @@ describe("gears.geometry", function() end) end) + describe("rectangle.is_inside", function() + + it("equality1", function() + assert.is_true(geo.rectangle.is_inside( + {x=0, y=0, width=10, height=10}, + {x=0, y=0, width=10, height=10} + )) + end) + + it("equality2", function() + assert.is_true(geo.rectangle.is_inside( + {x=10, y=10, width=10, height=10}, + {x=10, y=10, width=10, height=10} + )) + end) + + it("top left edge", function() + assert.is_true(geo.rectangle.is_inside( + {x=0, y=0, width=5 , height=5 }, + {x=0, y=0, width=10, height=10} + )) + end) + + it("bottom right edge", function() + assert.is_true(geo.rectangle.is_inside( + {x=5, y=5, width=5 , height=5 }, + {x=0, y=0, width=10, height=10} + )) + end) + + it("middle", function() + assert.is_true(geo.rectangle.is_inside( + {x=2.5, y=2.5, width=5 , height=5 }, + {x=0 , y=0 , width=10, height=10} + )) + end) + + it("edge overflow", function() + assert.is_false(geo.rectangle.is_inside( + {x=0, y=0, width=11, height=11}, + {x=0, y=0, width=10, height=10} + )) + end) + + it("middle overflow", function() + assert.is_false(geo.rectangle.is_inside( + {x=2.5, y=2.5, width=11, height=11}, + {x=0 , y=0 , width=10, height=10} + )) + end) + + it("top left outside", function() + assert.is_false(geo.rectangle.is_inside( + {x=-10, y=-10, width=11, height=11}, + {x=0 , y=0 , width=10, height=10} + )) + end) + + it("no intersect top left", function() + assert.is_false(geo.rectangle.is_inside( + {x=-10, y=-10, width=5 , height=5 }, + {x=0 , y=0 , width=10, height=10} + )) + end) + + it("no intersect bottom right", function() + assert.is_false(geo.rectangle.is_inside( + {x=11, y=11, width=5 , height=5 }, + {x=0 , y=0 , width=10, height=10} + )) + end) + end) + describe("rectangle.area_remove", function() -- TODO perhaps it would be better to compare against a cairo.region -- than to have this overly specific tests? diff --git a/tests/test-naughty-legacy.lua b/tests/test-naughty-legacy.lua index 36d585e8f..6ef6542dd 100644 --- a/tests/test-naughty-legacy.lua +++ b/tests/test-naughty-legacy.lua @@ -11,6 +11,9 @@ local GLib = require("lgi" ).GLib local gpcall = require("gears.protected_call") local dwidget = require("naughty.widget._default") +-- Bypass the new rc.lua and force the legacy mode again. +naughty._reset_display_handlers() + -- This module test deprecated APIs require("gears.debug").deprecate = function() end diff --git a/tests/test-naughty-screen.lua b/tests/test-naughty-screen.lua new file mode 100644 index 000000000..eabed7a4d --- /dev/null +++ b/tests/test-naughty-screen.lua @@ -0,0 +1,158 @@ +--- This test suite focuses on the AwesomeWM v4.4+ notification API and +-- specifically how the `naughty.layout.box` popup widgets handle multi-screen +-- scenario. +local steps = {} + +local naughty = require("naughty") +local grect = require("gears.geometry").rectangle + +-- Do not use whatever `rc.lua` has. This avoids having to update the test +-- every time. + +naughty._reset_display_handlers() + +local called = 0 + +naughty.connect_signal("request::display", function(n) + called = called + 1 + n._private._box_wibox = naughty.layout.box { notification = n } +end) + +local positions = { + "top_left" , "top_middle" , "top_right" , + "bottom_left" , "bottom_middle" , "bottom_right" , +} + +local objs = {} + +local s1, s2 = mouse.screen, nil + +for _, p in ipairs(positions) do + objs[p] = setmetatable({},{ + __index=function(t,k) t[k]={};return t[k] end, + __mode = "k" + }) +end + +local function add_many(s) + for _, pos in ipairs(positions) do + for i=1, 5 do + table.insert(objs[pos][s], naughty.notification { + message = pos..i, + position = pos, + screen = s, + }) + end + end +end + +local function remove_at(s, idx) + -- This will be validated with many asserts in the code. + for _, pos in ipairs(positions) do + local n = table.remove(objs[pos][s], idx) + assert(n) + + n:destroy() + assert(n._private.is_destroyed) + end +end + +local function check_screen(s) + for _, pos in ipairs(positions) do + for _, n in pairs(objs[pos][s]) do + assert(n.screen == s) + assert(n._private._box_wibox) + assert(grect.is_inside( + n._private._box_wibox:geometry(), + s.geometry + )) + end + end +end + +-- Create notifications in each position. +table.insert(steps, function() + add_many(s1) + + return true +end) + +-- Make sure removing notification works. +table.insert(steps, function() + + remove_at(s1, 2) + + -- Split the screen + s1:split() + + s2 = screen[2] + assert(s1 ~= s2) + + return true +end) + +-- Make sure the notification moved as the screen shrunk. +table.insert(steps, function() + check_screen(s1) + + -- Make sure we can still remove them without errors. + remove_at(s1, 2) + + -- Add more! + add_many(s2) + + -- Make sure none got moved to the wrong position due to a fallback code + -- path triggered by accident. The first few iteration were prone to this. + check_screen(s1) + check_screen(s2) + + return true +end) + +-- Remove everything and see what happens. +table.insert(steps, function() + + for _=1, 3 do + for _, s in ipairs {s1,s2} do + remove_at(s, 1) + end + end + + for _=1, 2 do + remove_at(s2, 1) + end + + -- And add them again. + add_many(s1) + add_many(s2) + + return true +end) + +--local weak = nil --FIXME + +-- Delete a screen and make sure it gets GCed. +table.insert(steps, function() + s2:fake_remove() + + -- Help the weak tables a little. + for _, pos in ipairs(positions) do + objs[pos][s1] = nil + end + + -- Drop our string reference to s2. + --weak, s2 = setmetatable({s2}, {__mode="v"}), nil --FIXME + + return true +end) + +--FIXME +--table.insert(steps, function() +-- if weak[1] == nil then return true end +-- +-- for _=1, 10 do +-- collectgarbage("collect") +-- end +--end) + +require("_runner").run_steps(steps)