Add support for notification actions

The desktop notification specification says that a notification can have
different actions. These actions allow the user to interact with the client
application and should be displayed by the notification server.
* Add function to emit a DBus signal
* Notifications : emit NotificationClosed signal when closing notification
* Notifications: use constant for notification closed reasons
* notifications: Implement notifications actions

This is just a basic implementation to display the actions send with the
notifications. The actions should be displayed differently
* Notifications: add support for default action
This commit is contained in:
Da Risk 2014-12-31 14:28:46 +01:00
parent 30b313f77a
commit e664a74710
2 changed files with 166 additions and 8 deletions

55
dbus.c
View File

@ -791,6 +791,60 @@ luaA_dbus_disconnect_signal(lua_State *L)
return 0;
}
/** Emit a signal on the D-Bus.
* \param L The Lua VM state.
* \return The number of elements pushed on stack.
* \luastack
* \lparam A string indicating if we are using system or session bus.
* \lparam A string with the dbus path.
* \lparam A string with the dbus interface.
* \lparam A string with the dbus method name.
* \lparam type of 1st arg
* \lparam 1st arg value
* \lparam type of 2nd arg
* \lparam 2nd arg value
* ... etc
*/
static int
luaA_dbus_emit_signal(lua_State *L)
{
const char *bus_name = luaL_checkstring(L, 1);
const char *path = luaL_checkstring(L, 2);
const char *itface = luaL_checkstring(L, 3);
const char *name = luaL_checkstring(L, 4);
lua_remove(L, 1);
lua_remove(L, 1);
lua_remove(L, 1);
lua_remove(L, 1);
DBusConnection *dbus_connection = a_dbus_bus_getbyname(bus_name);
DBusMessage* msg = dbus_message_new_signal(path, itface, name);
DBusMessageIter iter;
dbus_message_iter_init_append(msg, &iter);
int nargs = lua_gettop(L);
if(nargs % 2 != 0)
{
luaA_warn(L, "your D-Bus signal emiting method has wrong number of arguments");
dbus_message_unref(msg);
lua_pushboolean(L, 0);
return 1;
}
for(int i = 1; i < nargs; i += 2) {
if(!a_dbus_convert_value(L, i, &iter))
{
luaA_warn(L, "your D-Bus signal emitting method has bad argument type");
dbus_message_unref(msg);
lua_pushboolean(L, 0);
return 1;
}
}
dbus_connection_send(dbus_connection, msg, NULL);
dbus_message_unref(msg);
lua_pushboolean(L, 1);
return 1;
}
const struct luaL_Reg awesome_dbus_lib[] =
{
{ "request_name", luaA_dbus_request_name },
@ -799,6 +853,7 @@ const struct luaL_Reg awesome_dbus_lib[] =
{ "remove_match", luaA_dbus_remove_match },
{ "connect_signal", luaA_dbus_connect_signal },
{ "disconnect_signal", luaA_dbus_disconnect_signal },
{ "emit_signal", luaA_dbus_emit_signal },
{ "__index", luaA_default_index },
{ "__newindex", luaA_default_newindex },
{ NULL, NULL }

View File

@ -95,6 +95,13 @@ local urgency = {
critical = "\2"
}
local notificationClosedReason = {
expired = 1,
dismissedByUser = 2,
dismissedByCommand = 3,
undefined = 4
}
--- DBUS notification to preset mapping
-- The first element is an object containing the filter
-- If the rules in the filter matches the associated preset will be applied
@ -279,6 +286,7 @@ end
-- @param 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
-- @param actions array of pair of String:String for each action (id:localized text)
-- @usage naughty.notify({ title = "Achtung!", text = "You're idling", timeout = 0 })
-- @return The notification object
function naughty.notify(args)
@ -304,6 +312,7 @@ function naughty.notify(args)
local margin = args.margin or preset.margin
local border_width = args.border_width or preset.border_width
local position = args.position or preset.position
local actions = args.actions
local escape_pattern = "[<>&]"
local escape_subs = { ['<'] = "&lt;", ['>'] = "&gt;", ['&'] = "&amp;" }
@ -340,10 +349,13 @@ function naughty.notify(args)
if title then title = title .. "\n" else title = "" end
-- hook destroy
local die = function () naughty.destroy(notification) end
local die = function (reason)
naughty.destroy(notification)
sendNotificationClosed(notification.id, reason)
end
if timeout > 0 then
local timer_die = timer { timeout = timeout }
timer_die:connect_signal("timeout", die)
timer_die:connect_signal("timeout", function() die(notificationClosedReason.expired) end)
if not suspended then
timer_die:start()
end
@ -351,21 +363,25 @@ function naughty.notify(args)
end
notification.die = die
local has_default_action = false;
local run = function ()
if has_default_action then
sendActionInvoked(notification.id, "default")
end
if args.run then
args.run(notification)
else
die()
die(notificationClosedReason.dismissedByUser)
end
end
local hover_destroy = function ()
if hover_timeout == 0 then
die()
die(notificationClosedReason.expired)
else
if notification.timer then notification.timer:stop() end
notification.timer = timer { timeout = hover_timeout }
notification.timer:connect_signal("timeout", die)
notification.timer:connect_signal("timeout", function() die(notificationClosedReason.expired) end)
notification.timer:start()
end
end
@ -400,6 +416,56 @@ function naughty.notify(args)
end
end
local actionslayout = wibox.layout.fixed.vertical()
local actions_max_width = 0
local actions_total_height = 0
if actions then
local action
local actionid
-- create actions box
for i , v in ipairs(actions) do
if i % 2 == 1 then
actionid = v
if actionid == "default" then
has_default_action = true;
end
elseif actionid ~= nil and actionid ~= "default" then
action = v
local actiontextbox = wibox.widget.textbox()
local actionmarginbox = wibox.layout.margin()
actionmarginbox:set_margins(margin)
actionmarginbox:set_widget(actiontextbox)
actiontextbox:set_valign("middle")
actiontextbox:set_font(font)
actiontextbox:set_markup(string.format('<b>%s</b>', action))
-- calculate the height
local w, h = actiontextbox:fit(-1, -1)
local height = h + 2 * margin
-- calculate the width
w, h = actiontextbox:fit(-1, -1)
local width = w + 2 * margin
actionmarginbox:buttons(util.table.join(
button({ }, 1, function()
sendActionInvoked(notification.id, actionid)
die(2)
end),
button({ }, 3, function()
sendActionInvoked(notification.id, actionid)
die(2)
end))
)
actionslayout:add(actionmarginbox)
actions_total_height = actions_total_height + height
if actions_max_width < width then
actions_max_width = width
end
end
end
end
-- create iconbox
local iconbox = nil
local iconmargin = nil
@ -461,12 +527,18 @@ function naughty.notify(args)
end
end
height = height + actions_total_height
-- calculate the width
if not width then
local w, h = textbox:fit(-1, -1)
width = w + (iconbox and icon_w + 2 * margin or 0) + 2 * margin
end
if width < actions_max_width then
width = actions_max_width
end
-- crop to workarea size if too big
local workarea = capi.screen[screen].workarea
if width > workarea.width - 2 * (border_width or 0) - 2 * (naughty.config.padding or 0) then
@ -497,10 +569,17 @@ function naughty.notify(args)
layout:add(iconmargin)
end
layout:add(marginbox)
notification.box:set_widget(layout)
local completelayout = wibox.layout.fixed.vertical()
completelayout:add(layout)
completelayout:add(actionslayout)
notification.box:set_widget(completelayout)
-- Setup the mouse events
layout:buttons(util.table.join(button({ }, 1, run), button({ }, 3, die)))
layout:buttons(util.table.join(button({ }, 1, run),
button({ }, 3, function()
die(notificationClosedReason.dismissedByUser)
end)))
-- insert the notification to the table
table.insert(naughty.notifications[screen][notification.position], notification)
@ -544,6 +623,7 @@ if capi.dbus then
end
end
local preset = args.preset or naughty.config.defaults
args.actions = actions
if not preset.callback or (type(preset.callback) == "function" and
preset.callback(data, appname, replaces_id, icon, title, text, actions, hints, expire)) then
if icon ~= "" then
@ -610,6 +690,7 @@ if capi.dbus then
elseif data.member == "CloseNotification" then
local obj = getById(appname)
if obj then
sendNotificationClosed(obj.id, 3)
naughty.destroy(obj)
end
elseif data.member == "GetServerInfo" or data.member == "GetServerInformation" then
@ -618,7 +699,7 @@ if capi.dbus then
elseif data.member == "GetCapabilities" then
-- We actually do display the body of the message, we support <b>, <i>
-- and <u> in the body and we handle static (non-animated) icons.
return "as", { "s", "body", "s", "body-markup", "s", "icon-static" }
return "as", { "s", "body", "s", "body-markup", "s", "icon-static", "s", "actions" }
end
end)
@ -663,6 +744,14 @@ if capi.dbus then
<arg name="return_vendor" type="s" direction="out"/>
<arg name="return_version" type="s" direction="out"/>
</method>
<signal name="NotificationClosed">
<arg name="id" type="u" direction="out"/>
<arg name="reason" type="u" direction="out"/>
</signal>
<signal name="ActionInvoked">
<arg name="id" type="u" direction="out"/>
<arg name="action_key" type="s" direction="out"/>
</signal>
</interface>
</node>]=]
return "s", xml
@ -673,6 +762,20 @@ if capi.dbus then
capi.dbus.request_name("session", "org.freedesktop.Notifications")
end
function sendActionInvoked(notificationId, action)
res = capi.dbus.emit_signal("session", "/org/freedesktop/Notifications",
"org.freedesktop.Notifications", "ActionInvoked",
"i", notificationId,
"s", action)
end
function sendNotificationClosed(notificationId, reason)
res = capi.dbus.emit_signal("session", "/org/freedesktop/Notifications",
"org.freedesktop.Notifications", "NotificationClosed",
"i", notificationId,
"i", reason)
end
return naughty
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80