Unverified Commit ba4a2665 authored by Erik Reider's avatar Erik Reider Committed by GitHub

Add inline replies to notifications (#221)

parent 4aefd3aa
...@@ -56,6 +56,7 @@ Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotif ...@@ -56,6 +56,7 @@ Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotif
- Keyboard shortcuts - Keyboard shortcuts
- Notification body markup with image support - Notification body markup with image support
- Inline replies
- A panel to view previous notifications - A panel to view previous notifications
- Show album art for notifications like Spotify - Show album art for notifications like Spotify
- Do not disturb - Do not disturb
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"control-center-margin-right": 0, "control-center-margin-right": 0,
"control-center-margin-left": 0, "control-center-margin-left": 0,
"notification-2fa-action": true, "notification-2fa-action": true,
"notification-inline-replies": false,
"notification-icon-size": 64, "notification-icon-size": 64,
"notification-body-image-height": 100, "notification-body-image-height": 100,
"notification-body-image-width": 200, "notification-body-image-width": 200,
......
...@@ -556,6 +556,12 @@ namespace SwayNotificationCenter { ...@@ -556,6 +556,12 @@ namespace SwayNotificationCenter {
public bool notification_2fa_action { get; set; default = true; } public bool notification_2fa_action { get; set; default = true; }
/** /**
* If notifications should display a text field to reply if the
* sender requests it.
*/
public bool notification_inline_replies { get; set; default = false; }
/**
* Notification icon size, in pixels. * Notification icon size, in pixels.
*/ */
private const int NOTIFICATION_ICON_MINIMUM_SIZE = 16; private const int NOTIFICATION_ICON_MINIMUM_SIZE = 16;
......
...@@ -80,6 +80,11 @@ ...@@ -80,6 +80,11 @@
"description": "If each notification should display a 'COPY \"1234\"' action", "description": "If each notification should display a 'COPY \"1234\"' action",
"default": true "default": true
}, },
"notification-inline-replies": {
"type": "boolean",
"description": "If notifications should display a text field to reply if the sender requests it. NOTE: Replying in popup notifications is only available if the compositor supports GTK Layer-Shell ON_DEMAND keyboard interactivity.",
"default": false
},
"notification-icon-size": { "notification-icon-size": {
"type": "integer", "type": "integer",
"description": "The notification icon size (in pixels)", "description": "The notification icon size (in pixels)",
......
...@@ -133,6 +133,14 @@ namespace SwayNotificationCenter { ...@@ -133,6 +133,14 @@ namespace SwayNotificationCenter {
// sometimes being passed through to unfucused application // sometimes being passed through to unfucused application
// Ex: Firefox in a fullscreen YouTube video // Ex: Firefox in a fullscreen YouTube video
this.key_release_event.connect ((w, event_key) => { this.key_release_event.connect ((w, event_key) => {
if (this.get_focus () is Gtk.Entry) {
switch (Gdk.keyval_name (event_key.keyval)) {
case "Escape":
this.set_focus (null);
return true;
}
return false;
}
if (event_key.type == Gdk.EventType.KEY_RELEASE) { if (event_key.type == Gdk.EventType.KEY_RELEASE) {
switch (Gdk.keyval_name (event_key.keyval)) { switch (Gdk.keyval_name (event_key.keyval)) {
case "Escape": case "Escape":
...@@ -145,6 +153,7 @@ namespace SwayNotificationCenter { ...@@ -145,6 +153,7 @@ namespace SwayNotificationCenter {
}); });
this.key_press_event.connect ((w, event_key) => { this.key_press_event.connect ((w, event_key) => {
if (this.get_focus () is Gtk.Entry) return false;
if (event_key.type == Gdk.EventType.KEY_PRESS) { if (event_key.type == Gdk.EventType.KEY_PRESS) {
var children = list_box.get_children (); var children = list_box.get_children ();
Notification noti = (Notification) Notification noti = (Notification)
...@@ -205,7 +214,7 @@ namespace SwayNotificationCenter { ...@@ -205,7 +214,7 @@ namespace SwayNotificationCenter {
} }
navigate_list (list_position); navigate_list (list_position);
} }
return true; return false;
}); });
// Switches the stack page depending on the // Switches the stack page depending on the
...@@ -464,7 +473,9 @@ namespace SwayNotificationCenter { ...@@ -464,7 +473,9 @@ namespace SwayNotificationCenter {
public void add_notification (NotifyParams param, public void add_notification (NotifyParams param,
NotiDaemon noti_daemon) { NotiDaemon noti_daemon) {
var noti = new Notification.regular (param, noti_daemon); var noti = new Notification.regular (param,
noti_daemon,
NotificationType.CONTROL_CENTER);
noti.grab_focus.connect ((w) => { noti.grab_focus.connect ((w) => {
uint i = list_box.get_children ().index (w); uint i = list_box.get_children ().index (w);
if (list_position != uint.MAX && list_position != i) { if (list_position != uint.MAX && list_position != i) {
......
...@@ -3,6 +3,8 @@ namespace SwayNotificationCenter { ...@@ -3,6 +3,8 @@ namespace SwayNotificationCenter {
static string ? style_path; static string ? style_path;
static string ? config_path; static string ? config_path;
static uint layer_shell_protocol_version = 3;
static Settings self_settings; static Settings self_settings;
public void main (string[] args) { public void main (string[] args) {
...@@ -40,6 +42,10 @@ namespace SwayNotificationCenter { ...@@ -40,6 +42,10 @@ namespace SwayNotificationCenter {
ConfigModel.init (config_path); ConfigModel.init (config_path);
Functions.load_css (style_path); Functions.load_css (style_path);
if (ConfigModel.instance.layer_shell) {
layer_shell_protocol_version = GtkLayerShell.get_protocol_version ();
}
swaync_daemon = new SwayncDaemon (); swaync_daemon = new SwayncDaemon ();
Bus.own_name (BusType.SESSION, "org.erikreider.swaync.cc", Bus.own_name (BusType.SESSION, "org.erikreider.swaync.cc",
BusNameOwnerFlags.NONE, BusNameOwnerFlags.NONE,
......
...@@ -113,6 +113,7 @@ namespace SwayNotificationCenter { ...@@ -113,6 +113,7 @@ namespace SwayNotificationCenter {
"synchronous", "synchronous",
"private-synchronous", "private-synchronous",
"x-canonical-private-synchronous", "x-canonical-private-synchronous",
"inline-reply",
}; };
} }
...@@ -339,5 +340,8 @@ namespace SwayNotificationCenter { ...@@ -339,5 +340,8 @@ namespace SwayNotificationCenter {
* support the concept of being able to "invoke" a notification. * support the concept of being able to "invoke" a notification.
*/ */
public signal void ActionInvoked (uint32 id, string action_key); public signal void ActionInvoked (uint32 id, string action_key);
/** To be used by the non-standard "inline-reply" capability */
public signal void NotificationReplied (uint32 id, string text);
} }
} }
...@@ -102,6 +102,10 @@ namespace SwayNotificationCenter { ...@@ -102,6 +102,10 @@ namespace SwayNotificationCenter {
private int priv_value { private get; private set; } private int priv_value { private get; private set; }
public bool has_synch { public get; private set; } public bool has_synch { public get; private set; }
/** Inline-replies */
public Action ? inline_reply { get; set; }
public string ? inline_reply_placeholder { get; set; }
// Custom hints // Custom hints
/** Disables scripting for notification */ /** Disables scripting for notification */
public bool swaync_no_script { get; set; } public bool swaync_no_script { get; set; }
...@@ -133,30 +137,12 @@ namespace SwayNotificationCenter { ...@@ -133,30 +137,12 @@ namespace SwayNotificationCenter {
this.replaces = false; this.replaces = false;
this.has_synch = false; this.has_synch = false;
s_hints (); parse_hints ();
Array<Action> ac_array = new Array<Action> ();
if (actions.length > 1 && actions.length % 2 == 0) {
for (int i = 0; i < actions.length; i++) {
var action = new Action ();
action.identifier = actions[i];
action.name = actions[i + 1];
if (action.name != null && action.identifier != null
&& action.name != "" && action.identifier != "") {
if (action.identifier.down () == "default") { parse_actions (actions);
default_action = action;
} else {
ac_array.append_val (action);
}
}
i++;
}
}
this.actions = ac_array;
} }
private void s_hints () { private void parse_hints () {
foreach (var hint in hints.get_keys ()) { foreach (var hint in hints.get_keys ()) {
Variant hint_value = hints[hint]; Variant hint_value = hints[hint];
switch (hint) { switch (hint) {
...@@ -239,12 +225,46 @@ namespace SwayNotificationCenter { ...@@ -239,12 +225,46 @@ namespace SwayNotificationCenter {
urgency = UrgencyLevels.from_value (hint_value.get_byte ()); urgency = UrgencyLevels.from_value (hint_value.get_byte ());
} }
break; break;
case "x-kde-reply-placeholder-text":
if (hint_value.is_of_type (VariantType.STRING)) {
inline_reply_placeholder = hint_value.get_string ();
}
break;
}
}
}
private void parse_actions (string[] actions) {
Array<Action> parsed_actions = new Array<Action> ();
if (actions.length > 1 && actions.length % 2 == 0) {
for (int i = 0; i < actions.length; i++) {
var action = new Action ();
action.identifier = actions[i];
action.name = actions[i + 1];
if (action.name != null && action.identifier != null
&& action.name != "" && action.identifier != "") {
string id = action.identifier.down ();
switch (id) {
case "default":
default_action = action;
break;
case "inline-reply":
inline_reply = action;
break;
default:
parsed_actions.append_val (action);
break;
}
}
i++;
} }
} }
this.actions = parsed_actions;
} }
public string to_string () { public string to_string () {
var params = new HashTable<string, string ? > (str_hash, str_equal); var params = new HashTable<string, string ?> (str_hash, str_equal);
params.set ("applied_id", applied_id.to_string ()); params.set ("applied_id", applied_id.to_string ());
params.set ("app_name", app_name); params.set ("app_name", app_name);
...@@ -280,6 +300,8 @@ namespace SwayNotificationCenter { ...@@ -280,6 +300,8 @@ namespace SwayNotificationCenter {
_actions += "\n\t" + _action.to_string (); _actions += "\n\t" + _action.to_string ();
} }
params.set ("actions", string.joinv ("", _actions)); params.set ("actions", string.joinv ("", _actions));
params.set ("inline-reply", inline_reply == null
? null : inline_reply.to_string ());
string[] result = {}; string[] result = {};
foreach (var k in params.get_keys ()) { foreach (var k in params.get_keys ()) {
......
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 --> <!-- Generated with glade 3.40.0 -->
<interface> <interface>
<requires lib="gtk+" version="3.24"/> <requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.2"/> <requires lib="libhandy" version="1.2"/>
...@@ -45,10 +45,10 @@ ...@@ -45,10 +45,10 @@
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkButton" id="default_button"> <object class="GtkEventBox" id="default_action">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="receives-default">False</property> <property name="events">GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_STRUCTURE_MASK</property>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="visible">True</property> <property name="visible">True</property>
...@@ -192,6 +192,49 @@ ...@@ -192,6 +192,49 @@
<property name="position">2</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkBox" id="inline_reply_box">
<property name="can-focus">False</property>
<child>
<object class="GtkEntry" id="inline_reply_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="input-hints">GTK_INPUT_HINT_SPELLCHECK | GTK_INPUT_HINT_EMOJI | GTK_INPUT_HINT_NONE</property>
<style>
<class name="inline-reply-entry"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="inline_reply_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<style>
<class name="inline-reply-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="inline-reply"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<style> <style>
<class name="notification-content"/> <class name="notification-content"/>
</style> </style>
......
...@@ -40,6 +40,8 @@ namespace SwayNotificationCenter { ...@@ -40,6 +40,8 @@ namespace SwayNotificationCenter {
private double last_upper = 0; private double last_upper = 0;
Gee.HashSet<uint32> inline_reply_notifications = new Gee.HashSet<uint32> ();
private const int MAX_HEIGHT = 600; private const int MAX_HEIGHT = 600;
private NotificationWindow () { private NotificationWindow () {
...@@ -158,6 +160,7 @@ namespace SwayNotificationCenter { ...@@ -158,6 +160,7 @@ namespace SwayNotificationCenter {
public delegate bool remove_iter_func (Notification notification); public delegate bool remove_iter_func (Notification notification);
public void close_all_notifications (remove_iter_func ? func = null) { public void close_all_notifications (remove_iter_func ? func = null) {
inline_reply_notifications.clear ();
if (!this.get_realized ()) return; if (!this.get_realized ()) return;
foreach (var w in box.get_children ()) { foreach (var w in box.get_children ()) {
Notification notification = (Notification) w; Notification notification = (Notification) w;
...@@ -170,6 +173,17 @@ namespace SwayNotificationCenter { ...@@ -170,6 +173,17 @@ namespace SwayNotificationCenter {
private void remove_notification (Notification ? noti, bool replaces) { private void remove_notification (Notification ? noti, bool replaces) {
// Remove notification and its destruction timeout // Remove notification and its destruction timeout
if (noti != null) { if (noti != null) {
#if HAVE_LATEST_GTK_LAYER_SHELL
if (noti.has_inline_reply) {
inline_reply_notifications.remove (noti.param.applied_id);
if (inline_reply_notifications.size == 0
&& GtkLayerShell.get_keyboard_mode (this)
!= GtkLayerShell.KeyboardMode.NONE) {
GtkLayerShell.set_keyboard_mode (
this, GtkLayerShell.KeyboardMode.NONE);
}
}
#endif
noti.remove_noti_timeout (); noti.remove_noti_timeout ();
noti.destroy (); noti.destroy ();
} }
...@@ -188,9 +202,21 @@ namespace SwayNotificationCenter { ...@@ -188,9 +202,21 @@ namespace SwayNotificationCenter {
NotiDaemon noti_daemon) { NotiDaemon noti_daemon) {
var noti = new Notification.timed (param, var noti = new Notification.timed (param,
noti_daemon, noti_daemon,
NotificationType.POPUP,
ConfigModel.instance.timeout, ConfigModel.instance.timeout,
ConfigModel.instance.timeout_low, ConfigModel.instance.timeout_low,
ConfigModel.instance.timeout_critical); ConfigModel.instance.timeout_critical);
#if HAVE_LATEST_GTK_LAYER_SHELL
if (noti.has_inline_reply) {
inline_reply_notifications.add (param.applied_id);
if (GtkLayerShell.get_keyboard_mode (this)
!= GtkLayerShell.KeyboardMode.ON_DEMAND) {
GtkLayerShell.set_keyboard_mode (
this, GtkLayerShell.KeyboardMode.ON_DEMAND);
}
}
#endif
if (list_reverse) { if (list_reverse) {
box.pack_start (noti); box.pack_start (noti);
......
...@@ -6,11 +6,15 @@ ...@@ -6,11 +6,15 @@
@define-color noti-border-color rgba(255, 255, 255, 0.15); @define-color noti-border-color rgba(255, 255, 255, 0.15);
@define-color noti-bg rgb(48, 48, 48); @define-color noti-bg rgb(48, 48, 48);
@define-color noti-bg-darker rgb(38, 38, 38);
@define-color noti-bg-hover rgb(56, 56, 56); @define-color noti-bg-hover rgb(56, 56, 56);
@define-color noti-bg-focus rgba(68, 68, 68, 0.6); @define-color noti-bg-focus rgba(68, 68, 68, 0.6);
@define-color noti-close-bg rgba(255, 255, 255, 0.1); @define-color noti-close-bg rgba(255, 255, 255, 0.1);
@define-color noti-close-bg-hover rgba(255, 255, 255, 0.15); @define-color noti-close-bg-hover rgba(255, 255, 255, 0.15);
@define-color text-color rgb(255, 255, 255);
@define-color text-color-disabled rgb(150, 150, 150);
@define-color bg-selected rgb(0, 128, 255); @define-color bg-selected rgb(0, 128, 255);
.notification-row { .notification-row {
...@@ -58,7 +62,7 @@ ...@@ -58,7 +62,7 @@
.close-button { .close-button {
background: @noti-close-bg; background: @noti-close-bg;
color: white; color: @text-color;
text-shadow: none; text-shadow: none;
padding: 0; padding: 0;
border-radius: 100%; border-radius: 100%;
...@@ -84,7 +88,8 @@ ...@@ -84,7 +88,8 @@
box-shadow: none; box-shadow: none;
background: @noti-bg; background: @noti-bg;
border: 1px solid @noti-border-color; border: 1px solid @noti-border-color;
color: white; color: @text-color;
transition: all 0.15s ease-in-out;
} }
.notification-default-action:hover, .notification-default-action:hover,
...@@ -119,6 +124,32 @@ ...@@ -119,6 +124,32 @@
border-right: 1px solid @noti-border-color; border-right: 1px solid @noti-border-color;
} }
.inline-reply {
margin-top: 8px;
}
.inline-reply-entry {
background: @noti-bg-darker;
color: @text-color;
caret-color: @text-color;
border: 1px solid @noti-border-color;
border-radius: 12px;
}
.inline-reply-button {
margin-left: 4px;
background: @noti-bg;
border: 1px solid @noti-border-color;
border-radius: 12px;
color: @text-color;
}
.inline-reply-button:disabled {
background: initial;
color: @text-color-disabled;
border: 1px solid transparent;
}
.inline-reply-button:hover {
background: @noti-bg-hover;
}
.image { .image {
} }
...@@ -132,7 +163,7 @@ ...@@ -132,7 +163,7 @@
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
background: transparent; background: transparent;
color: white; color: @text-color;
text-shadow: none; text-shadow: none;
} }
...@@ -140,7 +171,7 @@ ...@@ -140,7 +171,7 @@
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
background: transparent; background: transparent;
color: white; color: @text-color;
text-shadow: none; text-shadow: none;
margin-right: 18px; margin-right: 18px;
} }
...@@ -149,7 +180,7 @@ ...@@ -149,7 +180,7 @@
font-size: 15px; font-size: 15px;
font-weight: normal; font-weight: normal;
background: transparent; background: transparent;
color: white; color: @text-color;
text-shadow: none; text-shadow: none;
} }
...@@ -183,7 +214,7 @@ ...@@ -183,7 +214,7 @@
} }
.widget-title > button { .widget-title > button {
font-size: initial; font-size: initial;
color: white; color: @text-color;
text-shadow: none; text-shadow: none;
background: @noti-bg; background: @noti-bg;
border: 1px solid @noti-border-color; border: 1px solid @noti-border-color;
...@@ -318,7 +349,7 @@ ...@@ -318,7 +349,7 @@
} }
.widget-inhibitors > button { .widget-inhibitors > button {
font-size: initial; font-size: initial;
color: white; color: @text-color;
text-shadow: none; text-shadow: none;
background: @noti-bg; background: @noti-bg;
border: 1px solid @noti-border-color; border: 1px solid @noti-border-color;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment