mirror of
https://github.com/elementary/gala.git
synced 2024-11-27 15:45:31 +03:00
Implement notifications plugin
This commit is contained in:
commit
642e840a15
15
configure.ac
15
configure.ac
@ -164,6 +164,20 @@ if test "x$have_mutter314" = "xyes" ; then
|
||||
MUTTER_API="3.14"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# Dependencies for Notifications plugin
|
||||
# -----------------------------------------------------------
|
||||
|
||||
NOTIFICATION_PLUGIN_PKGS="libcanberra \
|
||||
libcanberra-gtk"
|
||||
|
||||
NOTIFICATION_PLUGIN_VALA_PKGS="--pkg libcanberra \
|
||||
--pkg libcanberra-gtk"
|
||||
|
||||
PKG_CHECK_MODULES(NOTIFICAION_PLUGIN, $NOTIFICATION_PLUGIN_PKGS)
|
||||
|
||||
AC_SUBST([NOTIFICATION_PLUGIN_VALA_PKGS])
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# Additional configure flags
|
||||
# -----------------------------------------------------------
|
||||
@ -232,6 +246,7 @@ docs/Makefile
|
||||
data/Makefile
|
||||
vapi/Makefile
|
||||
plugins/Makefile
|
||||
plugins/notify/Makefile
|
||||
plugins/zoom/Makefile
|
||||
po/Makefile.in
|
||||
])
|
||||
|
@ -1,5 +1,5 @@
|
||||
stylesdir = $(pkgdatadir)
|
||||
styles_DATA = texture.png close.png
|
||||
styles_DATA = gala.css texture.png close.png
|
||||
|
||||
applicationsdir = $(datadir)/applications
|
||||
applications_DATA = gala.desktop
|
||||
@ -19,6 +19,7 @@ gschemas.compiled: Makefile $(gsettings_SCHEMAS:.xml=.valid)
|
||||
all-local: gschemas.compiled
|
||||
|
||||
EXTRA_DIST = \
|
||||
gala.css \
|
||||
gala.desktop \
|
||||
texture.png \
|
||||
close.png \
|
||||
|
30
data/gala.css
Normal file
30
data/gala.css
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Gala Developers
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http: *www.gnu.org/licenses/>.
|
||||
*
|
||||
* Authored by: Tom Beckmann
|
||||
*/
|
||||
|
||||
.gala-notification {
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
|
||||
background-color: rgb(2434, 2434, 2434);
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.gala-notification .title, .gala-notification .label {
|
||||
color: #333;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
SUBDIRS = \
|
||||
notify \
|
||||
zoom \
|
||||
$(NULL)
|
||||
|
||||
|
128
plugins/notify/ConfirmationNotification.vala
Normal file
128
plugins/notify/ConfirmationNotification.vala
Normal file
@ -0,0 +1,128 @@
|
||||
//
|
||||
// Copyright (C) 2014 Tom Beckmann
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
using Clutter;
|
||||
using Meta;
|
||||
|
||||
namespace Gala.Plugins.Notify
|
||||
{
|
||||
public class ConfirmationNotification : Notification
|
||||
{
|
||||
const int DURATION = 2000;
|
||||
const int PROGRESS_HEIGHT = 6;
|
||||
|
||||
public bool has_progress { get; private set; }
|
||||
|
||||
int _progress;
|
||||
public int progress {
|
||||
get {
|
||||
return _progress;
|
||||
}
|
||||
private set {
|
||||
_progress = value;
|
||||
content.invalidate ();
|
||||
}
|
||||
}
|
||||
|
||||
public string confirmation_type { get; private set; }
|
||||
|
||||
int old_progress;
|
||||
|
||||
public ConfirmationNotification (uint32 id, Gdk.Pixbuf? icon, bool icon_only,
|
||||
int progress, string confirmation_type)
|
||||
{
|
||||
Object (id: id, icon: icon, urgency: NotificationUrgency.LOW, expire_timeout: DURATION);
|
||||
|
||||
this.icon_only = icon_only;
|
||||
this.has_progress = progress > -1;
|
||||
this.progress = progress;
|
||||
this.confirmation_type = confirmation_type;
|
||||
}
|
||||
|
||||
public override void update_allocation (out float content_height, AllocationFlags flags)
|
||||
{
|
||||
content_height = ICON_SIZE;
|
||||
}
|
||||
|
||||
public override void draw_content (Cairo.Context cr)
|
||||
{
|
||||
if (!has_progress)
|
||||
return;
|
||||
|
||||
var x = MARGIN + PADDING + ICON_SIZE + SPACING;
|
||||
var y = MARGIN + PADDING + (ICON_SIZE - PROGRESS_HEIGHT) / 2;
|
||||
var width = WIDTH - x - MARGIN;
|
||||
|
||||
if (!transitioning)
|
||||
draw_progress_bar (cr, x, y, width, progress);
|
||||
else {
|
||||
Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, MARGIN, MARGIN, WIDTH - MARGIN * 2, ICON_SIZE + PADDING * 2, 4);
|
||||
cr.clip ();
|
||||
|
||||
draw_progress_bar (cr, x, y + animation_slide_y_offset, width, old_progress);
|
||||
draw_progress_bar (cr, x, y + animation_slide_y_offset - animation_slide_height, width, progress);
|
||||
|
||||
cr.reset_clip ();
|
||||
}
|
||||
}
|
||||
|
||||
void draw_progress_bar (Cairo.Context cr, int x, float y, int width, int progress)
|
||||
{
|
||||
var fraction = (int) Math.floor (progress.clamp (0, 100) / 100.0 * width);
|
||||
|
||||
Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, x, y, width,
|
||||
PROGRESS_HEIGHT, PROGRESS_HEIGHT / 2);
|
||||
cr.set_source_rgb (0.8, 0.8, 0.8);
|
||||
cr.fill ();
|
||||
|
||||
if (progress > 0) {
|
||||
Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, x, y, fraction,
|
||||
PROGRESS_HEIGHT, PROGRESS_HEIGHT / 2);
|
||||
cr.set_source_rgb (0.3, 0.3, 0.3);
|
||||
cr.fill ();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void update_slide_animation ()
|
||||
{
|
||||
// just trigger the draw function, which will move our progress bar down
|
||||
content.invalidate ();
|
||||
}
|
||||
|
||||
public void update (Gdk.Pixbuf? icon, int progress, string confirmation_type,
|
||||
bool icon_only)
|
||||
{
|
||||
if (this.confirmation_type != confirmation_type) {
|
||||
this.confirmation_type = confirmation_type;
|
||||
|
||||
old_progress = this.progress;
|
||||
|
||||
play_update_transition (ICON_SIZE + PADDING * 2);
|
||||
}
|
||||
|
||||
if (this.icon_only != icon_only) {
|
||||
this.icon_only = icon_only;
|
||||
queue_relayout ();
|
||||
}
|
||||
|
||||
this.has_progress = progress > -1;
|
||||
this.progress = progress;
|
||||
|
||||
update_base (icon, DURATION);
|
||||
}
|
||||
}
|
||||
}
|
96
plugins/notify/Main.vala
Normal file
96
plugins/notify/Main.vala
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// Copyright (C) 2014 Tom Beckmann
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
using Clutter;
|
||||
using Meta;
|
||||
|
||||
namespace Gala.Plugins.Notify
|
||||
{
|
||||
public class Main : Gala.Plugin
|
||||
{
|
||||
Gala.WindowManager? wm = null;
|
||||
|
||||
NotifyServer server;
|
||||
NotificationStack stack;
|
||||
|
||||
public override void initialize (Gala.WindowManager wm)
|
||||
{
|
||||
this.wm = wm;
|
||||
var screen = wm.get_screen ();
|
||||
|
||||
stack = new NotificationStack (wm.get_screen ());
|
||||
wm.ui_group.add_child (stack);
|
||||
track_actor (stack);
|
||||
|
||||
stack.animations_changed.connect ((running) => {
|
||||
freeze_track = running;
|
||||
});
|
||||
|
||||
server = new NotifyServer (stack);
|
||||
|
||||
update_position ();
|
||||
screen.monitors_changed.connect (update_position);
|
||||
screen.workareas_changed.connect (update_position);
|
||||
|
||||
Bus.own_name (BusType.SESSION, "org.freedesktop.Notifications", BusNameOwnerFlags.NONE,
|
||||
(connection) => {
|
||||
try {
|
||||
connection.register_object ("/org/freedesktop/Notifications", server);
|
||||
} catch (Error e) {
|
||||
warning ("Registring notification server failed: %s", e.message);
|
||||
destroy ();
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
(con, name) => {
|
||||
warning ("Could not aquire bus %s", name);
|
||||
destroy ();
|
||||
});
|
||||
}
|
||||
|
||||
void update_position ()
|
||||
{
|
||||
var screen = wm.get_screen ();
|
||||
var primary = screen.get_primary_monitor ();
|
||||
var area = screen.get_active_workspace ().get_work_area_for_monitor (primary);
|
||||
|
||||
stack.x = area.x + area.width - stack.width;
|
||||
stack.y = area.y;
|
||||
}
|
||||
|
||||
public override void destroy ()
|
||||
{
|
||||
if (wm == null)
|
||||
return;
|
||||
|
||||
untrack_actor (stack);
|
||||
stack.destroy ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Gala.PluginInfo register_plugin ()
|
||||
{
|
||||
return Gala.PluginInfo () {
|
||||
name = "Notify",
|
||||
author = "Gala Developers",
|
||||
plugin_type = typeof (Gala.Plugins.Notify.Main),
|
||||
provides = Gala.PluginFunction.ADDITION,
|
||||
load_priority = Gala.LoadPriority.IMMEDIATE
|
||||
};
|
||||
}
|
||||
|
72
plugins/notify/Makefile.am
Normal file
72
plugins/notify/Makefile.am
Normal file
@ -0,0 +1,72 @@
|
||||
include $(top_srcdir)/Makefile.common
|
||||
|
||||
VAPIDIR = $(top_srcdir)/vapi
|
||||
|
||||
imagedir = $(pkgdatadir)
|
||||
image_DATA = data/image-mask.png
|
||||
|
||||
BUILT_SOURCES = libgala_notify_la_vala.stamp
|
||||
|
||||
libgala_notify_la_LTLIBRARIES = libgala-notify.la
|
||||
|
||||
libgala_notify_ladir = $(pkglibdir)/plugins
|
||||
|
||||
libgala_notify_la_LDFLAGS = \
|
||||
$(PLUGIN_LDFLAGS) \
|
||||
$(GALA_CORE_LDFLAGS) \
|
||||
$(NOTIFICATION_PLUGIN_LDFLAGS) \
|
||||
$(top_builddir)/lib/libgala.la \
|
||||
$(NULL)
|
||||
|
||||
libgala_notify_la_CFLAGS = \
|
||||
$(GALA_CORE_CFLAGS) \
|
||||
$(NOTIFICATION_PLUGIN_CFLAGS) \
|
||||
-include config.h \
|
||||
-w \
|
||||
-I$(top_builddir)/lib \
|
||||
$(NULL)
|
||||
|
||||
libgala_notify_la_VALAFLAGS = \
|
||||
$(GALA_CORE_VALAFLAGS) \
|
||||
$(NOTIFICATION_PLUGIN_VALA_PKGS) \
|
||||
$(top_builddir)/lib/gala.vapi \
|
||||
--vapidir $(VAPIDIR) \
|
||||
$(VAPIDIR)/config.vapi \
|
||||
$(NULL)
|
||||
|
||||
libgala_notify_la_LIBADD = \
|
||||
$(GALA_CORE_LIBS) \
|
||||
$(NOTIFICATION_PLUGIN_LIBS) \
|
||||
$(NULL)
|
||||
|
||||
libgala_notify_la_VALASOURCES = \
|
||||
Main.vala \
|
||||
ConfirmationNotification.vala \
|
||||
NormalNotification.vala \
|
||||
Notification.vala \
|
||||
NotificationStack.vala \
|
||||
NotifyServer.vala \
|
||||
$(NULL)
|
||||
|
||||
nodist_libgala_notify_la_SOURCES = \
|
||||
$(BUILT_SOURCES) \
|
||||
$(libgala_notify_la_VALASOURCES:.vala=.c) \
|
||||
$(NULL)
|
||||
|
||||
libgala_notify_la_vala.stamp: $(libgala_notify_la_VALASOURCES)
|
||||
$(AM_V_VALA)$(VALAC) \
|
||||
$(libgala_notify_la_VALAFLAGS) \
|
||||
-C \
|
||||
$(filter %.vala %.c,$^)
|
||||
$(AM_V_at)touch $@
|
||||
|
||||
CLEANFILES = \
|
||||
$(image_DATA) \
|
||||
$(nodist_libgala_notify_la_SOURCES) \
|
||||
$(NULL)
|
||||
|
||||
EXTRA_DIST = \
|
||||
$(image_DATA) \
|
||||
$(libgala_notify_la_VALASOURCES) \
|
||||
$(NULL)
|
||||
|
304
plugins/notify/NormalNotification.vala
Normal file
304
plugins/notify/NormalNotification.vala
Normal file
@ -0,0 +1,304 @@
|
||||
//
|
||||
// Copyright (C) 2014 Tom Beckmann
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
using Clutter;
|
||||
using Meta;
|
||||
|
||||
namespace Gala.Plugins.Notify
|
||||
{
|
||||
/**
|
||||
* Wrapper class only containing the summary and body label. Allows us to
|
||||
* instantiate the content very easily for when we need to slide the old
|
||||
* and new content down.
|
||||
*/
|
||||
class NormalNotificationContent : Actor
|
||||
{
|
||||
static Regex entity_regex;
|
||||
static Regex tag_regex;
|
||||
|
||||
static construct
|
||||
{
|
||||
try {
|
||||
entity_regex = new Regex ("&(?!amp;|quot;|apos;|lt;|gt;)");
|
||||
tag_regex = new Regex ("<(?!\\/?[biu]>)");
|
||||
} catch (Error e) {}
|
||||
}
|
||||
|
||||
const int LABEL_SPACING = 2;
|
||||
|
||||
Text summary_label;
|
||||
Text body_label;
|
||||
|
||||
construct
|
||||
{
|
||||
summary_label = new Text.with_text (null, "");
|
||||
summary_label.line_wrap = true;
|
||||
summary_label.use_markup = true;
|
||||
summary_label.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
|
||||
|
||||
body_label = new Text.with_text (null, "");
|
||||
body_label.line_wrap = true;
|
||||
body_label.use_markup = true;
|
||||
body_label.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
|
||||
|
||||
var style_path = new Gtk.WidgetPath ();
|
||||
style_path.append_type (typeof (Gtk.Window));
|
||||
style_path.append_type (typeof (Gtk.EventBox));
|
||||
style_path.iter_add_class (1, "gala-notification");
|
||||
style_path.append_type (typeof (Gtk.Label));
|
||||
|
||||
var label_style_context = new Gtk.StyleContext ();
|
||||
label_style_context.add_provider (Notification.default_css, Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK);
|
||||
label_style_context.set_path (style_path);
|
||||
|
||||
Gdk.RGBA color;
|
||||
|
||||
label_style_context.save ();
|
||||
label_style_context.add_class ("title");
|
||||
color = label_style_context.get_color (Gtk.StateFlags.NORMAL);
|
||||
summary_label.color = {
|
||||
(uint8) (color.red * 255),
|
||||
(uint8) (color.green * 255),
|
||||
(uint8) (color.blue * 255),
|
||||
(uint8) (color.alpha * 255)
|
||||
};
|
||||
label_style_context.restore ();
|
||||
|
||||
label_style_context.save ();
|
||||
label_style_context.add_class ("label");
|
||||
color = label_style_context.get_color (Gtk.StateFlags.NORMAL);
|
||||
body_label.color = {
|
||||
(uint8) (color.red * 255),
|
||||
(uint8) (color.green * 255),
|
||||
(uint8) (color.blue * 255),
|
||||
(uint8) (color.alpha * 255)
|
||||
};
|
||||
label_style_context.restore ();
|
||||
|
||||
add_child (summary_label);
|
||||
add_child (body_label);
|
||||
}
|
||||
|
||||
public void set_values (string summary, string body)
|
||||
{
|
||||
summary_label.set_markup ("<b>%s</b>".printf (fix_markup (summary)));
|
||||
body_label.set_markup (fix_markup (body));
|
||||
}
|
||||
|
||||
public override void get_preferred_height (float for_width, out float min_height, out float nat_height)
|
||||
{
|
||||
float label_height;
|
||||
get_allocation_values (null, null, null, null, out label_height, null);
|
||||
|
||||
min_height = nat_height = label_height;
|
||||
}
|
||||
|
||||
public override void allocate (ActorBox box, AllocationFlags flags)
|
||||
{
|
||||
float label_x, label_width, summary_height, body_height, label_height, label_y;
|
||||
get_allocation_values (out label_x, out label_width, out summary_height,
|
||||
out body_height, out label_height, out label_y);
|
||||
|
||||
var summary_alloc = ActorBox ();
|
||||
summary_alloc.set_origin (label_x, label_y);
|
||||
summary_alloc.set_size (label_width, summary_height);
|
||||
summary_label.allocate (summary_alloc, flags);
|
||||
|
||||
var body_alloc = ActorBox ();
|
||||
body_alloc.set_origin (label_x, label_y + summary_height + LABEL_SPACING);
|
||||
body_alloc.set_size (label_width, body_height);
|
||||
body_label.allocate (body_alloc, flags);
|
||||
|
||||
base.allocate (box, flags);
|
||||
}
|
||||
|
||||
void get_allocation_values (out float label_x, out float label_width, out float summary_height,
|
||||
out float body_height, out float label_height, out float label_y)
|
||||
{
|
||||
var height = Notification.ICON_SIZE;
|
||||
|
||||
label_x = Notification.MARGIN + Notification.PADDING + height + Notification.SPACING;
|
||||
label_width = Notification.WIDTH - label_x - Notification.MARGIN - Notification.SPACING;
|
||||
|
||||
summary_label.get_preferred_height (label_width, null, out summary_height);
|
||||
body_label.get_preferred_height (label_width, null, out body_height);
|
||||
|
||||
label_height = summary_height + LABEL_SPACING + body_height;
|
||||
label_y = Notification.MARGIN + Notification.PADDING;
|
||||
// center
|
||||
if (label_height < height) {
|
||||
label_y += (height - (int) label_height) / 2;
|
||||
label_height = height;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from gnome-shell, fixes the mess of markup that is sent to us
|
||||
*/
|
||||
string fix_markup (string markup)
|
||||
{
|
||||
var text = markup;
|
||||
|
||||
try {
|
||||
text = entity_regex.replace (markup, markup.length, 0, "&");
|
||||
text = tag_regex.replace (text, text.length, 0, "<");
|
||||
} catch (Error e) {}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
public class NormalNotification : Notification
|
||||
{
|
||||
public string summary { get; construct set; }
|
||||
public string body { get; construct set; }
|
||||
public uint32 sender_pid { get; construct; }
|
||||
public string[] notification_actions { get; construct; }
|
||||
public Screen screen { get; construct; }
|
||||
|
||||
Actor content_container;
|
||||
NormalNotificationContent notification_content;
|
||||
NormalNotificationContent? old_notification_content = null;
|
||||
|
||||
public NormalNotification (Screen screen, uint32 id, string summary, string body, Gdk.Pixbuf? icon,
|
||||
NotificationUrgency urgency, int32 expire_timeout, uint32 pid, string[] actions)
|
||||
{
|
||||
Object (
|
||||
id: id,
|
||||
icon: icon,
|
||||
urgency: urgency,
|
||||
expire_timeout: expire_timeout,
|
||||
screen: screen,
|
||||
summary: summary,
|
||||
body: body,
|
||||
sender_pid: pid,
|
||||
notification_actions: actions
|
||||
);
|
||||
}
|
||||
|
||||
construct
|
||||
{
|
||||
content_container = new Actor ();
|
||||
|
||||
notification_content = new NormalNotificationContent ();
|
||||
notification_content.set_values (summary, body);
|
||||
|
||||
content_container.add_child (notification_content);
|
||||
add_child (content_container);
|
||||
}
|
||||
|
||||
public void update (string summary, string body, Gdk.Pixbuf? icon, int32 expire_timeout,
|
||||
string[] actions)
|
||||
{
|
||||
var visible_change = this.summary != summary || this.body != body;
|
||||
|
||||
if (visible_change) {
|
||||
if (old_notification_content != null)
|
||||
old_notification_content.destroy ();
|
||||
|
||||
old_notification_content = new NormalNotificationContent ();
|
||||
old_notification_content.set_values (this.summary, this.body);
|
||||
|
||||
content_container.add_child (old_notification_content);
|
||||
|
||||
this.summary = summary;
|
||||
this.body = body;
|
||||
notification_content.set_values (summary, body);
|
||||
|
||||
float content_height, old_content_height;
|
||||
notification_content.get_preferred_height (0, null, out content_height);
|
||||
old_notification_content.get_preferred_height (0, null, out old_content_height);
|
||||
|
||||
content_height = float.max (content_height, old_content_height);
|
||||
|
||||
play_update_transition (content_height + PADDING * 2);
|
||||
|
||||
get_transition ("switch").completed.connect (() => {
|
||||
if (old_notification_content != null)
|
||||
old_notification_content.destroy ();
|
||||
old_notification_content = null;
|
||||
});
|
||||
}
|
||||
|
||||
update_base (icon, expire_timeout);
|
||||
}
|
||||
|
||||
protected override void update_slide_animation ()
|
||||
{
|
||||
if (old_notification_content != null)
|
||||
old_notification_content.y = animation_slide_y_offset;
|
||||
|
||||
notification_content.y = animation_slide_y_offset - animation_slide_height;
|
||||
}
|
||||
|
||||
public override void update_allocation (out float content_height, AllocationFlags flags)
|
||||
{
|
||||
var box = ActorBox ();
|
||||
box.set_origin (0, 0);
|
||||
box.set_size (width, height);
|
||||
|
||||
content_container.allocate (box, flags);
|
||||
|
||||
// the for_width is not needed in our implementation of get_preferred_height as we
|
||||
// assume a constant width
|
||||
notification_content.get_preferred_height (0, null, out content_height);
|
||||
|
||||
content_container.set_clip (MARGIN, MARGIN, MARGIN * 2 + WIDTH, content_height + PADDING * 2);
|
||||
}
|
||||
|
||||
public override void get_preferred_height (float for_width, out float min_height, out float nat_height)
|
||||
{
|
||||
float content_height;
|
||||
notification_content.get_preferred_height (for_width, null, out content_height);
|
||||
|
||||
min_height = nat_height = content_height + (MARGIN + PADDING) * 2;
|
||||
}
|
||||
|
||||
public override void activate ()
|
||||
{
|
||||
var window = get_window ();
|
||||
if (window != null) {
|
||||
var workspace = window.get_workspace ();
|
||||
var time = screen.get_display ().get_current_time ();
|
||||
|
||||
if (workspace != screen.get_active_workspace ())
|
||||
workspace.activate_with_focus (window, time);
|
||||
else
|
||||
window.activate (time);
|
||||
}
|
||||
}
|
||||
|
||||
Window? get_window ()
|
||||
{
|
||||
if (sender_pid == 0)
|
||||
return null;
|
||||
|
||||
foreach (var actor in Compositor.get_window_actors (screen)) {
|
||||
var window = actor.get_meta_window ();
|
||||
|
||||
// the windows are sorted by stacking order when returned
|
||||
// from meta_get_window_actors, so we can just pick the first
|
||||
// one we find and have a pretty good match
|
||||
if (window.get_pid () == sender_pid)
|
||||
return window;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
394
plugins/notify/Notification.vala
Normal file
394
plugins/notify/Notification.vala
Normal file
@ -0,0 +1,394 @@
|
||||
//
|
||||
// Copyright (C) 2014 Tom Beckmann
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
using Clutter;
|
||||
using Meta;
|
||||
|
||||
namespace Gala.Plugins.Notify
|
||||
{
|
||||
public abstract class Notification : Actor
|
||||
{
|
||||
public static Gtk.CssProvider? default_css = null;
|
||||
|
||||
public const int WIDTH = 300;
|
||||
public const int ICON_SIZE = 48;
|
||||
public const int MARGIN = 12;
|
||||
|
||||
public const int SPACING = 6;
|
||||
public const int PADDING = 4;
|
||||
|
||||
public signal void closed (uint32 id, uint32 reason);
|
||||
|
||||
public uint32 id { get; construct; }
|
||||
public Gdk.Pixbuf? icon { get; construct set; }
|
||||
public NotificationUrgency urgency { get; construct; }
|
||||
public int32 expire_timeout { get; construct set; }
|
||||
|
||||
public uint64 relevancy_time { get; private set; }
|
||||
public bool being_destroyed { get; private set; default = false; }
|
||||
|
||||
protected bool icon_only { get; protected set; default = false; }
|
||||
protected GtkClutter.Texture icon_texture { get; private set; }
|
||||
protected Actor icon_container { get; private set; }
|
||||
|
||||
/**
|
||||
* Whether we're currently sliding content for an update animation
|
||||
*/
|
||||
protected bool transitioning { get; private set; default = false; }
|
||||
|
||||
GtkClutter.Texture close_button;
|
||||
|
||||
Gtk.StyleContext style_context;
|
||||
|
||||
uint remove_timeout = 0;
|
||||
|
||||
// temporary things needed for the slide transition
|
||||
protected float animation_slide_height { get; private set; }
|
||||
GtkClutter.Texture old_texture;
|
||||
float _animation_slide_y_offset = 0.0f;
|
||||
public float animation_slide_y_offset {
|
||||
get {
|
||||
return _animation_slide_y_offset;
|
||||
}
|
||||
set {
|
||||
_animation_slide_y_offset = value;
|
||||
|
||||
icon_texture.y = -animation_slide_height + _animation_slide_y_offset;
|
||||
old_texture.y = _animation_slide_y_offset;
|
||||
|
||||
update_slide_animation ();
|
||||
}
|
||||
}
|
||||
|
||||
public Notification (uint32 id, Gdk.Pixbuf? icon, NotificationUrgency urgency,
|
||||
int32 expire_timeout)
|
||||
{
|
||||
Object (
|
||||
id: id,
|
||||
icon: icon,
|
||||
urgency: urgency,
|
||||
expire_timeout: expire_timeout
|
||||
);
|
||||
}
|
||||
|
||||
construct
|
||||
{
|
||||
relevancy_time = new DateTime.now_local ().to_unix ();
|
||||
width = WIDTH + MARGIN * 2;
|
||||
reactive = true;
|
||||
set_pivot_point (0.5f, 0.5f);
|
||||
|
||||
icon_texture = new GtkClutter.Texture ();
|
||||
icon_texture.set_pivot_point (0.5f, 0.5f);
|
||||
|
||||
icon_container = new Actor ();
|
||||
icon_container.add_child (icon_texture);
|
||||
|
||||
close_button = Utils.create_close_button ();
|
||||
close_button.opacity = 0;
|
||||
close_button.reactive = true;
|
||||
close_button.set_easing_duration (300);
|
||||
|
||||
var close_click = new ClickAction ();
|
||||
close_click.clicked.connect (() => {
|
||||
closed (id, NotificationClosedReason.DISMISSED);
|
||||
close ();
|
||||
});
|
||||
close_button.add_action (close_click);
|
||||
|
||||
add_child (icon_container);
|
||||
add_child (close_button);
|
||||
|
||||
if (default_css == null) {
|
||||
default_css = new Gtk.CssProvider ();
|
||||
try {
|
||||
default_css.load_from_path (Config.PKGDATADIR + "/gala.css");
|
||||
} catch (Error e) {
|
||||
warning ("Loading default styles failed: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
var style_path = new Gtk.WidgetPath ();
|
||||
style_path.append_type (typeof (Gtk.Window));
|
||||
style_path.append_type (typeof (Gtk.EventBox));
|
||||
|
||||
style_context = new Gtk.StyleContext ();
|
||||
style_context.add_provider (default_css, Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK);
|
||||
style_context.add_class ("gala-notification");
|
||||
style_context.set_path (style_path);
|
||||
|
||||
var label_style_path = style_path.copy ();
|
||||
label_style_path.iter_add_class (1, "gala-notification");
|
||||
label_style_path.append_type (typeof (Gtk.Label));
|
||||
|
||||
var canvas = new Canvas ();
|
||||
canvas.draw.connect (draw);
|
||||
content = canvas;
|
||||
|
||||
set_values ();
|
||||
|
||||
var click = new ClickAction ();
|
||||
click.clicked.connect (() => {
|
||||
activate ();
|
||||
});
|
||||
add_action (click);
|
||||
|
||||
open ();
|
||||
}
|
||||
|
||||
public void open () {
|
||||
var entry = new TransitionGroup ();
|
||||
entry.remove_on_complete = true;
|
||||
entry.duration = 400;
|
||||
|
||||
var opacity_transition = new PropertyTransition ("opacity");
|
||||
opacity_transition.set_from_value (0);
|
||||
opacity_transition.set_to_value (255);
|
||||
|
||||
var flip_transition = new KeyframeTransition ("rotation-angle-x");
|
||||
flip_transition.set_from_value (90.0);
|
||||
flip_transition.set_to_value (0.0);
|
||||
flip_transition.set_key_frames ({ 0.6 });
|
||||
flip_transition.set_values ({ -10.0 });
|
||||
|
||||
entry.add_transition (opacity_transition);
|
||||
entry.add_transition (flip_transition);
|
||||
add_transition ("entry", entry);
|
||||
|
||||
switch (urgency) {
|
||||
case NotificationUrgency.LOW:
|
||||
case NotificationUrgency.NORMAL:
|
||||
return;
|
||||
case NotificationUrgency.CRITICAL:
|
||||
var icon_entry = new TransitionGroup ();
|
||||
icon_entry.duration = 1000;
|
||||
icon_entry.remove_on_complete = true;
|
||||
icon_entry.progress_mode = AnimationMode.EASE_IN_OUT_CUBIC;
|
||||
|
||||
double[] keyframes = { 0.2, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
|
||||
GLib.Value[] scale = { 0.0, 1.2, 1.6, 1.6, 1.6, 1.6, 1.2, 1.0 };
|
||||
|
||||
var rotate_transition = new KeyframeTransition ("rotation-angle-z");
|
||||
rotate_transition.set_from_value (30.0);
|
||||
rotate_transition.set_to_value (0.0);
|
||||
rotate_transition.set_key_frames (keyframes);
|
||||
rotate_transition.set_values ({ 30.0, -30.0, 30.0, -20.0, 10.0, -5.0, 2.0, 0.0 });
|
||||
|
||||
var scale_x_transition = new KeyframeTransition ("scale-x");
|
||||
scale_x_transition.set_from_value (0.0);
|
||||
scale_x_transition.set_to_value (1.0);
|
||||
scale_x_transition.set_key_frames (keyframes);
|
||||
scale_x_transition.set_values (scale);
|
||||
|
||||
var scale_y_transition = new KeyframeTransition ("scale-y");
|
||||
scale_y_transition.set_from_value (0.0);
|
||||
scale_y_transition.set_to_value (1.0);
|
||||
scale_y_transition.set_key_frames (keyframes);
|
||||
scale_y_transition.set_values (scale);
|
||||
|
||||
icon_entry.add_transition (rotate_transition);
|
||||
icon_entry.add_transition (scale_x_transition);
|
||||
icon_entry.add_transition (scale_y_transition);
|
||||
|
||||
icon_texture.add_transition ("entry", icon_entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void close ()
|
||||
{
|
||||
set_easing_duration (100);
|
||||
|
||||
set_easing_mode (AnimationMode.EASE_IN_QUAD);
|
||||
opacity = 0;
|
||||
|
||||
x = WIDTH + MARGIN * 2;
|
||||
|
||||
being_destroyed = true;
|
||||
var transition = get_transition ("x");
|
||||
if (transition != null)
|
||||
transition.completed.connect (() => destroy ());
|
||||
else
|
||||
destroy ();
|
||||
}
|
||||
|
||||
protected void update_base (Gdk.Pixbuf? icon, int32 expire_timeout)
|
||||
{
|
||||
this.icon = icon;
|
||||
this.expire_timeout = expire_timeout;
|
||||
this.relevancy_time = new DateTime.now_local ().to_unix ();
|
||||
|
||||
set_values ();
|
||||
}
|
||||
|
||||
void set_values ()
|
||||
{
|
||||
if (icon != null) {
|
||||
try {
|
||||
icon_texture.set_from_pixbuf (icon);
|
||||
} catch (Error e) {}
|
||||
}
|
||||
|
||||
set_timeout ();
|
||||
}
|
||||
|
||||
void set_timeout ()
|
||||
{
|
||||
// crtitical notifications have to be dismissed manually
|
||||
if (expire_timeout <= 0 || urgency == NotificationUrgency.CRITICAL)
|
||||
return;
|
||||
|
||||
clear_timeout ();
|
||||
|
||||
remove_timeout = Timeout.add (expire_timeout, () => {
|
||||
closed (id, NotificationClosedReason.EXPIRED);
|
||||
close ();
|
||||
remove_timeout = 0;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void clear_timeout ()
|
||||
{
|
||||
if (remove_timeout != 0) {
|
||||
Source.remove (remove_timeout);
|
||||
remove_timeout = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool enter_event (CrossingEvent event)
|
||||
{
|
||||
close_button.opacity = 255;
|
||||
|
||||
clear_timeout ();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool leave_event (CrossingEvent event)
|
||||
{
|
||||
close_button.opacity = 0;
|
||||
|
||||
// TODO consider decreasing the timeout now or calculating the remaining
|
||||
set_timeout ();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual void activate ()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void draw_content (Cairo.Context cr)
|
||||
{
|
||||
}
|
||||
|
||||
public abstract void update_allocation (out float content_height, AllocationFlags flags);
|
||||
|
||||
public override void allocate (ActorBox box, AllocationFlags flags)
|
||||
{
|
||||
var icon_alloc = ActorBox ();
|
||||
|
||||
icon_alloc.set_origin (icon_only ? (WIDTH - ICON_SIZE) / 2 : MARGIN + PADDING, MARGIN + PADDING);
|
||||
icon_alloc.set_size (ICON_SIZE, ICON_SIZE);
|
||||
icon_container.allocate (icon_alloc, flags);
|
||||
|
||||
var close_alloc = ActorBox ();
|
||||
close_alloc.set_origin (MARGIN + PADDING - close_button.width / 2,
|
||||
MARGIN + PADDING - close_button.height / 2);
|
||||
close_alloc.set_size (close_button.width, close_button.height);
|
||||
close_button.allocate (close_alloc, flags);
|
||||
|
||||
float content_height;
|
||||
update_allocation (out content_height, flags);
|
||||
box.set_size (MARGIN * 2 + WIDTH, (MARGIN + PADDING) * 2 + content_height);
|
||||
|
||||
base.allocate (box, flags);
|
||||
|
||||
var canvas = (Canvas) content;
|
||||
var canvas_width = (int) box.get_width ();
|
||||
var canvas_height = (int) box.get_height ();
|
||||
if (canvas.width != canvas_width || canvas.height != canvas_height)
|
||||
canvas.set_size (canvas_width, canvas_height);
|
||||
}
|
||||
|
||||
public override void get_preferred_height (float for_width, out float min_height, out float nat_height)
|
||||
{
|
||||
min_height = nat_height = ICON_SIZE + (MARGIN + PADDING) * 2;
|
||||
}
|
||||
|
||||
protected void play_update_transition (float slide_height)
|
||||
{
|
||||
Transition transition;
|
||||
if ((transition = get_transition ("switch")) != null) {
|
||||
transition.completed ();
|
||||
remove_transition ("switch");
|
||||
}
|
||||
|
||||
animation_slide_height = slide_height;
|
||||
|
||||
old_texture = new GtkClutter.Texture ();
|
||||
icon_container.add_child (old_texture);
|
||||
icon_container.set_clip (0, -PADDING, ICON_SIZE, ICON_SIZE + PADDING * 2);
|
||||
|
||||
try {
|
||||
old_texture.set_from_pixbuf (this.icon);
|
||||
} catch (Error e) {}
|
||||
|
||||
transition = new PropertyTransition ("animation-slide-y-offset");
|
||||
transition.duration = 200;
|
||||
transition.progress_mode = AnimationMode.EASE_IN_OUT_QUAD;
|
||||
transition.set_from_value (0.0f);
|
||||
transition.set_to_value (animation_slide_height);
|
||||
transition.remove_on_complete = true;
|
||||
|
||||
transition.completed.connect (() => {
|
||||
old_texture.destroy ();
|
||||
icon_container.remove_clip ();
|
||||
_animation_slide_y_offset = 0;
|
||||
transitioning = false;
|
||||
});
|
||||
|
||||
add_transition ("switch", transition);
|
||||
transitioning = true;
|
||||
}
|
||||
|
||||
protected virtual void update_slide_animation ()
|
||||
{
|
||||
}
|
||||
|
||||
bool draw (Cairo.Context cr)
|
||||
{
|
||||
var canvas = (Canvas) content;
|
||||
|
||||
var x = MARGIN;
|
||||
var y = MARGIN;
|
||||
var width = canvas.width - MARGIN * 2;
|
||||
var height = canvas.height - MARGIN * 2;
|
||||
cr.set_operator (Cairo.Operator.CLEAR);
|
||||
cr.paint ();
|
||||
cr.set_operator (Cairo.Operator.OVER);
|
||||
|
||||
style_context.render_background (cr, x, y, width, height);
|
||||
style_context.render_frame (cr, x, y, width, height);
|
||||
|
||||
draw_content (cr);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
96
plugins/notify/NotificationStack.vala
Normal file
96
plugins/notify/NotificationStack.vala
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// Copyright (C) 2014 Tom Beckmann
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
using Clutter;
|
||||
using Meta;
|
||||
|
||||
namespace Gala.Plugins.Notify
|
||||
{
|
||||
public class NotificationStack : Actor
|
||||
{
|
||||
const int ADDITIONAL_MARGIN = 12;
|
||||
|
||||
public signal void animations_changed (bool running);
|
||||
|
||||
public Screen screen { get; construct; }
|
||||
|
||||
int animation_counter = 0;
|
||||
|
||||
public NotificationStack (Screen screen)
|
||||
{
|
||||
Object (screen: screen);
|
||||
}
|
||||
|
||||
construct
|
||||
{
|
||||
width = Notification.WIDTH + 2 * Notification.MARGIN + ADDITIONAL_MARGIN;
|
||||
clip_to_allocation = true;
|
||||
}
|
||||
|
||||
public void show_notification (Notification notification)
|
||||
{
|
||||
if (animation_counter == 0)
|
||||
animations_changed (true);
|
||||
|
||||
// raise ourselves when we got something to show
|
||||
get_parent ().set_child_above_sibling (this, null);
|
||||
|
||||
// we have a shoot-over on the start of the close animation, which gets clipped
|
||||
// unless we make our container a bit wider and move the notifications over
|
||||
notification.margin_left = ADDITIONAL_MARGIN;
|
||||
|
||||
notification.destroy.connect (() => {
|
||||
update_positions ();
|
||||
});
|
||||
|
||||
float height;
|
||||
notification.get_preferred_height (Notification.WIDTH, out height, null);
|
||||
update_positions (height);
|
||||
|
||||
insert_child_at_index (notification, 0);
|
||||
|
||||
animation_counter++;
|
||||
|
||||
notification.get_transition ("entry").completed.connect (() => {
|
||||
if (--animation_counter == 0)
|
||||
animations_changed (false);
|
||||
});
|
||||
}
|
||||
|
||||
void update_positions (float add_y = 0.0f)
|
||||
{
|
||||
var y = add_y;
|
||||
var i = get_n_children ();
|
||||
var delay_step = i > 0 ? 150 / i : 0;
|
||||
foreach (var child in get_children ()) {
|
||||
if (((Notification) child).being_destroyed)
|
||||
continue;
|
||||
|
||||
child.save_easing_state ();
|
||||
child.set_easing_mode (AnimationMode.EASE_OUT_BACK);
|
||||
child.set_easing_duration (200);
|
||||
child.set_easing_delay ((i--) * delay_step);
|
||||
|
||||
child.y = y;
|
||||
child.restore_easing_state ();
|
||||
|
||||
y += child.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
457
plugins/notify/NotifyServer.vala
Normal file
457
plugins/notify/NotifyServer.vala
Normal file
@ -0,0 +1,457 @@
|
||||
//
|
||||
// Copyright (C) 2014 Tom Beckmann
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
using Meta;
|
||||
|
||||
namespace Gala.Plugins.Notify
|
||||
{
|
||||
public enum NotificationUrgency
|
||||
{
|
||||
LOW = 0,
|
||||
NORMAL = 1,
|
||||
CRITICAL = 2
|
||||
}
|
||||
|
||||
public enum NotificationClosedReason
|
||||
{
|
||||
EXPIRED = 1,
|
||||
DISMISSED = 2,
|
||||
CLOSE_NOTIFICATION_CALL = 3,
|
||||
UNDEFINED = 4
|
||||
}
|
||||
|
||||
[DBus (name = "org.freedesktop.DBus")]
|
||||
private interface DBus : Object
|
||||
{
|
||||
[DBus (name = "GetConnectionUnixProcessID")]
|
||||
public abstract uint32 get_connection_unix_process_id (string name) throws Error;
|
||||
}
|
||||
|
||||
[DBus (name = "org.freedesktop.Notifications")]
|
||||
public class NotifyServer : Object
|
||||
{
|
||||
const int DEFAULT_TMEOUT = 4000;
|
||||
const string FALLBACK_ICON = "dialog-information";
|
||||
|
||||
[DBus (visible = false)]
|
||||
public signal void show_notification (Notification notification);
|
||||
|
||||
public signal void notification_closed (uint32 id, uint32 reason);
|
||||
public signal void action_invoked (uint32 id, string action_key);
|
||||
|
||||
[DBus (visible = false)]
|
||||
public NotificationStack stack { get; construct; }
|
||||
|
||||
uint32 id_counter = 0;
|
||||
|
||||
DBus? bus_proxy = null;
|
||||
unowned Canberra.Context? ca_context = null;
|
||||
|
||||
public NotifyServer (NotificationStack stack)
|
||||
{
|
||||
Object (stack: stack);
|
||||
}
|
||||
|
||||
construct
|
||||
{
|
||||
try {
|
||||
bus_proxy = Bus.get_proxy_sync (BusType.SESSION, "org.freedesktop.DBus", "/");
|
||||
} catch (Error e) {
|
||||
warning (e.message);
|
||||
bus_proxy = null;
|
||||
}
|
||||
|
||||
var locale = Intl.setlocale (LocaleCategory.MESSAGES, null);
|
||||
ca_context = CanberraGtk.context_get ();
|
||||
ca_context.change_props (Canberra.PROP_APPLICATION_NAME, "Gala",
|
||||
Canberra.PROP_APPLICATION_ID, "org.pantheon.gala",
|
||||
Canberra.PROP_APPLICATION_NAME, "start-here",
|
||||
Canberra.PROP_APPLICATION_LANGUAGE, locale,
|
||||
null);
|
||||
ca_context.open ();
|
||||
}
|
||||
|
||||
public string [] get_capabilities ()
|
||||
{
|
||||
return {
|
||||
"body",
|
||||
"body-markup",
|
||||
"sound",
|
||||
"x-canonical-private-synchronous",
|
||||
"x-canonical-private-icon-only"
|
||||
};
|
||||
}
|
||||
|
||||
public void get_server_information (out string name, out string vendor,
|
||||
out string version, out string spec_version)
|
||||
{
|
||||
name = "pantheon-notify";
|
||||
vendor = "elementaryOS";
|
||||
version = "0.1";
|
||||
spec_version = "1.1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the CloseNotification DBus method
|
||||
*
|
||||
* @param id The id of the notification to be closed.
|
||||
*/
|
||||
public void close_notification (uint32 id) throws DBusError
|
||||
{
|
||||
foreach (var child in stack.get_children ()) {
|
||||
unowned Notification notification = (Notification) child;
|
||||
if (notification.id != id)
|
||||
continue;
|
||||
|
||||
notification_closed_callback (notification, id,
|
||||
NotificationClosedReason.CLOSE_NOTIFICATION_CALL);
|
||||
notification.close ();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// according to spec, an empty dbus error should be sent if the notification
|
||||
// doesn't exist (anymore)
|
||||
throw new DBusError.FAILED ("");
|
||||
}
|
||||
|
||||
public new uint32 notify (string app_name, uint32 replaces_id, string app_icon, string summary,
|
||||
string body, string[] actions, HashTable<string, Variant> hints, int32 expire_timeout, BusName sender)
|
||||
{
|
||||
Variant? variant;
|
||||
|
||||
var id = replaces_id != 0 ? replaces_id : ++id_counter;
|
||||
var pixbuf = get_pixbuf (app_name, app_icon, hints);
|
||||
var timeout = expire_timeout == uint32.MAX ? DEFAULT_TMEOUT : expire_timeout;
|
||||
|
||||
var urgency = NotificationUrgency.NORMAL;
|
||||
if ((variant = hints.lookup ("urgency")) != null)
|
||||
urgency = (NotificationUrgency) variant.get_byte ();
|
||||
|
||||
var icon_only = hints.contains ("x-canonical-private-icon-only");
|
||||
var confirmation = hints.contains ("x-canonical-private-synchronous");
|
||||
var progress = confirmation && hints.contains ("value");
|
||||
|
||||
#if 0 // enable to debug notifications
|
||||
print ("Notification from '%s', replaces: %u\n" +
|
||||
"\tapp icon: '%s'\n\tsummary: '%s'\n\tbody: '%s'\n\tn actions: %u\n\texpire: %i\n\tHints:\n",
|
||||
app_name, replaces_id, app_icon, summary, body, actions.length);
|
||||
hints.@foreach ((key, val) => {
|
||||
print ("\t\t%s => %s\n", key, val.is_of_type (VariantType.STRING) ?
|
||||
val.get_string () : "<" + val.get_type ().dup_string () + ">");
|
||||
});
|
||||
#endif
|
||||
|
||||
uint32 pid = 0;
|
||||
try {
|
||||
pid = bus_proxy.get_connection_unix_process_id (sender);
|
||||
} catch (Error e) { warning (e.message); }
|
||||
|
||||
handle_sounds (hints);
|
||||
|
||||
foreach (var child in stack.get_children ()) {
|
||||
unowned Notification notification = (Notification) child;
|
||||
|
||||
if (notification.being_destroyed)
|
||||
continue;
|
||||
|
||||
// we only want a single confirmation notification, so we just take the
|
||||
// first one that can be found, no need to check ids or anything
|
||||
unowned ConfirmationNotification? confirmation_notification = notification as ConfirmationNotification;
|
||||
if (confirmation
|
||||
&& confirmation_notification != null) {
|
||||
confirmation_notification.update (pixbuf,
|
||||
progress ? hints.@get ("value").get_int32 () : -1,
|
||||
hints.@get ("x-canonical-private-synchronous").get_string (),
|
||||
icon_only);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
unowned NormalNotification? normal_notification = notification as NormalNotification;
|
||||
if (!confirmation
|
||||
&& notification.id == id
|
||||
&& normal_notification != null) {
|
||||
|
||||
normal_notification.update (summary, body, pixbuf, timeout, actions);
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
Notification notification;
|
||||
if (confirmation)
|
||||
notification = new ConfirmationNotification (id, pixbuf, icon_only,
|
||||
progress ? hints.@get ("value").get_int32 () : -1,
|
||||
hints.@get ("x-canonical-private-synchronous").get_string ());
|
||||
else
|
||||
notification = new NormalNotification (stack.screen, id, summary, body, pixbuf,
|
||||
urgency, timeout, pid, actions);
|
||||
|
||||
notification.closed.connect (notification_closed_callback);
|
||||
stack.show_notification (notification);
|
||||
|
||||
#if !VALA_0_26
|
||||
// fixes memleaks as described in https://bugzilla.gnome.org/show_bug.cgi?id=698260
|
||||
// valac >= 0.26 already has this fix
|
||||
hints.@foreach ((key, val) => {
|
||||
g_variant_unref (val);
|
||||
});
|
||||
#endif
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
static Gdk.Pixbuf? get_pixbuf (string app_name, string app_icon, HashTable<string, Variant> hints)
|
||||
{
|
||||
// decide on the icon, order:
|
||||
// - image-data
|
||||
// - image-path
|
||||
// - app_icon
|
||||
// - icon_data
|
||||
// - from app name?
|
||||
// - fallback to dialog-information
|
||||
|
||||
Gdk.Pixbuf? pixbuf = null;
|
||||
Variant? variant = null;
|
||||
var size = Notification.ICON_SIZE;
|
||||
var mask_offset = 4;
|
||||
var mask_size_offset = mask_offset * 2;
|
||||
var has_mask = false;
|
||||
|
||||
if ((variant = hints.lookup ("image-data")) != null
|
||||
|| (variant = hints.lookup ("image_data")) != null
|
||||
|| (variant = hints.lookup ("icon_data")) != null) {
|
||||
|
||||
has_mask = true;
|
||||
size = size - mask_size_offset;
|
||||
|
||||
pixbuf = load_from_variant_at_size (variant, size);
|
||||
|
||||
} else if ((variant = hints.lookup ("image-path")) != null
|
||||
|| (variant = hints.lookup ("image_path")) != null) {
|
||||
|
||||
var image_path = variant.get_string ();
|
||||
|
||||
try {
|
||||
if (image_path.has_prefix ("file://") || image_path.has_prefix ("/")) {
|
||||
has_mask = true;
|
||||
size = size - mask_size_offset;
|
||||
|
||||
var file_path = File.new_for_commandline_arg (image_path).get_path ();
|
||||
pixbuf = new Gdk.Pixbuf.from_file_at_scale (file_path, size, size, true);
|
||||
} else {
|
||||
pixbuf = Gtk.IconTheme.get_default ().load_icon (image_path, size, 0);
|
||||
}
|
||||
} catch (Error e) { warning (e.message); }
|
||||
|
||||
} else if (app_icon != "") {
|
||||
|
||||
try {
|
||||
var themed = new ThemedIcon.with_default_fallbacks (app_icon);
|
||||
var info = Gtk.IconTheme.get_default ().lookup_by_gicon (themed, size, 0);
|
||||
if (info != null)
|
||||
pixbuf = info.load_icon ();
|
||||
} catch (Error e) { warning (e.message); }
|
||||
|
||||
}
|
||||
|
||||
if (pixbuf == null) {
|
||||
|
||||
try {
|
||||
pixbuf = Gtk.IconTheme.get_default ().load_icon (app_name.down (), size, 0);
|
||||
} catch (Error e) {
|
||||
|
||||
try {
|
||||
pixbuf = Gtk.IconTheme.get_default ().load_icon (FALLBACK_ICON, size, 0);
|
||||
} catch (Error e) { warning (e.message); }
|
||||
}
|
||||
} else if (has_mask) {
|
||||
var mask_size = Notification.ICON_SIZE;
|
||||
var offset_x = mask_offset;
|
||||
var offset_y = mask_offset + 1;
|
||||
|
||||
var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, mask_size, mask_size);
|
||||
var cr = new Cairo.Context (surface);
|
||||
|
||||
Granite.Drawing.Utilities.cairo_rounded_rectangle (cr,
|
||||
offset_x, offset_y, size, size, 4);
|
||||
cr.clip ();
|
||||
|
||||
Gdk.cairo_set_source_pixbuf (cr, pixbuf, offset_x, offset_y);
|
||||
cr.paint ();
|
||||
|
||||
cr.reset_clip ();
|
||||
|
||||
var mask = new Cairo.ImageSurface.from_png (Config.PKGDATADIR + "/image-mask.png");
|
||||
cr.set_source_surface (mask, 0, 0);
|
||||
cr.paint ();
|
||||
|
||||
pixbuf = Gdk.pixbuf_get_from_surface (surface, 0, 0, mask_size, mask_size);
|
||||
}
|
||||
|
||||
return pixbuf;
|
||||
}
|
||||
|
||||
static Gdk.Pixbuf? load_from_variant_at_size (Variant variant, int size)
|
||||
{
|
||||
if (!variant.is_of_type (new VariantType ("(iiibiiay)"))) {
|
||||
critical ("notify icon/image-data format invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
int width, height, rowstride, bits_per_sample, n_channels;
|
||||
bool has_alpha;
|
||||
|
||||
variant.get ("(iiibiiay)", out width, out height, out rowstride,
|
||||
out has_alpha, out bits_per_sample, out n_channels, null);
|
||||
|
||||
var data = variant.get_child_value (6);
|
||||
unowned uint8[] pixel_data = (uint8[]) data.get_data ();
|
||||
|
||||
var pixbuf = new Gdk.Pixbuf.with_unowned_data (pixel_data, Gdk.Colorspace.RGB, has_alpha,
|
||||
bits_per_sample, width, height, rowstride, null);
|
||||
|
||||
return pixbuf.scale_simple (size, size, Gdk.InterpType.BILINEAR);
|
||||
}
|
||||
|
||||
void handle_sounds (HashTable<string,Variant> hints)
|
||||
{
|
||||
if (ca_context == null)
|
||||
return;
|
||||
|
||||
Variant? variant = null;
|
||||
|
||||
// Are we suppose to play a sound at all?
|
||||
if ((variant = hints.lookup ("supress-sound")) != null
|
||||
&& variant.get_boolean ())
|
||||
return;
|
||||
|
||||
Canberra.Proplist props;
|
||||
Canberra.Proplist.create (out props);
|
||||
props.sets (Canberra.PROP_CANBERRA_CACHE_CONTROL, "volatile");
|
||||
|
||||
bool play_sound = false;
|
||||
|
||||
// no sounds for confirmation bubbles
|
||||
if ((variant = hints.lookup ("x-canonical-private-synchronous")) != null) {
|
||||
var confirmation_type = variant.get_string ();
|
||||
|
||||
// the sound indicator is an exception here, it won't emit a sound at all, even though for
|
||||
// consistency it should. So we make it emit the default one.
|
||||
if (confirmation_type != "indicator-sound")
|
||||
return;
|
||||
|
||||
props.sets (Canberra.PROP_EVENT_ID, "audio-volume-change");
|
||||
play_sound = true;
|
||||
}
|
||||
|
||||
if ((variant = hints.lookup ("sound-name")) != null) {
|
||||
props.sets (Canberra.PROP_EVENT_ID, variant.get_string ());
|
||||
play_sound = true;
|
||||
}
|
||||
|
||||
if ((variant = hints.lookup ("sound-file")) != null) {
|
||||
props.sets (Canberra.PROP_MEDIA_FILENAME, variant.get_string ());
|
||||
play_sound = true;
|
||||
}
|
||||
|
||||
// pick a sound according to the category
|
||||
if (!play_sound) {
|
||||
variant = hints.lookup ("category");
|
||||
string? sound_name = null;
|
||||
|
||||
if (variant != null)
|
||||
sound_name = category_to_sound (variant.get_string ());
|
||||
else
|
||||
sound_name = "dialog-information";
|
||||
|
||||
if (sound_name != null) {
|
||||
props.sets (Canberra.PROP_EVENT_ID, sound_name);
|
||||
play_sound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (play_sound)
|
||||
ca_context.play_full (0, props);
|
||||
}
|
||||
|
||||
static string? category_to_sound (string category)
|
||||
{
|
||||
string? sound = null;
|
||||
|
||||
switch (category) {
|
||||
case "device.added":
|
||||
sound = "device-added";
|
||||
break;
|
||||
case "device.removed":
|
||||
sound = "device-removed";
|
||||
break;
|
||||
case "im":
|
||||
sound = "message";
|
||||
break;
|
||||
case "im.received":
|
||||
sound = "message-new-instant";
|
||||
break;
|
||||
case "network.connected":
|
||||
sound = "network-connectivity-established";
|
||||
break;
|
||||
case "network.disconnected":
|
||||
sound = "network-connectivity-lost";
|
||||
break;
|
||||
case "presence.online":
|
||||
sound = "service-login";
|
||||
break;
|
||||
case "presence.offline":
|
||||
sound = "service-logout";
|
||||
break;
|
||||
// no sound at all
|
||||
case "x-gnome.music":
|
||||
sound = null;
|
||||
break;
|
||||
// generic errors
|
||||
case "device.error":
|
||||
case "email.bounced":
|
||||
case "im.error":
|
||||
case "network.error":
|
||||
case "transfer.error":
|
||||
sound = "dialog-error";
|
||||
break;
|
||||
// use generic default
|
||||
case "network":
|
||||
case "email":
|
||||
case "email.arrived":
|
||||
case "presence":
|
||||
case "transfer":
|
||||
case "transfer.complete":
|
||||
default:
|
||||
sound = "dialog-information";
|
||||
break;
|
||||
}
|
||||
|
||||
return sound;
|
||||
}
|
||||
|
||||
void notification_closed_callback (Notification notification, uint32 id, uint32 reason)
|
||||
{
|
||||
notification.closed.disconnect (notification_closed_callback);
|
||||
|
||||
notification_closed (id, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
plugins/notify/data/image-mask.png
Normal file
BIN
plugins/notify/data/image-mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 542 B |
Loading…
Reference in New Issue
Block a user