Implement notifications plugin

This commit is contained in:
Tom Beckmann 2014-08-10 03:20:35 +00:00 committed by RabbitBot
commit 642e840a15
12 changed files with 1595 additions and 1 deletions

View File

@ -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
])

View File

@ -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
View 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;
}

View File

@ -1,4 +1,5 @@
SUBDIRS = \
notify \
zoom \
$(NULL)

View 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
View 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
};
}

View 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)

View 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, "&amp;");
text = tag_regex.replace (text, text.length, 0, "&lt;");
} 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B