[notification-daemon] Redesign for GNOME 3.0



commit 48feafcd4a73ac23df4669552d689f7d287cb053
Author: William Jon McCann <jmccann redhat com>
Date:   Wed Oct 6 21:50:29 2010 -0400

    Redesign for GNOME 3.0
    
    Not quite finished...

 INSTALL                                 |  236 ----
 configure.ac                            |   33 +-
 data/Makefile.am                        |   17 -
 src/Makefile.am                         |   33 +-
 src/daemon.c                            |  417 +++++++
 src/{daemon => }/daemon.h               |   17 -
 src/daemon/Makefile.am                  |   30 -
 src/daemon/daemon.c                     | 1804 -------------------------------
 src/daemon/engines.c                    |  398 -------
 src/daemon/engines.h                    |   59 -
 src/daemon/stack.h                      |   57 -
 src/nd-bubble.c                         |  984 +++++++++++++++++
 src/nd-bubble.h                         |   59 +
 src/nd-notification.c                   |  600 ++++++++++
 src/nd-notification.h                   |   85 ++
 src/nd-queue.c                          | 1088 +++++++++++++++++++
 src/nd-queue.h                          |   68 ++
 src/{daemon/stack.c => nd-stack.c}      |  275 +++--
 src/nd-stack.h                          |   77 ++
 src/{daemon => }/notificationdaemon.xml |    0
 src/{daemon => }/sound.c                |    0
 src/{daemon => }/sound.h                |    0
 22 files changed, 3569 insertions(+), 2768 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index d81f6b9..c6eaa07 100644
--- a/configure.ac
+++ b/configure.ac
@@ -79,46 +79,22 @@ AC_DEFINE([GETTEXT_PACKAGE], [PACKAGE_TARNAME], [Define to the gettext package n
 dnl ---------------------------------------------------------------------------
 dnl Requirements for the daemon
 dnl ---------------------------------------------------------------------------
-REQ_GTK_VERSION=2.18.0
-REQ_GLIB_VERSION=$REQ_GTK_VERSION
+REQ_GTK_VERSION=2.91.0
+REQ_GLIB_VERSION=2.27.0
 REQ_DBUS_VERSION=0.78
 REQ_LIBCANBERRA_GTK_VERSION=0.4
 pkg_modules="
-	gtk+-2.0 >= $REQ_GTK_VERSION, \
+	gtk+-3.0 >= $REQ_GTK_VERSION, \
 	glib-2.0 >= $REQ_GLIB_VERSION, \
 	dbus-1 >= $REQ_DBUS_VERSION, \
 	dbus-glib-1 >= $REQ_DBUS_VERSION, \
-        libcanberra-gtk >= $REQ_LIBCANBERRA_GTK_VERSION, \
-	gconf-2.0, \
-	libwnck-1.0 \
+        libcanberra-gtk3 >= $REQ_LIBCANBERRA_GTK_VERSION, \
         x11 \
 "
 PKG_CHECK_MODULES(NOTIFICATION_DAEMON, $pkg_modules)
 AC_SUBST(NOTIFICATION_DAEMON_CFLAGS)
 AC_SUBST(NOTIFICATION_DAEMON_LIBS)
 
-gdk_modules="
-	gdk-2.0 >= $REQ_GTK_VERSION, \
-	gdk-pixbuf-2.0 >= $REQ_GTK_VERSION \
-"
-PKG_CHECK_MODULES(GDK, $gdk_modules, have_gdk=yes,
-[
-	have_gdk=no
-	AC_MSG_WARN("Some test apps will not be built")
-])
-
-AM_CONDITIONAL(HAVE_GDK, test "x$have_gdk" = "xyes")
-
-AC_SUBST(GDK_CFLAGS)
-AC_SUBST(GDK_LIBS)
-
-AM_GCONF_SOURCE_2
-AC_PATH_PROG(GCONFTOOL, gconftool-2, no)
-
-if test "x$GCONFTOOL" = "xno"; then
-	AC_MSG_ERROR([gconftool-2 executable not found in your path - should be installed with Gconf])
-fi
-
 dnl
 dnl Check the D-BUS version.
 dnl
@@ -217,7 +193,6 @@ data/Makefile
 data/org.freedesktop.Notifications.service
 po/Makefile.in
 src/Makefile
-src/daemon/Makefile
 ])
 
 AC_OUTPUT
diff --git a/data/Makefile.am b/data/Makefile.am
index 896f983..7145130 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -1,24 +1,7 @@
 servicedir   = $(DBUS_SERVICES_DIR)
 service_DATA = org.freedesktop.Notifications.service
 
-schemasdir       = $(GCONF_SCHEMA_FILE_DIR)
-schemas_in_files = notification-daemon.schemas.in
-schemas_DATA     = $(schemas_in_files:.schemas.in=.schemas)
-
- INTLTOOL_SCHEMAS_RULE@
-
-if GCONF_SCHEMAS_INSTALL
-install-data-local:
-	GCONF_CONFIG_SOURCE=$(GCONF_SCHEMA_CONFIG_SOURCE) $(GCONFTOOL) --makefile-install-rule $(schemas_DATA)
-else
-install-data-local:
-endif
-
 EXTRA_DIST = \
-	$(schemas_in_files) \
 	$(service_DATA)
 
-CLEANFILES = $(schemas_DATA)
-
-
 -include $(top_srcdir)/git.mk
diff --git a/src/Makefile.am b/src/Makefile.am
index 4b42b29..ad3cb8a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,3 +1,34 @@
-SUBDIRS = daemon
+libexec_PROGRAMS = notification-daemon
+
+notification_daemon_SOURCES = \
+	nd-notification.c \
+	nd-notification.h \
+	nd-bubble.c \
+	nd-bubble.h \
+	nd-stack.c \
+	nd-stack.h \
+	nd-queue.c \
+	nd-queue.h \
+	daemon.c \
+	daemon.h \
+	sound.c \
+	sound.h
+
+notification_daemon_LDADD = $(NOTIFICATION_DAEMON_LIBS)
+
+BUILT_SOURCES = notificationdaemon-dbus-glue.h
+
+notificationdaemon-dbus-glue.h: notificationdaemon.xml
+	dbus-binding-tool --mode=glib-server --prefix=notification_daemon \
+		$(srcdir)/notificationdaemon.xml > notificationdaemon-dbus-glue.h
+
+INCLUDES = \
+	-I$(top_srcdir) \
+	$(NOTIFICATION_DAEMON_CFLAGS) \
+	-DENGINES_DIR=\"$(libdir)/notification-daemon-1.0/engines\"
+
+EXTRA_DIST = notificationdaemon.xml
+DISTCLEANFILES = \
+	notificationdaemon-dbus-glue.h
 
 -include $(top_srcdir)/git.mk
diff --git a/src/daemon.c b/src/daemon.c
new file mode 100644
index 0000000..34325c2
--- /dev/null
+++ b/src/daemon.c
@@ -0,0 +1,417 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2006 Christian Hammond <chipx86 chipx86 com>
+ * Copyright (C) 2005 John (J5) Palmieri <johnp redhat com>
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * 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 2, 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <dbus/dbus.h>
+#include <dbus/dbus-glib.h>
+#include <glib/gi18n.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#include <X11/Xproto.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+#include <gdk/gdkx.h>
+
+#include "daemon.h"
+#include "nd-notification.h"
+#include "nd-queue.h"
+#include "notificationdaemon-dbus-glue.h"
+
+#define MAX_NOTIFICATIONS 20
+
+#define IDLE_SECONDS 30
+#define NOTIFICATION_BUS_NAME      "org.freedesktop.Notifications"
+#define NOTIFICATION_BUS_PATH      "/org/freedesktop/Notifications"
+
+#define NW_GET_DAEMON(nw) \
+        (g_object_get_data(G_OBJECT(nw), "_notify_daemon"))
+
+struct _NotifyDaemonPrivate
+{
+        guint           exit_timeout_source;
+        NdQueue        *queue;
+};
+
+static DBusConnection *dbus_conn = NULL;
+
+static void notify_daemon_finalize (GObject *object);
+
+G_DEFINE_TYPE (NotifyDaemon, notify_daemon, G_TYPE_OBJECT);
+
+static void
+notify_daemon_class_init (NotifyDaemonClass *daemon_class)
+{
+        GObjectClass   *object_class = G_OBJECT_CLASS (daemon_class);
+
+        object_class->finalize = notify_daemon_finalize;
+
+        g_type_class_add_private (daemon_class, sizeof (NotifyDaemonPrivate));
+}
+
+static gboolean
+do_exit (gpointer user_data)
+{
+        g_debug ("Exiting due to inactivity");
+        exit (1);
+        return FALSE;
+}
+
+static void
+add_exit_timeout (NotifyDaemon *daemon)
+{
+        if (daemon->priv->exit_timeout_source > 0)
+                return;
+
+        daemon->priv->exit_timeout_source = g_timeout_add_seconds (IDLE_SECONDS, do_exit, NULL);
+}
+
+static void
+remove_exit_timeout (NotifyDaemon *daemon)
+{
+        if (daemon->priv->exit_timeout_source == 0)
+                return;
+
+        g_source_remove (daemon->priv->exit_timeout_source);
+        daemon->priv->exit_timeout_source = 0;
+}
+
+static void
+on_queue_changed (NdQueue      *queue,
+                 NotifyDaemon *daemon)
+{
+        if (nd_queue_length (queue) > 0) {
+                remove_exit_timeout (daemon);
+        } else {
+                add_exit_timeout (daemon);
+        }
+}
+
+static void
+notify_daemon_init (NotifyDaemon *daemon)
+{
+        daemon->priv = G_TYPE_INSTANCE_GET_PRIVATE (daemon,
+                                                    NOTIFY_TYPE_DAEMON,
+                                                    NotifyDaemonPrivate);
+
+        daemon->priv->queue = nd_queue_new ();
+        add_exit_timeout (daemon);
+        g_signal_connect (daemon->priv->queue, "changed", G_CALLBACK (on_queue_changed), daemon);
+}
+
+static void
+notify_daemon_finalize (GObject *object)
+{
+        NotifyDaemon *daemon;
+
+        daemon = NOTIFY_DAEMON (object);
+
+        remove_exit_timeout (daemon);
+
+        g_object_unref (daemon->priv->queue);
+
+        g_free (daemon->priv);
+
+        G_OBJECT_CLASS (notify_daemon_parent_class)->finalize (object);
+}
+
+static DBusMessage *
+create_signal_for_notification (NdNotification *notification,
+                                const char     *signal_name)
+{
+        guint           id;
+        const char     *dest;
+        DBusMessage    *message;
+
+        id = nd_notification_get_id (notification);
+        dest = nd_notification_get_sender (notification);
+        g_assert (dest != NULL);
+
+        message = dbus_message_new_signal (NOTIFICATION_BUS_PATH,
+                                           NOTIFICATION_BUS_NAME,
+                                           signal_name);
+
+        dbus_message_set_destination (message, dest);
+        dbus_message_append_args (message,
+                                  DBUS_TYPE_UINT32,
+                                  &id,
+                                  DBUS_TYPE_INVALID);
+
+        return message;
+}
+
+GQuark
+notify_daemon_error_quark (void)
+{
+        static GQuark   q = 0;
+
+        if (q == 0)
+                q = g_quark_from_static_string ("notification-daemon-error-quark");
+
+        return q;
+}
+
+static void
+on_notification_close (NdNotification *notification,
+                       int             reason,
+                       NotifyDaemon   *daemon)
+{
+        DBusMessage *message;
+
+        message = create_signal_for_notification (notification, "NotificationClosed");
+        dbus_message_append_args (message,
+                                  DBUS_TYPE_UINT32,
+                                  &reason,
+                                  DBUS_TYPE_INVALID);
+        dbus_connection_send (dbus_conn, message, NULL);
+        dbus_message_unref (message);
+
+        nd_queue_remove (daemon->priv->queue,
+                         nd_notification_get_id (notification));
+}
+
+static void
+on_notification_action_invoked (NdNotification *notification,
+                                const char     *action,
+                                NotifyDaemon   *daemon)
+{
+        guint           id;
+        DBusMessage    *message;
+
+        id = nd_notification_get_id (notification);
+        message = create_signal_for_notification (notification, "ActionInvoked");
+        dbus_message_append_args (message,
+                                  DBUS_TYPE_STRING,
+                                  &action,
+                                  DBUS_TYPE_INVALID);
+
+        dbus_connection_send (dbus_conn, message, NULL);
+        dbus_message_unref (message);
+
+        nd_notification_close (notification, ND_NOTIFICATION_CLOSED_USER);
+}
+
+gboolean
+notify_daemon_notify_handler (NotifyDaemon *daemon,
+                              const char   *app_name,
+                              guint         id,
+                              const char   *icon,
+                              const char   *summary,
+                              const char   *body,
+                              char        **actions,
+                              GHashTable   *hints,
+                              int           timeout,
+                              DBusGMethodInvocation *context)
+{
+        NotifyDaemonPrivate *priv = daemon->priv;
+        NdNotification      *notification;
+
+        if (nd_queue_length (priv->queue) > MAX_NOTIFICATIONS) {
+                GError *error;
+
+                error = g_error_new (notify_daemon_error_quark (),
+                                     1,
+                                     _("Exceeded maximum number of notifications"));
+                dbus_g_method_return_error (context, error);
+                g_error_free (error);
+
+                return TRUE;
+        }
+
+        if (id > 0) {
+                notification = nd_queue_lookup (priv->queue, id);
+                if (notification == NULL) {
+                        id = 0;
+                } else {
+                        g_object_ref (notification);
+                }
+        }
+
+        if (id == 0) {
+                char *sender;
+
+                sender = dbus_g_method_get_sender (context);
+
+                notification = nd_notification_new (sender);
+
+                g_free (sender);
+        }
+
+        nd_notification_update (notification,
+                                app_name,
+                                icon,
+                                summary,
+                                body,
+                                (const char **)actions,
+                                hints,
+                                timeout);
+        g_signal_connect (notification, "closed", G_CALLBACK (on_notification_close), daemon);
+        g_signal_connect (notification, "action-invoked", G_CALLBACK (on_notification_action_invoked), daemon);
+
+        if (id == 0) {
+                nd_queue_add (priv->queue, notification);
+        }
+
+        dbus_g_method_return (context, nd_notification_get_id (notification));
+
+        g_object_unref (notification);
+
+        return TRUE;
+}
+
+gboolean
+notify_daemon_close_notification_handler (NotifyDaemon *daemon,
+                                          guint         id,
+                                          GError      **error)
+{
+        if (id == 0) {
+                g_set_error (error,
+                             notify_daemon_error_quark (),
+                             100,
+                             _("%u is not a valid notification ID"),
+                             id);
+                return FALSE;
+        } else {
+                NdNotification *notification;
+
+                notification = nd_queue_lookup (daemon->priv->queue, id);
+                if (notification != NULL) {
+                        nd_notification_close (notification, ND_NOTIFICATION_CLOSED_API);
+                }
+
+                return TRUE;
+        }
+}
+
+gboolean
+notify_daemon_get_capabilities (NotifyDaemon *daemon,
+                                char       ***caps)
+{
+        GPtrArray *a;
+        char     **_caps;
+
+        a = g_ptr_array_new ();
+        g_ptr_array_add (a, g_strdup ("actions"));
+        g_ptr_array_add (a, g_strdup ("body"));
+        g_ptr_array_add (a, g_strdup ("body-hyperlinks"));
+        g_ptr_array_add (a, g_strdup ("body-markup"));
+        g_ptr_array_add (a, g_strdup ("icon-static"));
+        g_ptr_array_add (a, g_strdup ("sound"));
+        g_ptr_array_add (a, NULL);
+        _caps = (char **) g_ptr_array_free (a, FALSE);
+
+        *caps = _caps;
+
+        return TRUE;
+}
+
+gboolean
+notify_daemon_get_server_information (NotifyDaemon *daemon,
+                                      char        **out_name,
+                                      char        **out_vendor,
+                                      char        **out_version,
+                                      char        **out_spec_ver)
+{
+        *out_name = g_strdup ("Notification Daemon");
+        *out_vendor = g_strdup ("GNOME");
+        *out_version = g_strdup (PACKAGE_VERSION);
+        *out_spec_ver = g_strdup ("1.1");
+
+        return TRUE;
+}
+
+int
+main (int argc, char **argv)
+{
+        NotifyDaemon    *daemon;
+        DBusGConnection *connection;
+        DBusGProxy      *bus_proxy;
+        GError          *error;
+        gboolean         res;
+        guint            request_name_result;
+
+        g_log_set_always_fatal (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
+
+        gtk_init (&argc, &argv);
+
+        error = NULL;
+        connection = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
+        if (connection == NULL) {
+                g_printerr ("Failed to open connection to bus: %s\n",
+                            error->message);
+                g_error_free (error);
+                exit (1);
+        }
+
+        dbus_conn = dbus_g_connection_get_connection (connection);
+
+        dbus_g_object_type_install_info (NOTIFY_TYPE_DAEMON,
+                                         &dbus_glib_notification_daemon_object_info);
+
+        bus_proxy = dbus_g_proxy_new_for_name (connection,
+                                               "org.freedesktop.DBus",
+                                               "/org/freedesktop/DBus",
+                                               "org.freedesktop.DBus");
+
+        res = dbus_g_proxy_call (bus_proxy,
+                                 "RequestName",
+                                 &error,
+                                 G_TYPE_STRING, NOTIFICATION_BUS_NAME,
+                                 G_TYPE_UINT, 0,
+                                 G_TYPE_INVALID,
+                                 G_TYPE_UINT, &request_name_result,
+                                 G_TYPE_INVALID);
+        if (! res
+            || request_name_result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
+                if (error != NULL) {
+                        g_warning ("Failed to acquire name %s: %s",
+                                   NOTIFICATION_BUS_NAME,
+                                   error->message);
+                        g_error_free (error);
+                } else {
+                        g_warning ("Failed to acquire name %s", NOTIFICATION_BUS_NAME);
+                }
+                goto out;
+        }
+
+        daemon = g_object_new (NOTIFY_TYPE_DAEMON, NULL);
+
+        dbus_g_connection_register_g_object (connection,
+                                             "/org/freedesktop/Notifications",
+                                             G_OBJECT (daemon));
+
+        gtk_main ();
+
+        g_object_unref (daemon);
+ out:
+
+        return 0;
+}
diff --git a/src/daemon/daemon.h b/src/daemon.h
similarity index 87%
rename from src/daemon/daemon.h
rename to src/daemon.h
index 00e2fe1..7a55dd1 100644
--- a/src/daemon/daemon.h
+++ b/src/daemon.h
@@ -22,19 +22,12 @@
 #ifndef NOTIFY_DAEMON_H
 #define NOTIFY_DAEMON_H
 
-#include <gconf/gconf-client.h>
 #include <glib.h>
 #include <glib-object.h>
 
 #include <dbus/dbus-glib.h>
 #include <dbus/dbus-glib-lowlevel.h>
 
-#define GCONF_KEY_DAEMON         "/apps/notification-daemon"
-#define GCONF_KEY_THEME          GCONF_KEY_DAEMON "/theme"
-#define GCONF_KEY_POPUP_LOCATION GCONF_KEY_DAEMON "/popup_location"
-#define GCONF_KEY_SOUND_ENABLED  GCONF_KEY_DAEMON "/sound_enabled"
-#define GCONF_KEY_DEFAULT_SOUND  GCONF_KEY_DAEMON "/default_sound"
-
 #define NOTIFY_TYPE_DAEMON (notify_daemon_get_type())
 #define NOTIFY_DAEMON(obj) \
         (G_TYPE_CHECK_INSTANCE_CAST ((obj), NOTIFY_TYPE_DAEMON, NotifyDaemon))
@@ -56,14 +49,6 @@ enum
         URGENCY_CRITICAL
 };
 
-typedef enum
-{
-        NOTIFYD_CLOSED_EXPIRED = 1,
-        NOTIFYD_CLOSED_USER = 2,
-        NOTIFYD_CLOSED_API = 3,
-        NOTIFYD_CLOSED_RESERVED = 4
-} NotifydClosedReason;
-
 typedef struct _NotifyDaemon NotifyDaemon;
 typedef struct _NotifyDaemonClass NotifyDaemonClass;
 typedef struct _NotifyDaemonPrivate NotifyDaemonPrivate;
@@ -109,7 +94,5 @@ gboolean        notify_daemon_get_server_information     (NotifyDaemon *daemon,
                                                           char        **out_version,
                                                           char        **out_spec_ver);
 
-GConfClient    *get_gconf_client (void);
-
 G_END_DECLS
 #endif /* NOTIFY_DAEMON_H */
diff --git a/src/nd-bubble.c b/src/nd-bubble.c
new file mode 100644
index 0000000..9c7656e
--- /dev/null
+++ b/src/nd-bubble.c
@@ -0,0 +1,984 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <strings.h>
+#include <glib.h>
+
+#include "nd-notification.h"
+#include "nd-bubble.h"
+
+#define ND_BUBBLE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), ND_TYPE_BUBBLE, NdBubblePrivate))
+
+#define TIMEOUT_SEC   5
+
+#define WIDTH         400
+#define DEFAULT_X0    0
+#define DEFAULT_Y0    0
+#define DEFAULT_RADIUS 16
+#define IMAGE_SIZE    48
+#define BODY_X_OFFSET (IMAGE_SIZE + 8)
+#define BACKGROUND_ALPHA    0.90
+
+#define MAX_ICON_SIZE IMAGE_SIZE
+
+struct NdBubblePrivate
+{
+        NdNotification *notification;
+
+        GtkWidget      *main_hbox;
+        GtkWidget      *iconbox;
+        GtkWidget      *icon;
+        GtkWidget      *content_hbox;
+        GtkWidget      *summary_label;
+        GtkWidget      *close_button;
+        GtkWidget      *body_label;
+        GtkWidget      *actions_box;
+        GtkWidget      *last_sep;
+
+        int             width;
+        int             height;
+        int             last_width;
+        int             last_height;
+
+        gboolean        have_icon;
+        gboolean        have_body;
+        gboolean        have_actions;
+
+        gboolean        url_clicked_lock;
+
+        gboolean        composited;
+        glong           remaining;
+        guint           timeout_id;
+};
+
+static void     nd_bubble_class_init  (NdBubbleClass *klass);
+static void     nd_bubble_init        (NdBubble      *bubble);
+static void     nd_bubble_finalize    (GObject       *object);
+static void     on_notification_changed (NdNotification *notification,
+                                         NdBubble       *bubble);
+
+G_DEFINE_TYPE (NdBubble, nd_bubble, GTK_TYPE_WINDOW)
+
+static gboolean
+nd_bubble_configure_event (GtkWidget         *widget,
+                           GdkEventConfigure *event)
+{
+        NdBubble *bubble = ND_BUBBLE (widget);
+
+        bubble->priv->width = event->width;
+        bubble->priv->height = event->height;
+
+        gtk_widget_queue_draw (widget);
+
+        return FALSE;
+}
+
+static void
+color_reverse (const GdkColor *a,
+               GdkColor       *b)
+{
+        gdouble red;
+        gdouble green;
+        gdouble blue;
+        gdouble h;
+        gdouble s;
+        gdouble v;
+
+        red = (gdouble) a->red / 65535.0;
+        green = (gdouble) a->green / 65535.0;
+        blue = (gdouble) a->blue / 65535.0;
+
+        gtk_rgb_to_hsv (red, green, blue, &h, &s, &v);
+
+        /* pivot brightness around the center */
+        v = 0.5 + (0.5 - v);
+        if (v > 1.0)
+                v = 1.0;
+        else if (v < 0.0)
+                v = 0.0;
+
+        /* reduce saturation by 50% */
+        s *= 0.5;
+
+        gtk_hsv_to_rgb (h, s, v, &red, &green, &blue);
+
+        b->red = red * 65535.0;
+        b->green = green * 65535.0;
+        b->blue = blue * 65535.0;
+}
+
+static void
+override_style (GtkWidget *widget,
+                GtkStyle  *previous_style)
+{
+        GtkStateType state;
+        GtkStyle    *style;
+        GtkStyle    *old_style;
+        GdkColor     fg;
+        GdkColor     bg;
+
+        /* FIXME! causes a segfault */
+        return;
+
+        gtk_widget_ensure_style (widget);
+        old_style = gtk_widget_get_style (widget);
+        style = gtk_style_copy (old_style);
+        if (previous_style == NULL
+            || (previous_style != NULL
+                && (previous_style->bg[GTK_STATE_NORMAL].red != style->bg[GTK_STATE_NORMAL].red
+                    || previous_style->bg[GTK_STATE_NORMAL].green != style->bg[GTK_STATE_NORMAL].green
+                    || previous_style->bg[GTK_STATE_NORMAL].blue != style->bg[GTK_STATE_NORMAL].blue))) {
+
+                state = (GtkStateType) 0;
+                while (state < (GtkStateType) G_N_ELEMENTS (old_style->bg))  {
+                        color_reverse (&style->bg[state], &bg);
+                        gtk_widget_modify_bg (widget, state, &bg);
+                        state++;
+                }
+
+        }
+
+        if (previous_style == NULL
+            || (previous_style != NULL
+                && (previous_style->fg[GTK_STATE_NORMAL].red != style->fg[GTK_STATE_NORMAL].red
+                    || previous_style->fg[GTK_STATE_NORMAL].green != style->fg[GTK_STATE_NORMAL].green
+                    || previous_style->fg[GTK_STATE_NORMAL].blue != style->fg[GTK_STATE_NORMAL].blue))) {
+
+                state = (GtkStateType) 0;
+                while (state < (GtkStateType) G_N_ELEMENTS (old_style->fg)) {
+                        color_reverse (&style->fg[state], &fg);
+                        gtk_widget_modify_fg (widget, state, &fg);
+                        state++;
+                }
+        }
+
+        g_object_unref (style);
+}
+
+static void
+nd_bubble_style_set (GtkWidget *widget,
+                     GtkStyle  *previous_style)
+{
+        override_style (widget, previous_style);
+        gtk_widget_queue_draw (widget);
+
+        GTK_WIDGET_CLASS (nd_bubble_parent_class)->style_set (widget, previous_style);
+}
+
+static void
+nd_bubble_composited_changed (GtkWidget *widget)
+{
+        NdBubble *bubble = ND_BUBBLE (widget);
+
+        bubble->priv->composited = gdk_screen_is_composited (gtk_widget_get_screen (widget));
+        gtk_widget_queue_draw (widget);
+}
+
+static void
+draw_round_rect (cairo_t *cr,
+                 gdouble  aspect,
+                 gdouble  x,
+                 gdouble  y,
+                 gdouble  corner_radius,
+                 gdouble  width,
+                 gdouble  height)
+{
+        gdouble radius = corner_radius / aspect;
+
+        cairo_move_to (cr, x + radius, y);
+
+        // top-right, left of the corner
+        cairo_line_to (cr,
+                       x + width - radius,
+                       y);
+
+        // top-right, below the corner
+        cairo_arc (cr,
+                   x + width - radius,
+                   y + radius,
+                   radius,
+                   -90.0f * G_PI / 180.0f,
+                   0.0f * G_PI / 180.0f);
+
+        // bottom-right, above the corner
+        cairo_line_to (cr,
+                       x + width,
+                       y + height - radius);
+
+        // bottom-right, left of the corner
+        cairo_arc (cr,
+                   x + width - radius,
+                   y + height - radius,
+                   radius,
+                   0.0f * G_PI / 180.0f,
+                   90.0f * G_PI / 180.0f);
+
+        // bottom-left, right of the corner
+        cairo_line_to (cr,
+                       x + radius,
+                       y + height);
+
+        // bottom-left, above the corner
+        cairo_arc (cr,
+                   x + radius,
+                   y + height - radius,
+                   radius,
+                   90.0f * G_PI / 180.0f,
+                   180.0f * G_PI / 180.0f);
+
+        // top-left, below the corner
+        cairo_line_to (cr,
+                       x,
+                       y + radius);
+
+        // top-left, right of the corner
+        cairo_arc (cr,
+                   x + radius,
+                   y + radius,
+                   radius,
+                   180.0f * G_PI / 180.0f,
+                   270.0f * G_PI / 180.0f);
+}
+
+static void
+paint_bubble (NdBubble *bubble,
+              cairo_t  *cr)
+{
+        GdkColor         color;
+        double           r, g, b;
+        cairo_t         *cr2;
+        cairo_surface_t *surface;
+        cairo_region_t  *region;
+        GtkStyle        *style;
+        GtkAllocation    allocation;
+
+        gtk_widget_get_allocation (GTK_WIDGET (bubble), &allocation);
+        if (bubble->priv->width == 0 || bubble->priv->height == 0) {
+                bubble->priv->width = MAX (allocation.width, 1);
+                bubble->priv->height = MAX (allocation.height, 1);
+        }
+
+        surface = cairo_surface_create_similar (cairo_get_target (cr),
+                                                CAIRO_CONTENT_COLOR_ALPHA,
+                                                bubble->priv->width,
+                                                bubble->priv->height);
+        cr2 = cairo_create (surface);
+
+        /* transparent background */
+        cairo_rectangle (cr2, 0, 0, bubble->priv->width, bubble->priv->height);
+        cairo_set_source_rgba (cr2, 0.0, 0.0, 0.0, 0.0);
+        cairo_fill (cr2);
+
+        draw_round_rect (cr2,
+                         1.0f,
+                         DEFAULT_X0 + 1,
+                         DEFAULT_Y0 + 1,
+                         DEFAULT_RADIUS,
+                         allocation.width - 2,
+                         allocation.height - 2);
+
+        style = gtk_widget_get_style (GTK_WIDGET (bubble));
+        color = style->bg [GTK_STATE_NORMAL];
+        r = (float)color.red / 65535.0;
+        g = (float)color.green / 65535.0;
+        b = (float)color.blue / 65535.0;
+        cairo_set_source_rgba (cr2, r, g, b, BACKGROUND_ALPHA);
+        cairo_fill_preserve (cr2);
+
+        color = style->text_aa [GTK_STATE_NORMAL];
+        r = (float) color.red / 65535.0;
+        g = (float) color.green / 65535.0;
+        b = (float) color.blue / 65535.0;
+        cairo_set_source_rgba (cr2, r, g, b, BACKGROUND_ALPHA / 2);
+        cairo_set_line_width (cr2, 2);
+        cairo_stroke (cr2);
+
+        cairo_destroy (cr2);
+        cairo_set_source_surface (cr, surface, 0, 0);
+        cairo_paint (cr);
+
+        if (bubble->priv->width == bubble->priv->last_width
+            && bubble->priv->height == bubble->priv->last_height) {
+                goto done;
+        }
+
+        /* Don't shape when composited */
+        if (bubble->priv->composited) {
+                gtk_widget_shape_combine_region (GTK_WIDGET (bubble), NULL);
+                goto done;
+        }
+
+        bubble->priv->last_width = bubble->priv->width;
+        bubble->priv->last_height = bubble->priv->height;
+
+        region = gdk_cairo_region_create_from_surface (surface);
+        gtk_widget_shape_combine_region (GTK_WIDGET (bubble), region);
+        cairo_region_destroy (region);
+
+ done:
+        cairo_surface_destroy (surface);
+
+}
+
+static gboolean
+nd_bubble_draw (GtkWidget *widget,
+                cairo_t   *cr)
+{
+        NdBubble *bubble = ND_BUBBLE (widget);
+
+        paint_bubble (bubble, cr);
+
+        GTK_WIDGET_CLASS (nd_bubble_parent_class)->draw (widget, cr);
+
+        return FALSE;
+}
+
+static gboolean
+nd_bubble_button_release_event (GtkWidget      *widget,
+                                GdkEventButton *event)
+{
+        NdBubble *bubble = ND_BUBBLE (widget);
+
+        if (bubble->priv->url_clicked_lock) {
+                bubble->priv->url_clicked_lock = FALSE;
+                return FALSE;
+        }
+
+        nd_notification_action_invoked (bubble->priv->notification, "default");
+        gtk_widget_destroy (GTK_WIDGET (bubble));
+
+        return FALSE;
+}
+
+static gboolean
+timeout_bubble (NdBubble *bubble)
+{
+        bubble->priv->timeout_id = 0;
+
+        /* FIXME: if transient also close it */
+
+        gtk_widget_destroy (GTK_WIDGET (bubble));
+
+        return FALSE;
+}
+
+static void
+add_timeout (NdBubble *bubble)
+{
+        if (bubble->priv->timeout_id != 0) {
+                g_source_remove (bubble->priv->timeout_id);
+        }
+        bubble->priv->timeout_id = g_timeout_add_seconds (TIMEOUT_SEC,
+                                                          (GSourceFunc)timeout_bubble,
+                                                          bubble);
+}
+
+static void
+nd_bubble_realize (GtkWidget *widget)
+{
+        NdBubble *bubble = ND_BUBBLE (widget);
+
+        add_timeout (bubble);
+
+        GTK_WIDGET_CLASS (nd_bubble_parent_class)->realize (widget);
+}
+
+static gboolean
+nd_bubble_enter_notify_event (GtkWidget        *widget,
+                              GdkEventCrossing *event)
+{
+        NdBubble *bubble = ND_BUBBLE (widget);
+        if (bubble->priv->timeout_id != 0) {
+                g_source_remove (bubble->priv->timeout_id);
+        }
+
+        return FALSE;
+}
+
+static gboolean
+nd_bubble_leave_notify_event (GtkWidget        *widget,
+                              GdkEventCrossing *event)
+{
+        NdBubble *bubble = ND_BUBBLE (widget);
+
+        add_timeout (bubble);
+        return FALSE;
+}
+
+static void
+nd_bubble_class_init (NdBubbleClass *klass)
+{
+        GObjectClass   *object_class = G_OBJECT_CLASS (klass);
+        GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+        object_class->finalize = nd_bubble_finalize;
+
+        widget_class->style_set = nd_bubble_style_set;
+        widget_class->draw = nd_bubble_draw;
+        widget_class->configure_event = nd_bubble_configure_event;
+        widget_class->composited_changed = nd_bubble_composited_changed;
+        widget_class->button_release_event = nd_bubble_button_release_event;
+        widget_class->enter_notify_event = nd_bubble_enter_notify_event;
+        widget_class->leave_notify_event = nd_bubble_leave_notify_event;
+        widget_class->realize = nd_bubble_realize;
+
+        g_type_class_add_private (klass, sizeof (NdBubblePrivate));
+}
+
+static gboolean
+on_activate_link (GtkLabel *label,
+                  char     *uri,
+                  NdBubble *bubble)
+{
+        char *escaped_uri;
+        char *cmd = NULL;
+
+        /* Somewhat of a hack.. */
+        bubble->priv->url_clicked_lock = TRUE;
+
+        escaped_uri = g_shell_quote (uri);
+
+        if (g_find_program_in_path ("gvfs-open") != NULL) {
+                cmd = g_strdup_printf ("gvfs-open %s", escaped_uri);
+        } else if (g_find_program_in_path ("xdg-open") != NULL) {
+                cmd = g_strdup_printf ("xdg-open %s", escaped_uri);
+        } else if (g_find_program_in_path ("firefox") != NULL) {
+                cmd = g_strdup_printf ("firefox %s", escaped_uri);
+        } else {
+                g_warning ("Unable to find a browser.");
+        }
+
+        g_free (escaped_uri);
+
+        if (cmd != NULL) {
+                g_spawn_command_line_async (cmd, NULL);
+                g_free (cmd);
+        }
+
+        return TRUE;
+}
+
+static void
+on_close_button_clicked (GtkButton *button,
+                         NdBubble  *bubble)
+{
+        nd_notification_close (bubble->priv->notification, ND_NOTIFICATION_CLOSED_USER);
+        gtk_widget_destroy (GTK_WIDGET (bubble));
+}
+
+static void
+on_style_set (GtkWidget  *widget,
+              GtkStyle   *previous_style,
+              NdBubble  *bubble)
+{
+        g_signal_handlers_block_by_func (G_OBJECT (widget), on_style_set, bubble);
+        override_style (widget, previous_style);
+
+        gtk_widget_queue_draw (widget);
+
+        g_signal_handlers_unblock_by_func (G_OBJECT (widget), on_style_set, bubble);
+}
+
+static void
+nd_bubble_init (NdBubble *bubble)
+{
+        GtkWidget   *main_vbox;
+        GtkWidget   *vbox;
+        GtkWidget   *close_button;
+        GtkWidget   *image;
+        GtkWidget   *alignment;
+        AtkObject   *atkobj;
+        GtkRcStyle  *rcstyle;
+        GdkScreen   *screen;
+        GdkVisual   *visual;
+
+        bubble->priv = ND_BUBBLE_GET_PRIVATE (bubble);
+
+        gtk_widget_add_events (GTK_WIDGET (bubble), GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
+        atk_object_set_role (gtk_widget_get_accessible (GTK_WIDGET (bubble)), ATK_ROLE_ALERT);
+
+        screen = gtk_window_get_screen (GTK_WINDOW (bubble));
+        visual = gdk_screen_get_rgba_visual (screen);
+        if (visual == NULL) {
+                visual = gdk_screen_get_system_visual (screen);
+         }
+
+        gtk_widget_set_visual (GTK_WIDGET (bubble), visual);
+
+        if (gdk_screen_is_composited (screen)) {
+                bubble->priv->composited = TRUE;
+        }
+
+        main_vbox = gtk_vbox_new (FALSE, 0);
+        g_signal_connect (G_OBJECT (main_vbox),
+                          "style-set",
+                          G_CALLBACK (on_style_set),
+                          bubble);
+        gtk_widget_show (main_vbox);
+        gtk_container_add (GTK_CONTAINER (bubble), main_vbox);
+        gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12);
+
+        bubble->priv->main_hbox = gtk_hbox_new (FALSE, 0);
+        gtk_widget_show (bubble->priv->main_hbox);
+        gtk_box_pack_start (GTK_BOX (main_vbox),
+                            bubble->priv->main_hbox,
+                            FALSE, FALSE, 0);
+
+        /* First row (icon, vbox, close) */
+        bubble->priv->iconbox = gtk_alignment_new (0.5, 0, 0, 0);
+        gtk_widget_show (bubble->priv->iconbox);
+        gtk_alignment_set_padding (GTK_ALIGNMENT (bubble->priv->iconbox),
+                                   5, 0, 0, 0);
+        gtk_box_pack_start (GTK_BOX (bubble->priv->main_hbox),
+                            bubble->priv->iconbox,
+                            FALSE, FALSE, 0);
+        gtk_widget_set_size_request (bubble->priv->iconbox, BODY_X_OFFSET, -1);
+
+        bubble->priv->icon = gtk_image_new ();
+        gtk_widget_show (bubble->priv->icon);
+        gtk_container_add (GTK_CONTAINER (bubble->priv->iconbox), bubble->priv->icon);
+
+        vbox = gtk_vbox_new (FALSE, 6);
+        gtk_widget_show (vbox);
+        gtk_box_pack_start (GTK_BOX (bubble->priv->main_hbox), vbox, TRUE, TRUE, 0);
+        gtk_container_set_border_width (GTK_CONTAINER (vbox), 10);
+
+        /* Add the close button */
+        alignment = gtk_alignment_new (0.5, 0, 0, 0);
+        gtk_widget_show (alignment);
+        gtk_box_pack_start (GTK_BOX (bubble->priv->main_hbox), alignment, FALSE, FALSE, 0);
+
+        close_button = gtk_button_new ();
+        g_signal_connect (G_OBJECT (close_button),
+                          "style-set",
+                          G_CALLBACK (on_style_set),
+                          bubble);
+        gtk_widget_show (close_button);
+        bubble->priv->close_button = close_button;
+        gtk_container_add (GTK_CONTAINER (alignment), close_button);
+        gtk_button_set_relief (GTK_BUTTON (close_button), GTK_RELIEF_NONE);
+        gtk_container_set_border_width (GTK_CONTAINER (close_button), 0);
+        g_signal_connect (G_OBJECT (close_button),
+                          "clicked",
+                          G_CALLBACK (on_close_button_clicked),
+                          bubble);
+
+        rcstyle = gtk_rc_style_new ();
+        rcstyle->xthickness = rcstyle->ythickness = 0;
+        gtk_widget_modify_style (close_button, rcstyle);
+        g_object_unref (rcstyle);
+
+        atkobj = gtk_widget_get_accessible (close_button);
+        atk_action_set_description (ATK_ACTION (atkobj), 0,
+                                    "Closes the notification.");
+        atk_object_set_name (atkobj, "");
+        atk_object_set_description (atkobj, "Closes the notification.");
+
+        image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU);
+        gtk_widget_show (image);
+        gtk_container_add (GTK_CONTAINER (close_button), image);
+
+        /* center vbox */
+        bubble->priv->summary_label = gtk_label_new (NULL);
+        g_signal_connect (G_OBJECT (bubble->priv->summary_label),
+                          "style-set",
+                          G_CALLBACK (on_style_set),
+                          bubble);
+        gtk_widget_show (bubble->priv->summary_label);
+        gtk_box_pack_start (GTK_BOX (vbox), bubble->priv->summary_label, TRUE, TRUE, 0);
+        gtk_misc_set_alignment (GTK_MISC (bubble->priv->summary_label), 0, 0);
+        gtk_label_set_line_wrap (GTK_LABEL (bubble->priv->summary_label), TRUE);
+
+        atkobj = gtk_widget_get_accessible (bubble->priv->summary_label);
+        atk_object_set_description (atkobj, "Notification summary text.");
+
+        bubble->priv->content_hbox = gtk_hbox_new (FALSE, 6);
+        gtk_widget_show (bubble->priv->content_hbox);
+        gtk_box_pack_start (GTK_BOX (vbox), bubble->priv->content_hbox, FALSE, FALSE, 0);
+
+
+        vbox = gtk_vbox_new (FALSE, 6);
+        gtk_widget_show (vbox);
+        gtk_box_pack_start (GTK_BOX (bubble->priv->content_hbox), vbox, TRUE, TRUE, 0);
+
+        bubble->priv->body_label = gtk_label_new (NULL);
+        g_signal_connect (G_OBJECT (bubble->priv->body_label),
+                          "style-set",
+                          G_CALLBACK (on_style_set),
+                          bubble);
+        gtk_widget_show (bubble->priv->body_label);
+        gtk_box_pack_start (GTK_BOX (vbox), bubble->priv->body_label, TRUE, TRUE, 0);
+        gtk_misc_set_alignment (GTK_MISC (bubble->priv->body_label), 0, 0);
+        gtk_label_set_line_wrap (GTK_LABEL (bubble->priv->body_label), TRUE);
+        g_signal_connect (bubble->priv->body_label,
+                          "activate-link",
+                          G_CALLBACK (on_activate_link),
+                          bubble);
+
+        atkobj = gtk_widget_get_accessible (bubble->priv->body_label);
+        atk_object_set_description (atkobj, "Notification body text.");
+
+        alignment = gtk_alignment_new (1, 0.5, 0, 0);
+        gtk_widget_show (alignment);
+        gtk_box_pack_start (GTK_BOX (vbox), alignment, FALSE, TRUE, 0);
+
+        bubble->priv->actions_box = gtk_hbox_new (FALSE, 6);
+        gtk_widget_show (bubble->priv->actions_box);
+        gtk_container_add (GTK_CONTAINER (alignment), bubble->priv->actions_box);
+}
+
+static void
+nd_bubble_finalize (GObject *object)
+{
+        NdBubble *bubble;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (ND_IS_BUBBLE (object));
+
+        bubble = ND_BUBBLE (object);
+
+        g_return_if_fail (bubble->priv != NULL);
+
+        if (bubble->priv->timeout_id != 0) {
+                g_source_remove (bubble->priv->timeout_id);
+        }
+
+        g_signal_handlers_disconnect_by_func (bubble->priv->notification, G_CALLBACK (on_notification_changed), bubble);
+
+        g_object_unref (bubble->priv->notification);
+
+        G_OBJECT_CLASS (nd_bubble_parent_class)->finalize (object);
+}
+
+static void
+update_content_hbox_visibility (NdBubble *bubble)
+{
+        if (bubble->priv->have_icon
+            || bubble->priv->have_body
+            || bubble->priv->have_actions) {
+                gtk_widget_show (bubble->priv->content_hbox);
+        } else {
+                gtk_widget_hide (bubble->priv->content_hbox);
+        }
+}
+
+static void
+set_notification_text (NdBubble   *bubble,
+                       const char *summary,
+                       const char *body)
+{
+        char          *str;
+        char          *quoted;
+        GtkRequisition req;
+        int            summary_width;
+
+        quoted = g_markup_escape_text (summary, -1);
+        str = g_strdup_printf ("<b><big>%s</big></b>", quoted);
+        g_free (quoted);
+
+        gtk_label_set_markup (GTK_LABEL (bubble->priv->summary_label), str);
+
+        g_free (str);
+        gtk_widget_show_all (GTK_WIDGET (bubble));
+        gtk_label_set_markup (GTK_LABEL (bubble->priv->body_label), body);
+
+        if (body == NULL || *body == '\0') {
+                bubble->priv->have_body = FALSE;
+                gtk_widget_hide (bubble->priv->body_label);
+        } else {
+                bubble->priv->have_body = TRUE;
+                gtk_widget_show (bubble->priv->body_label);
+        }
+        update_content_hbox_visibility (bubble);
+
+        gtk_widget_size_request (bubble->priv->close_button, &req);
+        /* -1: main_vbox border width
+           -10: vbox border width
+           -6: spacing for hbox */
+        summary_width = WIDTH - (1*2) - (10*2) - BODY_X_OFFSET - req.width - (6*2);
+
+        if (body != NULL && *body != '\0') {
+                gtk_widget_set_size_request (bubble->priv->body_label,
+                                             summary_width,
+                                             -1);
+        }
+
+        gtk_widget_set_size_request (bubble->priv->summary_label,
+                                     summary_width,
+                                     -1);
+}
+
+static GdkPixbuf *
+scale_pixbuf (GdkPixbuf *pixbuf,
+              int        max_width,
+              int        max_height,
+              gboolean   no_stretch_hint)
+{
+        int        pw;
+        int        ph;
+        float      scale_factor_x = 1.0;
+        float      scale_factor_y = 1.0;
+        float      scale_factor = 1.0;
+
+        pw = gdk_pixbuf_get_width (pixbuf);
+        ph = gdk_pixbuf_get_height (pixbuf);
+
+        /* Determine which dimension requires the smallest scale. */
+        scale_factor_x = (float) max_width / (float) pw;
+        scale_factor_y = (float) max_height / (float) ph;
+
+        if (scale_factor_x > scale_factor_y) {
+                scale_factor = scale_factor_y;
+        } else {
+                scale_factor = scale_factor_x;
+        }
+
+        /* always scale down, allow to disable scaling up */
+        if (scale_factor < 1.0 || !no_stretch_hint) {
+                int scale_x;
+                int scale_y;
+
+                scale_x = (int) (pw * scale_factor);
+                scale_y = (int) (ph * scale_factor);
+                return gdk_pixbuf_scale_simple (pixbuf,
+                                                scale_x,
+                                                scale_y,
+                                                GDK_INTERP_BILINEAR);
+        } else {
+                return g_object_ref (pixbuf);
+        }
+}
+
+static void
+set_notification_icon (NdBubble  *bubble,
+                       GdkPixbuf *pixbuf)
+{
+        GdkPixbuf  *scaled;
+
+        scaled = NULL;
+        if (pixbuf != NULL) {
+                scaled = scale_pixbuf (pixbuf,
+                                       MAX_ICON_SIZE,
+                                       MAX_ICON_SIZE,
+                                       TRUE);
+        }
+
+        gtk_image_set_from_pixbuf (GTK_IMAGE (bubble->priv->icon), scaled);
+
+        if (scaled != NULL) {
+                int pixbuf_width = gdk_pixbuf_get_width (scaled);
+
+                gtk_widget_show (bubble->priv->icon);
+                gtk_widget_set_size_request (bubble->priv->iconbox,
+                                             MAX (BODY_X_OFFSET, pixbuf_width), -1);
+                g_object_unref (scaled);
+                bubble->priv->have_icon = TRUE;
+        } else {
+                gtk_widget_hide (bubble->priv->icon);
+                gtk_widget_set_size_request (bubble->priv->iconbox,
+                                             BODY_X_OFFSET,
+                                             -1);
+                bubble->priv->have_icon = FALSE;
+        }
+
+        update_content_hbox_visibility (bubble);
+}
+
+static void
+on_action_clicked (GtkButton      *button,
+                   GdkEventButton *event,
+                   NdBubble       *bubble)
+{
+        const char *key = g_object_get_data (G_OBJECT (button), "_action_key");
+
+        nd_notification_action_invoked (bubble->priv->notification,
+                                        key);
+        gtk_widget_destroy (GTK_WIDGET (bubble));
+}
+
+static void
+add_notification_action (NdBubble       *bubble,
+                         const char     *text,
+                         const char     *key)
+{
+        GtkWidget *label;
+        GtkWidget *button;
+        GtkWidget *hbox;
+        GdkPixbuf *pixbuf;
+        char      *buf;
+
+        if (!gtk_widget_get_visible (bubble->priv->actions_box)) {
+                GtkWidget *alignment;
+
+                gtk_widget_show (bubble->priv->actions_box);
+                update_content_hbox_visibility (bubble);
+
+                alignment = gtk_alignment_new (1, 0.5, 0, 0);
+                gtk_widget_show (alignment);
+                gtk_box_pack_end (GTK_BOX (bubble->priv->actions_box),
+                                  alignment,
+                                  FALSE, TRUE, 0);
+        }
+
+        button = gtk_button_new ();
+        g_signal_connect (G_OBJECT (button),
+                          "style-set",
+                          G_CALLBACK (on_style_set),
+                          bubble);
+        gtk_widget_show (button);
+        gtk_box_pack_start (GTK_BOX (bubble->priv->actions_box), button, FALSE, FALSE, 0);
+        gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
+        gtk_container_set_border_width (GTK_CONTAINER (button), 0);
+
+        hbox = gtk_hbox_new (FALSE, 6);
+        gtk_widget_show (hbox);
+        gtk_container_add (GTK_CONTAINER (button), hbox);
+
+        /* Try to be smart and find a suitable icon. */
+        buf = g_strdup_printf ("stock_%s", key);
+        pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_for_screen (gtk_widget_get_screen (GTK_WIDGET (bubble))),
+                                           buf,
+                                           16,
+                                           GTK_ICON_LOOKUP_USE_BUILTIN,
+                                           NULL);
+        g_free(buf);
+
+        if (pixbuf != NULL) {
+                GtkWidget *image = gtk_image_new_from_pixbuf (pixbuf);
+                g_signal_connect (G_OBJECT (image),
+                                  "style-set",
+                                  G_CALLBACK (on_style_set),
+                                  bubble);
+                gtk_widget_show (image);
+                gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
+                gtk_misc_set_alignment (GTK_MISC (image), 0.5, 0.5);
+        }
+
+        label = gtk_label_new (NULL);
+        g_signal_connect (G_OBJECT (label),
+                          "style-set",
+                          G_CALLBACK (on_style_set),
+                          bubble);
+        gtk_widget_show (label);
+        gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
+        gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);
+        buf = g_strdup_printf ("<small>%s</small>", text);
+        gtk_label_set_markup (GTK_LABEL (label), buf);
+        g_free (buf);
+
+        g_object_set_data_full (G_OBJECT (button),
+                                "_action_key", g_strdup (key), g_free);
+        g_signal_connect (G_OBJECT (button),
+                          "button-release-event",
+                          G_CALLBACK (on_action_clicked),
+                          bubble);
+}
+
+static void
+clear_actions (NdBubble *bubble)
+{
+        gtk_widget_hide (bubble->priv->actions_box);
+        gtk_container_foreach (GTK_CONTAINER (bubble->priv->actions_box),
+                               (GtkCallback)gtk_widget_destroy,
+                               NULL);
+        bubble->priv->have_actions = FALSE;
+}
+
+static void
+add_actions (NdBubble *bubble)
+{
+        char **actions;
+        int    i;
+
+        actions = nd_notification_get_actions (bubble->priv->notification);
+
+        for (i = 0; actions[i] != NULL; i += 2) {
+                char *l = actions[i + 1];
+
+                if (l == NULL) {
+                        g_warning ("Label not found for action %s. "
+                                   "The protocol specifies that a label must "
+                                   "follow an action in the actions array",
+                                   actions[i]);
+
+                        break;
+                }
+
+                if (strcasecmp (actions[i], "default") != 0) {
+                        add_notification_action (bubble,
+                                                 l,
+                                                 actions[i]);
+                        bubble->priv->have_actions = TRUE;
+                }
+        }
+}
+
+static void
+update_image (NdBubble *bubble)
+{
+        GdkPixbuf *pixbuf;
+
+        pixbuf = nd_notification_load_image (bubble->priv->notification, IMAGE_SIZE);
+        if (pixbuf != NULL) {
+                set_notification_icon (bubble, pixbuf);
+                g_object_unref (G_OBJECT (pixbuf));
+        }
+}
+
+static void
+update_bubble (NdBubble *bubble)
+{
+        set_notification_text (bubble,
+                               nd_notification_get_summary (bubble->priv->notification),
+                               nd_notification_get_body (bubble->priv->notification));
+        clear_actions (bubble);
+        add_actions (bubble);
+        update_image (bubble);
+        update_content_hbox_visibility (bubble);
+}
+
+static void
+on_notification_changed (NdNotification *notification,
+                         NdBubble       *bubble)
+{
+        update_bubble (bubble);
+}
+
+NdBubble *
+nd_bubble_new_for_notification (NdNotification *notification)
+{
+        NdBubble *bubble;
+
+        bubble = g_object_new (ND_TYPE_BUBBLE,
+                               "app-paintable", TRUE,
+                               "type", GTK_WINDOW_POPUP,
+                               "title", "Notification",
+                               "resizable", FALSE,
+                               "type-hint", GDK_WINDOW_TYPE_HINT_NOTIFICATION,
+                               NULL);
+        bubble->priv->notification = g_object_ref (notification);
+        g_signal_connect (notification, "changed", G_CALLBACK (on_notification_changed), bubble);
+        update_bubble (bubble);
+
+        return bubble;
+}
diff --git a/src/nd-bubble.h b/src/nd-bubble.h
new file mode 100644
index 0000000..617c653
--- /dev/null
+++ b/src/nd-bubble.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+
+#ifndef __ND_BUBBLE_H
+#define __ND_BUBBLE_H
+
+#include <gtk/gtk.h>
+#include "nd-notification.h"
+
+G_BEGIN_DECLS
+
+#define ND_TYPE_BUBBLE         (nd_bubble_get_type ())
+#define ND_BUBBLE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), ND_TYPE_BUBBLE, NdBubble))
+#define ND_BUBBLE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), ND_TYPE_BUBBLE, NdBubbleClass))
+#define ND_IS_BUBBLE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), ND_TYPE_BUBBLE))
+#define ND_IS_BUBBLE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), ND_TYPE_BUBBLE))
+#define ND_BUBBLE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), ND_TYPE_BUBBLE, NdBubbleClass))
+
+typedef struct NdBubblePrivate NdBubblePrivate;
+
+typedef struct
+{
+        GtkWindow        parent;
+        NdBubblePrivate *priv;
+} NdBubble;
+
+typedef struct
+{
+        GtkWindowClass   parent_class;
+
+        void          (* changed) (NdBubble      *bubble);
+} NdBubbleClass;
+
+GType               nd_bubble_get_type                      (void);
+
+NdBubble *          nd_bubble_new_for_notification          (NdNotification *notification);
+
+
+G_END_DECLS
+
+#endif /* __ND_BUBBLE_H */
diff --git a/src/nd-notification.c b/src/nd-notification.c
new file mode 100644
index 0000000..255e64b
--- /dev/null
+++ b/src/nd-notification.c
@@ -0,0 +1,600 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <strings.h>
+#include <gtk/gtk.h>
+#include <dbus/dbus-glib.h>
+
+#include "nd-notification.h"
+
+#define ND_NOTIFICATION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ND_TYPE_NOTIFICATION, NdNotificationClass))
+#define ND_IS_NOTIFICATION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ND_TYPE_NOTIFICATION))
+#define ND_NOTIFICATION_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS ((object), ND_TYPE_NOTIFICATION, NdNotificationClass))
+
+enum {
+        CHANGED,
+        CLOSED,
+        ACTION_INVOKED,
+        LAST_SIGNAL
+};
+
+struct _NdNotification {
+        GObject       parent;
+
+        gboolean      is_closed;
+
+        GTimeVal      update_time;
+
+        char         *sender;
+        guint32       id;
+        char         *app_name;
+        char         *icon;
+        char         *summary;
+        char         *body;
+        char        **actions;
+        GHashTable   *hints;
+        int           timeout;
+};
+
+static void nd_notification_finalize     (GObject      *object);
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (NdNotification, nd_notification, G_TYPE_OBJECT)
+
+static guint32 notification_serial = 1;
+
+static guint32
+get_next_notification_serial (void)
+{
+        guint32 serial;
+
+        serial = notification_serial++;
+
+        if ((gint32)notification_serial < 0) {
+                notification_serial = 1;
+        }
+
+        return serial;
+}
+
+static void
+nd_notification_class_init (NdNotificationClass *class)
+{
+        GObjectClass *gobject_class;
+
+        gobject_class = G_OBJECT_CLASS (class);
+
+        gobject_class->finalize = nd_notification_finalize;
+
+        signals [CHANGED] =
+                g_signal_new ("changed",
+                              G_TYPE_FROM_CLASS (class),
+                              G_SIGNAL_RUN_LAST,
+                              0,
+                              NULL, NULL,
+                              g_cclosure_marshal_VOID__VOID,
+                              G_TYPE_NONE, 0);
+        signals [CLOSED] =
+                g_signal_new ("closed",
+                              G_TYPE_FROM_CLASS (class),
+                              G_SIGNAL_RUN_LAST,
+                              0,
+                              NULL, NULL,
+                              g_cclosure_marshal_VOID__INT,
+                              G_TYPE_NONE, 1, G_TYPE_INT);
+        signals [ACTION_INVOKED] =
+                g_signal_new ("action-invoked",
+                              G_TYPE_FROM_CLASS (class),
+                              G_SIGNAL_RUN_LAST,
+                              0,
+                              NULL, NULL,
+                              g_cclosure_marshal_VOID__STRING,
+                              G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+static void
+nd_notification_init (NdNotification *notification)
+{
+        notification->id = get_next_notification_serial ();
+
+        notification->app_name = NULL;
+        notification->icon = NULL;
+        notification->summary = NULL;
+        notification->body = NULL;
+        notification->actions = NULL;
+        notification->hints = g_hash_table_new (g_str_hash, g_str_equal);
+}
+
+static void
+nd_notification_finalize (GObject *object)
+{
+        NdNotification *notification;
+
+        notification = ND_NOTIFICATION (object);
+
+        g_free (notification->sender);
+        g_free (notification->app_name);
+        g_free (notification->icon);
+        g_free (notification->summary);
+        g_free (notification->body);
+        g_strfreev (notification->actions);
+
+        if (notification->hints != NULL) {
+                g_hash_table_destroy (notification->hints);
+        }
+
+        if (G_OBJECT_CLASS (nd_notification_parent_class)->finalize)
+                (*G_OBJECT_CLASS (nd_notification_parent_class)->finalize) (object);
+}
+
+gboolean
+nd_notification_update (NdNotification *notification,
+                        const char     *app_name,
+                        const char     *icon,
+                        const char     *summary,
+                        const char     *body,
+                        const char    **actions,
+                        GHashTable     *hints,
+                        int             timeout)
+{
+        GHashTableIter iter;
+        gpointer       key, value;
+
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), FALSE);
+
+        g_free (notification->app_name);
+        notification->app_name = g_strdup (app_name);
+
+        g_free (notification->icon);
+        notification->icon = g_strdup (icon);
+
+        g_free (notification->summary);
+        notification->summary = g_strdup (summary);
+
+        g_free (notification->body);
+        notification->body = g_strdup (body);
+
+        g_strfreev (notification->actions);
+        notification->actions = g_strdupv ((char **)actions);
+
+        g_hash_table_remove_all (notification->hints);
+
+        g_hash_table_iter_init (&iter, hints);
+        while (g_hash_table_iter_next (&iter, &key, &value)) {
+                g_hash_table_insert (notification->hints, key, value);
+        }
+
+        g_signal_emit (notification, signals[CHANGED], 0);
+
+        g_get_current_time (&notification->update_time);
+
+        return TRUE;
+}
+
+void
+nd_notification_get_update_time (NdNotification *notification,
+                                 GTimeVal       *tvp)
+{
+        g_return_if_fail (ND_IS_NOTIFICATION (notification));
+
+        if (tvp == NULL) {
+                return;
+        }
+
+        tvp->tv_usec = notification->update_time.tv_usec;
+        tvp->tv_sec = notification->update_time.tv_sec;
+}
+
+gboolean
+nd_notification_get_is_closed (NdNotification *notification)
+{
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), FALSE);
+
+        return notification->is_closed;
+}
+
+guint32
+nd_notification_get_id (NdNotification *notification)
+{
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), -1);
+
+        return notification->id;
+}
+
+GHashTable *
+nd_notification_get_hints (NdNotification *notification)
+{
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), NULL);
+
+        return notification->hints;
+}
+
+char **
+nd_notification_get_actions (NdNotification *notification)
+{
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), NULL);
+
+        return notification->actions;
+}
+
+const char *
+nd_notification_get_sender (NdNotification *notification)
+{
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), NULL);
+
+        return notification->sender;
+}
+
+const char *
+nd_notification_get_summary (NdNotification *notification)
+{
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), NULL);
+
+        return notification->summary;
+}
+
+const char *
+nd_notification_get_body (NdNotification *notification)
+{
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), NULL);
+
+        return notification->body;
+}
+
+const char *
+nd_notification_get_icon (NdNotification *notification)
+{
+        g_return_val_if_fail (ND_IS_NOTIFICATION (notification), NULL);
+
+        return notification->icon;
+}
+
+
+static GdkPixbuf *
+scale_pixbuf (GdkPixbuf *pixbuf,
+              int        max_width,
+              int        max_height,
+              gboolean   no_stretch_hint)
+{
+        int        pw;
+        int        ph;
+        float      scale_factor_x = 1.0;
+        float      scale_factor_y = 1.0;
+        float      scale_factor = 1.0;
+
+        pw = gdk_pixbuf_get_width (pixbuf);
+        ph = gdk_pixbuf_get_height (pixbuf);
+
+        /* Determine which dimension requires the smallest scale. */
+        scale_factor_x = (float) max_width / (float) pw;
+        scale_factor_y = (float) max_height / (float) ph;
+
+        if (scale_factor_x > scale_factor_y) {
+                scale_factor = scale_factor_y;
+        } else {
+                scale_factor = scale_factor_x;
+        }
+
+        /* always scale down, allow to disable scaling up */
+        if (scale_factor < 1.0 || !no_stretch_hint) {
+                int scale_x;
+                int scale_y;
+
+                scale_x = (int) (pw * scale_factor);
+                scale_y = (int) (ph * scale_factor);
+                return gdk_pixbuf_scale_simple (pixbuf,
+                                                scale_x,
+                                                scale_y,
+                                                GDK_INTERP_BILINEAR);
+        } else {
+                return g_object_ref (pixbuf);
+        }
+}
+
+static GdkPixbuf *
+_notify_daemon_pixbuf_from_data_hint (GValue *icon_data,
+                                      int     size)
+{
+        const guchar   *data = NULL;
+        gboolean        has_alpha;
+        int             bits_per_sample;
+        int             width;
+        int             height;
+        int             rowstride;
+        int             n_channels;
+        gsize           expected_len;
+        GdkPixbuf      *pixbuf;
+        GValueArray    *image_struct;
+        GValue         *value;
+        GArray         *tmp_array;
+        GType           struct_type;
+
+        struct_type = dbus_g_type_get_struct ("GValueArray",
+                                              G_TYPE_INT,
+                                              G_TYPE_INT,
+                                              G_TYPE_INT,
+                                              G_TYPE_BOOLEAN,
+                                              G_TYPE_INT,
+                                              G_TYPE_INT,
+                                              dbus_g_type_get_collection ("GArray", G_TYPE_UCHAR),
+                                              G_TYPE_INVALID);
+
+        if (!G_VALUE_HOLDS (icon_data, struct_type)) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected a "
+                           "GValue of type GValueArray");
+                return NULL;
+        }
+
+        image_struct = (GValueArray *) g_value_get_boxed (icon_data);
+        value = g_value_array_get_nth (image_struct, 0);
+
+        if (value == NULL) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected position "
+                           "0 of the GValueArray to exist");
+                return NULL;
+        }
+
+        if (!G_VALUE_HOLDS (value, G_TYPE_INT)) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 0 of the GValueArray to be of type int");
+                return NULL;
+        }
+
+        width = g_value_get_int (value);
+        value = g_value_array_get_nth (image_struct, 1);
+
+        if (value == NULL) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 1 of the GValueArray to exist");
+                return NULL;
+        }
+
+        if (!G_VALUE_HOLDS (value, G_TYPE_INT)) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 1 of the GValueArray to be of type int");
+                return NULL;
+        }
+
+        height = g_value_get_int (value);
+        value = g_value_array_get_nth (image_struct, 2);
+
+        if (value == NULL) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 2 of the GValueArray to exist");
+                return NULL;
+        }
+
+        if (!G_VALUE_HOLDS (value, G_TYPE_INT)) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 2 of the GValueArray to be of type int");
+                return NULL;
+        }
+
+        rowstride = g_value_get_int (value);
+        value = g_value_array_get_nth (image_struct, 3);
+
+        if (value == NULL) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 3 of the GValueArray to exist");
+                return NULL;
+        }
+
+        if (!G_VALUE_HOLDS (value, G_TYPE_BOOLEAN)) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 3 of the GValueArray to be of type gboolean");
+                return NULL;
+        }
+
+        has_alpha = g_value_get_boolean (value);
+        value = g_value_array_get_nth (image_struct, 4);
+
+        if (value == NULL) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 4 of the GValueArray to exist");
+                return NULL;
+        }
+
+        if (!G_VALUE_HOLDS (value, G_TYPE_INT)) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 4 of the GValueArray to be of type int");
+                return NULL;
+        }
+
+        bits_per_sample = g_value_get_int (value);
+        value = g_value_array_get_nth (image_struct, 5);
+
+        if (value == NULL) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 5 of the GValueArray to exist");
+                return NULL;
+        }
+
+        if (!G_VALUE_HOLDS (value, G_TYPE_INT)) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 5 of the GValueArray to be of type int");
+                return NULL;
+        }
+
+        n_channels = g_value_get_int (value);
+        value = g_value_array_get_nth (image_struct, 6);
+
+        if (value == NULL) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 6 of the GValueArray to exist");
+                return NULL;
+        }
+
+        if (!G_VALUE_HOLDS (value,
+                            dbus_g_type_get_collection ("GArray",
+                                                        G_TYPE_UCHAR))) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected "
+                           "position 6 of the GValueArray to be of type GArray");
+                return NULL;
+        }
+
+        tmp_array = (GArray *) g_value_get_boxed (value);
+        expected_len = (height - 1) * rowstride + width
+                * ((n_channels * bits_per_sample + 7) / 8);
+
+        if (expected_len != tmp_array->len) {
+                g_warning ("_notify_daemon_pixbuf_from_data_hint expected image "
+                           "data to be of length %" G_GSIZE_FORMAT
+                           " but got a " "length of %u", expected_len,
+                           tmp_array->len);
+                return NULL;
+        }
+
+        data = (guchar *) g_memdup (tmp_array->data, tmp_array->len);
+        pixbuf = gdk_pixbuf_new_from_data (data,
+                                           GDK_COLORSPACE_RGB,
+                                           has_alpha,
+                                           bits_per_sample,
+                                           width,
+                                           height,
+                                           rowstride,
+                                           (GdkPixbufDestroyNotify) g_free,
+                                           NULL);
+        if (pixbuf != NULL && size > 0) {
+                GdkPixbuf *scaled;
+                scaled = scale_pixbuf (pixbuf, size, size, TRUE);
+                g_object_unref (pixbuf);
+                pixbuf = scaled;
+        }
+
+        return pixbuf;
+}
+
+static GdkPixbuf *
+_notify_daemon_pixbuf_from_path (const char *path,
+                                 int         size)
+{
+        GdkPixbuf *pixbuf = NULL;
+
+        if (!strncmp (path, "file://", 7) || *path == '/') {
+                if (!strncmp (path, "file://", 7)) {
+                        path += 7;
+                }
+
+                /* Load file */
+                pixbuf = gdk_pixbuf_new_from_file_at_size (path, size, size, NULL);
+        } else {
+                /* Load icon theme icon */
+                GtkIconTheme *theme;
+                GtkIconInfo  *icon_info;
+
+                theme = gtk_icon_theme_get_default ();
+                icon_info = gtk_icon_theme_lookup_icon (theme,
+                                                        path,
+                                                        size,
+                                                        GTK_ICON_LOOKUP_USE_BUILTIN);
+
+                if (icon_info != NULL) {
+                        gint icon_size;
+
+                        icon_size = MIN (size,
+                                         gtk_icon_info_get_base_size (icon_info));
+
+                        if (icon_size == 0)
+                                icon_size = size;
+
+                        pixbuf = gtk_icon_theme_load_icon (theme,
+                                                           path,
+                                                           icon_size,
+                                                           GTK_ICON_LOOKUP_USE_BUILTIN,
+                                                           NULL);
+
+                        gtk_icon_info_free (icon_info);
+                }
+
+                if (pixbuf == NULL) {
+                        /* Well... maybe this is a file afterall. */
+                        pixbuf = gdk_pixbuf_new_from_file_at_size (path, size, size, NULL);
+                }
+        }
+
+        return pixbuf;
+}
+
+GdkPixbuf *
+nd_notification_load_image (NdNotification *notification,
+                            int             size)
+{
+        GValue    *data;
+        GdkPixbuf *pixbuf;
+
+        pixbuf = NULL;
+
+        if ((data = (GValue *) g_hash_table_lookup (notification->hints, "image_data"))) {
+                pixbuf = _notify_daemon_pixbuf_from_data_hint (data, size);
+        } else if ((data = (GValue *) g_hash_table_lookup (notification->hints, "image_path"))) {
+                if (G_VALUE_HOLDS_STRING (data)) {
+                        const char *path = g_value_get_string (data);
+                        pixbuf = _notify_daemon_pixbuf_from_path (path, size);
+                } else {
+                        g_warning ("notify_daemon_notify_handler expected "
+                                   "image_path hint to be of type string");
+                }
+        } else if (*notification->icon != '\0') {
+                pixbuf = _notify_daemon_pixbuf_from_path (notification->icon, size);
+        } else if ((data = (GValue *) g_hash_table_lookup (notification->hints, "icon_data"))) {
+                g_warning("\"icon_data\" hint is deprecated, please use \"image_data\" instead");
+                pixbuf = _notify_daemon_pixbuf_from_data_hint (data, size);
+        }
+
+        return pixbuf;
+}
+
+void
+nd_notification_close (NdNotification            *notification,
+                       NdNotificationClosedReason reason)
+{
+        g_return_if_fail (ND_IS_NOTIFICATION (notification));
+
+        g_object_ref (notification);
+        g_signal_emit (notification, signals[CLOSED], 0, reason);
+        g_object_unref (notification);
+
+        notification->is_closed = TRUE;
+}
+
+void
+nd_notification_action_invoked (NdNotification  *notification,
+                                const char      *action)
+{
+        g_return_if_fail (ND_IS_NOTIFICATION (notification));
+
+        g_object_ref (notification);
+        g_signal_emit (notification, signals[ACTION_INVOKED], 0, action);
+        g_object_unref (notification);
+}
+
+NdNotification *
+nd_notification_new (const char *sender)
+{
+        NdNotification *notification;
+
+        notification = (NdNotification *) g_object_new (ND_TYPE_NOTIFICATION, NULL);
+        notification->sender = g_strdup (sender);
+
+        return notification;
+}
diff --git a/src/nd-notification.h b/src/nd-notification.h
new file mode 100644
index 0000000..b2c4438
--- /dev/null
+++ b/src/nd-notification.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+#ifndef __ND_NOTIFICATION__
+#define __ND_NOTIFICATION__ 1
+
+#include <glib-object.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+G_BEGIN_DECLS
+
+#define ND_TYPE_NOTIFICATION (nd_notification_get_type ())
+#define ND_NOTIFICATION(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), ND_TYPE_NOTIFICATION, NdNotification))
+#define ND_IS_NOTIFICATION(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), ND_TYPE_NOTIFICATION))
+
+typedef struct _NdNotification NdNotification;
+
+typedef struct _NdNotificationClass
+{
+        GObjectClass parent_class;
+} NdNotificationClass;
+
+typedef enum
+{
+        ND_NOTIFICATION_CLOSED_EXPIRED = 1,
+        ND_NOTIFICATION_CLOSED_USER = 2,
+        ND_NOTIFICATION_CLOSED_API = 3,
+        ND_NOTIFICATION_CLOSED_RESERVED = 4
+} NdNotificationClosedReason;
+
+GType                 nd_notification_get_type            (void) G_GNUC_CONST;
+
+NdNotification *      nd_notification_new                 (const char     *sender);
+gboolean              nd_notification_update              (NdNotification *notification,
+                                                           const char     *app_name,
+                                                           const char     *icon,
+                                                           const char     *summary,
+                                                           const char     *body,
+                                                           const char    **actions,
+                                                           GHashTable     *hints,
+                                                           int             timeout);
+
+gboolean              nd_notification_get_is_closed       (NdNotification *notification);
+void                  nd_notification_get_update_time     (NdNotification *notification,
+                                                           GTimeVal       *timeval);
+
+guint                 nd_notification_get_id              (NdNotification *notification);
+int                   nd_notification_get_timeout         (NdNotification *notification);
+const char *          nd_notification_get_sender          (NdNotification *notification);
+const char *          nd_notification_get_app_name        (NdNotification *notification);
+const char *          nd_notification_get_icon            (NdNotification *notification);
+const char *          nd_notification_get_summary         (NdNotification *notification);
+const char *          nd_notification_get_body            (NdNotification *notification);
+char **               nd_notification_get_actions         (NdNotification *notification);
+GHashTable *          nd_notification_get_hints           (NdNotification *notification);
+
+GdkPixbuf *           nd_notification_load_image          (NdNotification *notification,
+                                                           int             size);
+
+void                  nd_notification_close               (NdNotification *notification,
+                                                           NdNotificationClosedReason reason);
+void                  nd_notification_action_invoked      (NdNotification *notification,
+                                                           const char     *action);
+void                  nd_notification_url_clicked         (NdNotification *notification,
+                                                           const char     *url);
+
+G_END_DECLS
+
+#endif
diff --git a/src/nd-queue.c b/src/nd-queue.c
new file mode 100644
index 0000000..2f2979b
--- /dev/null
+++ b/src/nd-queue.c
@@ -0,0 +1,1088 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include <X11/Xproto.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+#include <gdk/gdkx.h>
+
+#include "nd-queue.h"
+
+#include "nd-notification.h"
+#include "nd-stack.h"
+
+#define ND_QUEUE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), ND_TYPE_QUEUE, NdQueuePrivate))
+
+#define IMAGE_SIZE    48
+#define BODY_X_OFFSET (IMAGE_SIZE + 8)
+#define WIDTH         400
+
+typedef struct
+{
+        NdStack   **stacks;
+        int         n_stacks;
+        Atom        workarea_atom;
+} NotifyScreen;
+
+struct NdQueuePrivate
+{
+        GHashTable    *notifications;
+        GHashTable    *bubbles;
+        GQueue        *queue;
+
+        GtkStatusIcon *status_icon;
+        GtkWidget     *dock;
+        GtkWidget     *dock_scrolled_window;
+
+        NotifyScreen **screens;
+        int            n_screens;
+
+        guint          update_id;
+};
+
+enum {
+        CHANGED,
+        LAST_SIGNAL
+};
+
+static guint signals [LAST_SIGNAL] = { 0, };
+
+static void     nd_queue_class_init  (NdQueueClass *klass);
+static void     nd_queue_init        (NdQueue      *queue);
+static void     nd_queue_finalize    (GObject         *object);
+static void     queue_update         (NdQueue *queue);
+
+static gpointer queue_object = NULL;
+
+G_DEFINE_TYPE (NdQueue, nd_queue, G_TYPE_OBJECT)
+
+static void
+create_stack_for_monitor (NdQueue    *queue,
+                          GdkScreen  *screen,
+                          int         monitor_num)
+{
+        NotifyScreen *nscreen;
+        int           screen_num;
+
+        screen_num = gdk_screen_get_number (screen);
+        nscreen = queue->priv->screens[screen_num];
+
+        nscreen->stacks[monitor_num] = nd_stack_new (screen,
+                                                     monitor_num);
+}
+
+static void
+on_screen_monitors_changed (GdkScreen *screen,
+                            NdQueue   *queue)
+{
+        NotifyScreen *nscreen;
+        int           screen_num;
+        int           n_monitors;
+        int           i;
+
+        screen_num = gdk_screen_get_number (screen);
+        nscreen = queue->priv->screens[screen_num];
+
+        n_monitors = gdk_screen_get_n_monitors (screen);
+
+        if (n_monitors > nscreen->n_stacks) {
+                /* grow */
+                nscreen->stacks = g_renew (NdStack *,
+                                           nscreen->stacks,
+                                           n_monitors);
+
+                /* add more stacks */
+                for (i = nscreen->n_stacks; i < n_monitors; i++) {
+                        create_stack_for_monitor (queue, screen, i);
+                }
+
+                nscreen->n_stacks = n_monitors;
+        } else if (n_monitors < nscreen->n_stacks) {
+                NdStack *last_stack;
+
+                last_stack = nscreen->stacks[n_monitors - 1];
+
+                /* transfer items before removing stacks */
+                for (i = n_monitors; i < nscreen->n_stacks; i++) {
+                        NdStack     *stack;
+                        GList       *bubbles;
+                        GList       *l;
+
+                        stack = nscreen->stacks[i];
+                        bubbles = g_list_copy (nd_stack_get_bubbles (stack));
+                        for (l = bubbles; l != NULL; l = l->next) {
+                                /* skip removing the bubble from the
+                                   old stack since it will try to
+                                   unrealize the window.  And the
+                                   stack is going away anyhow. */
+                                nd_stack_add_bubble (last_stack, l->data, TRUE);
+                        }
+                        g_list_free (bubbles);
+                        g_object_unref (stack);
+                        nscreen->stacks[i] = NULL;
+                }
+
+                /* remove the extra stacks */
+                nscreen->stacks = g_renew (NdStack *,
+                                           nscreen->stacks,
+                                           n_monitors);
+                nscreen->n_stacks = n_monitors;
+        }
+}
+
+static void
+create_stacks_for_screen (NdQueue   *queue,
+                          GdkScreen *screen)
+{
+        NotifyScreen *nscreen;
+        int           screen_num;
+        int           i;
+
+        screen_num = gdk_screen_get_number (screen);
+        nscreen = queue->priv->screens[screen_num];
+
+        nscreen->n_stacks = gdk_screen_get_n_monitors (screen);
+
+        nscreen->stacks = g_renew (NdStack *,
+                                   nscreen->stacks,
+                                   nscreen->n_stacks);
+
+        for (i = 0; i < nscreen->n_stacks; i++) {
+                create_stack_for_monitor (queue, screen, i);
+        }
+}
+
+static GdkFilterReturn
+screen_xevent_filter (GdkXEvent    *xevent,
+                      GdkEvent     *event,
+                      NotifyScreen *nscreen)
+{
+        XEvent *xev;
+
+        xev = (XEvent *) xevent;
+
+        if (xev->type == PropertyNotify &&
+            xev->xproperty.atom == nscreen->workarea_atom) {
+                int i;
+
+                for (i = 0; i < nscreen->n_stacks; i++) {
+                        nd_stack_queue_update_position (nscreen->stacks[i]);
+                }
+        }
+
+        return GDK_FILTER_CONTINUE;
+}
+
+static void
+create_screens (NdQueue *queue)
+{
+        GdkDisplay  *display;
+        int          i;
+
+        g_assert (queue->priv->screens == NULL);
+
+        display = gdk_display_get_default ();
+        queue->priv->n_screens = gdk_display_get_n_screens (display);
+
+        queue->priv->screens = g_new0 (NotifyScreen *, queue->priv->n_screens);
+
+        for (i = 0; i < queue->priv->n_screens; i++) {
+                GdkScreen *screen;
+                GdkWindow *gdkwindow;
+
+                screen = gdk_display_get_screen (display, i);
+                g_signal_connect (screen,
+                                  "monitors-changed",
+                                  G_CALLBACK (on_screen_monitors_changed),
+                                  queue);
+
+                queue->priv->screens[i] = g_new0 (NotifyScreen, 1);
+
+                queue->priv->screens[i]->workarea_atom = XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "_NET_WORKAREA", True);
+                gdkwindow = gdk_screen_get_root_window (screen);
+                gdk_window_add_filter (gdkwindow, (GdkFilterFunc) screen_xevent_filter, queue->priv->screens[i]);
+                gdk_window_set_events (gdkwindow, gdk_window_get_events (gdkwindow) | GDK_PROPERTY_CHANGE_MASK);
+
+                create_stacks_for_screen (queue, gdk_display_get_screen (display, i));
+        }
+}
+
+static void
+nd_queue_class_init (NdQueueClass *klass)
+{
+        GObjectClass   *object_class = G_OBJECT_CLASS (klass);
+
+        object_class->finalize = nd_queue_finalize;
+
+        signals [CHANGED] =
+                g_signal_new ("changed",
+                              G_TYPE_FROM_CLASS (object_class),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (NdQueueClass, changed),
+                              NULL,
+                              NULL,
+                              g_cclosure_marshal_VOID__VOID,
+                              G_TYPE_NONE,
+                              0);
+
+        g_type_class_add_private (klass, sizeof (NdQueuePrivate));
+}
+
+static void
+popdown_dock (NdQueue *queue)
+{
+        GdkDisplay *display;
+
+        /* ungrab focus */
+        display = gtk_widget_get_display (queue->priv->dock);
+        gdk_display_keyboard_ungrab (display, GDK_CURRENT_TIME);
+        gdk_display_pointer_ungrab (display, GDK_CURRENT_TIME);
+        gtk_grab_remove (queue->priv->dock);
+
+        /* hide again */
+        gtk_widget_hide (queue->priv->dock);
+}
+
+/* This is called when the grab is broken for
+ * either the dock, or the scale itself */
+static void
+grab_notify (NdQueue   *queue,
+             gboolean   was_grabbed)
+{
+        if (was_grabbed != FALSE) {
+                return;
+        }
+
+        if (!gtk_widget_has_grab (queue->priv->dock)) {
+                return;
+        }
+
+        if (gtk_widget_is_ancestor (gtk_grab_get_current (), queue->priv->dock)) {
+                return;
+        }
+
+        popdown_dock (queue);
+}
+
+static void
+on_dock_grab_notify (GtkWidget *widget,
+                     gboolean   was_grabbed,
+                     NdQueue   *queue)
+{
+        grab_notify (queue, was_grabbed);
+}
+
+static gboolean
+on_dock_grab_broken_event (GtkWidget *widget,
+                           gboolean   was_grabbed,
+                           NdQueue   *queue)
+{
+        grab_notify (queue, FALSE);
+
+        return FALSE;
+}
+
+static gboolean
+on_dock_key_release (GtkWidget   *widget,
+                     GdkEventKey *event,
+                     NdQueue     *queue)
+{
+        if (event->keyval == GDK_KEY_Escape) {
+                popdown_dock (queue);
+                return TRUE;
+        }
+
+        return TRUE;
+}
+
+static void
+create_dock (NdQueue *queue)
+{
+        GtkWidget *frame;
+        GtkWidget *box;
+        GtkWidget *button;
+
+        queue->priv->dock = gtk_window_new (GTK_WINDOW_POPUP);
+        gtk_widget_set_name (queue->priv->dock, "notification-popup-window");
+        g_signal_connect (queue->priv->dock,
+                          "grab-notify",
+                          G_CALLBACK (on_dock_grab_notify),
+                          queue);
+        g_signal_connect (queue->priv->dock,
+                          "grab-broken-event",
+                          G_CALLBACK (on_dock_grab_broken_event),
+                          queue);
+        g_signal_connect (queue->priv->dock,
+                          "key-release-event",
+                          G_CALLBACK (on_dock_key_release),
+                          queue);
+#if 0
+        g_signal_connect (queue->priv->dock,
+                          "button-press-event",
+                          G_CALLBACK (on_dock_button_press),
+                          queue);
+        g_signal_connect (queue->priv->dock,
+                          "scroll-event",
+                          G_CALLBACK (on_dock_scroll_event),
+                          queue);
+#endif
+        gtk_window_set_decorated (GTK_WINDOW (queue->priv->dock), FALSE);
+
+        frame = gtk_frame_new (NULL);
+        gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_OUT);
+        gtk_container_add (GTK_CONTAINER (queue->priv->dock), frame);
+
+        box = gtk_vbox_new (FALSE, 6);
+        gtk_container_set_border_width (GTK_CONTAINER (box), 2);
+        gtk_container_add (GTK_CONTAINER (frame), box);
+
+        queue->priv->dock_scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+        gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (queue->priv->dock_scrolled_window),
+                                        GTK_POLICY_NEVER,
+                                        GTK_POLICY_AUTOMATIC);
+        gtk_widget_set_size_request (queue->priv->dock_scrolled_window,
+                                     WIDTH,
+                                     -1);
+        gtk_box_pack_start (GTK_BOX (box), queue->priv->dock_scrolled_window, TRUE, TRUE, 0);
+
+        button = gtk_button_new_with_label (_("Clear all notifications"));
+        gtk_box_pack_end (GTK_BOX (box), button, FALSE, FALSE, 0);
+}
+
+static void
+nd_queue_init (NdQueue *queue)
+{
+        queue->priv = ND_QUEUE_GET_PRIVATE (queue);
+        queue->priv->notifications = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref);
+        queue->priv->bubbles = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref);
+        queue->priv->queue = g_queue_new ();
+        queue->priv->status_icon = NULL;
+
+        create_dock (queue);
+        create_screens (queue);
+}
+
+static void
+nd_queue_finalize (GObject *object)
+{
+        NdQueue *queue;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (ND_IS_QUEUE (object));
+
+        queue = ND_QUEUE (object);
+
+        g_return_if_fail (queue->priv != NULL);
+
+        g_hash_table_destroy (queue->priv->notifications);
+        g_queue_free (queue->priv->queue);
+
+        G_OBJECT_CLASS (nd_queue_parent_class)->finalize (object);
+}
+
+NdNotification *
+nd_queue_lookup (NdQueue *queue,
+                 guint    id)
+{
+        NdNotification *notification;
+
+        g_return_val_if_fail (ND_IS_QUEUE (queue), NULL);
+
+        notification = g_hash_table_lookup (queue->priv->notifications, GUINT_TO_POINTER (id));
+
+        return notification;
+}
+
+guint
+nd_queue_length (NdQueue *queue)
+{
+        g_return_val_if_fail (ND_IS_QUEUE (queue), 0);
+
+        return g_hash_table_size (queue->priv->notifications);
+}
+
+static NdStack *
+get_stack_with_pointer (NdQueue *queue)
+{
+        GdkScreen *screen;
+        int        x, y;
+        int        screen_num;
+        int        monitor_num;
+
+        gdk_display_get_pointer (gdk_display_get_default (),
+                                 &screen,
+                                 &x,
+                                 &y,
+                                 NULL);
+        screen_num = gdk_screen_get_number (screen);
+        monitor_num = gdk_screen_get_monitor_at_point (screen, x, y);
+
+        if (monitor_num >= queue->priv->screens[screen_num]->n_stacks) {
+                /* screw it - dump it on the last one we'll get
+                   a monitors-changed signal soon enough*/
+                monitor_num = queue->priv->screens[screen_num]->n_stacks - 1;
+        }
+
+        return queue->priv->screens[screen_num]->stacks[monitor_num];
+}
+
+static void
+on_bubble_destroyed (NdBubble *bubble,
+                     NdQueue  *queue)
+{
+        g_debug ("Bubble destroyed");
+        queue_update (queue);
+}
+
+static void
+maybe_show_notification (NdQueue *queue)
+{
+        gpointer        id;
+        NdNotification *notification;
+        NdBubble       *bubble;
+        NdStack        *stack;
+        GList          *list;
+
+        /* FIXME: show one at a time if not busy or away */
+
+        stack = get_stack_with_pointer (queue);
+        list = nd_stack_get_bubbles (stack);
+        if (g_list_length (list) > 0) {
+                /* already showing bubbles */
+                g_debug ("Already showing bubbles");
+                return;
+        }
+
+        id = g_queue_pop_tail (queue->priv->queue);
+        if (id == NULL) {
+                /* Nothing to do */
+                g_debug ("No queued notifications");
+                return;
+        }
+
+        notification = g_hash_table_lookup (queue->priv->notifications, id);
+        g_assert (notification != NULL);
+
+        bubble = nd_bubble_new_for_notification (notification);
+        g_signal_connect (bubble, "destroy", G_CALLBACK (on_bubble_destroyed), queue);
+
+        nd_stack_add_bubble (stack, bubble, TRUE);
+}
+
+static int
+collate_notifications (NdNotification *a,
+                       NdNotification *b)
+{
+        GTimeVal tva;
+        GTimeVal tvb;
+
+        nd_notification_get_update_time (a, &tva);
+        nd_notification_get_update_time (b, &tvb);
+        if (tva.tv_sec > tvb.tv_sec) {
+                return 1;
+        } else {
+                return -1;
+        }
+}
+
+static gboolean
+on_activate_link (GtkLabel *label,
+                  char     *uri,
+                  NdQueue  *queue)
+{
+        char *escaped_uri;
+        char *cmd = NULL;
+
+        /* Somewhat of a hack.. */
+        //bubble->priv->url_clicked_lock = TRUE;
+
+        escaped_uri = g_shell_quote (uri);
+
+        if (g_find_program_in_path ("gvfs-open") != NULL) {
+                cmd = g_strdup_printf ("gvfs-open %s", escaped_uri);
+        } else if (g_find_program_in_path ("xdg-open") != NULL) {
+                cmd = g_strdup_printf ("xdg-open %s", escaped_uri);
+        } else if (g_find_program_in_path ("firefox") != NULL) {
+                cmd = g_strdup_printf ("firefox %s", escaped_uri);
+        } else {
+                g_warning ("Unable to find a browser.");
+        }
+
+        g_free (escaped_uri);
+
+        if (cmd != NULL) {
+                g_spawn_command_line_async (cmd, NULL);
+                g_free (cmd);
+        }
+
+        return TRUE;
+}
+
+static void
+on_close_button_clicked (GtkButton      *button,
+                         NdNotification *notification)
+{
+        nd_notification_close (notification, ND_NOTIFICATION_CLOSED_USER);
+}
+
+static void
+on_action_clicked (GtkButton      *button,
+                   GdkEventButton *event,
+                   NdNotification *notification)
+{
+        const char *key = g_object_get_data (G_OBJECT (button), "_action_key");
+
+        nd_notification_action_invoked (notification,
+                                        key);
+}
+
+static GtkWidget *
+create_notification_action (NdQueue        *queue,
+                            NdNotification *notification,
+                            const char     *text,
+                            const char     *key)
+{
+        GtkWidget *label;
+        GtkWidget *button;
+        GtkWidget *hbox;
+        GdkPixbuf *pixbuf;
+        char      *buf;
+
+        button = gtk_button_new ();
+        gtk_widget_show (button);
+        gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
+        gtk_container_set_border_width (GTK_CONTAINER (button), 0);
+
+        hbox = gtk_hbox_new (FALSE, 6);
+        gtk_widget_show (hbox);
+        gtk_container_add (GTK_CONTAINER (button), hbox);
+
+        /* Try to be smart and find a suitable icon. */
+        buf = g_strdup_printf ("stock_%s", key);
+        pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_for_screen (gtk_widget_get_screen (GTK_WIDGET (queue->priv->dock))),
+                                           buf,
+                                           16,
+                                           GTK_ICON_LOOKUP_USE_BUILTIN,
+                                           NULL);
+        g_free (buf);
+
+        if (pixbuf != NULL) {
+                GtkWidget *image = gtk_image_new_from_pixbuf (pixbuf);
+                gtk_widget_show (image);
+                gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
+                gtk_misc_set_alignment (GTK_MISC (image), 0.5, 0.5);
+        }
+
+        label = gtk_label_new (NULL);
+        gtk_widget_show (label);
+        gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
+        gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);
+        buf = g_strdup_printf ("<small>%s</small>", text);
+        gtk_label_set_markup (GTK_LABEL (label), buf);
+        g_free (buf);
+
+        g_object_set_data_full (G_OBJECT (button),
+                                "_action_key", g_strdup (key), g_free);
+        g_signal_connect (G_OBJECT (button),
+                          "button-release-event",
+                          G_CALLBACK (on_action_clicked),
+                          notification);
+        return button;
+}
+
+static gboolean
+on_button_release (GtkWidget      *widget,
+                   GdkEventButton *event,
+                   NdNotification *notification)
+{
+        g_debug ("CLICK");
+        nd_notification_action_invoked (notification, "default");
+
+        return FALSE;
+}
+
+static GtkWidget *
+create_notification_box (NdQueue        *queue,
+                         NdNotification *n)
+{
+        GtkWidget     *box;
+        GtkWidget     *iconbox;
+        GtkWidget     *icon;
+        GtkWidget     *image;
+        GtkWidget     *content_hbox;
+        GtkWidget     *actions_box;
+        GtkWidget     *vbox;
+        GtkWidget     *summary_label;
+        GtkWidget     *body_label;
+        GtkWidget     *alignment;
+        GtkWidget     *close_button;
+        AtkObject     *atkobj;
+        GtkRcStyle    *rcstyle;
+        char          *str;
+        char          *quoted;
+        GtkRequisition req;
+        int            summary_width;
+        gboolean       have_icon;
+        gboolean       have_body;
+        gboolean       have_actions;
+        GdkPixbuf     *pixbuf;
+        char         **actions;
+        int            i;
+
+        box = gtk_hbox_new (FALSE, 6);
+        gtk_widget_add_events (box, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
+
+        /* First row (icon, vbox, close) */
+        iconbox = gtk_alignment_new (0.5, 0, 0, 0);
+        gtk_widget_show (iconbox);
+        gtk_alignment_set_padding (GTK_ALIGNMENT (iconbox),
+                                   5, 0, 0, 0);
+        gtk_box_pack_start (GTK_BOX (box),
+                            iconbox,
+                            FALSE, FALSE, 0);
+        gtk_widget_set_size_request (iconbox, BODY_X_OFFSET, -1);
+
+        icon = gtk_image_new ();
+        gtk_widget_show (icon);
+        gtk_container_add (GTK_CONTAINER (iconbox), icon);
+
+        vbox = gtk_vbox_new (FALSE, 6);
+        gtk_widget_show (vbox);
+        gtk_box_pack_start (GTK_BOX (box), vbox, TRUE, TRUE, 0);
+        gtk_container_set_border_width (GTK_CONTAINER (vbox), 10);
+
+        /* Add the close button */
+        alignment = gtk_alignment_new (0.5, 0, 0, 0);
+        gtk_widget_show (alignment);
+        gtk_box_pack_start (GTK_BOX (box), alignment, FALSE, FALSE, 0);
+
+        close_button = gtk_button_new ();
+        gtk_widget_show (close_button);
+        gtk_container_add (GTK_CONTAINER (alignment), close_button);
+        gtk_button_set_relief (GTK_BUTTON (close_button), GTK_RELIEF_NONE);
+        gtk_container_set_border_width (GTK_CONTAINER (close_button), 0);
+        g_signal_connect (G_OBJECT (close_button),
+                          "clicked",
+                          G_CALLBACK (on_close_button_clicked),
+                          n);
+
+        rcstyle = gtk_rc_style_new ();
+        rcstyle->xthickness = rcstyle->ythickness = 0;
+        gtk_widget_modify_style (close_button, rcstyle);
+        g_object_unref (rcstyle);
+
+        atkobj = gtk_widget_get_accessible (close_button);
+        atk_action_set_description (ATK_ACTION (atkobj), 0,
+                                    "Closes the notification.");
+        atk_object_set_name (atkobj, "");
+        atk_object_set_description (atkobj, "Closes the notification.");
+
+        image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU);
+        gtk_widget_show (image);
+        gtk_container_add (GTK_CONTAINER (close_button), image);
+
+        /* center vbox */
+        summary_label = gtk_label_new (NULL);
+        gtk_widget_show (summary_label);
+        gtk_box_pack_start (GTK_BOX (vbox), summary_label, TRUE, TRUE, 0);
+        gtk_misc_set_alignment (GTK_MISC (summary_label), 0, 0);
+        gtk_label_set_line_wrap (GTK_LABEL (summary_label), TRUE);
+
+        atkobj = gtk_widget_get_accessible (summary_label);
+        atk_object_set_description (atkobj, "Notification summary text.");
+
+        content_hbox = gtk_hbox_new (FALSE, 6);
+        gtk_widget_show (content_hbox);
+        gtk_box_pack_start (GTK_BOX (vbox), content_hbox, FALSE, FALSE, 0);
+
+
+        vbox = gtk_vbox_new (FALSE, 6);
+        gtk_widget_show (vbox);
+        gtk_box_pack_start (GTK_BOX (content_hbox), vbox, TRUE, TRUE, 0);
+
+        body_label = gtk_label_new (NULL);
+        gtk_widget_show (body_label);
+        gtk_box_pack_start (GTK_BOX (vbox), body_label, TRUE, TRUE, 0);
+        gtk_misc_set_alignment (GTK_MISC (body_label), 0, 0);
+        gtk_label_set_line_wrap (GTK_LABEL (body_label), TRUE);
+        g_signal_connect (body_label,
+                          "activate-link",
+                          G_CALLBACK (on_activate_link),
+                          queue);
+
+        atkobj = gtk_widget_get_accessible (body_label);
+        atk_object_set_description (atkobj, "Notification body text.");
+
+        alignment = gtk_alignment_new (1, 0.5, 0, 0);
+        gtk_widget_show (alignment);
+        gtk_box_pack_start (GTK_BOX (vbox), alignment, FALSE, TRUE, 0);
+
+        actions_box = gtk_hbox_new (FALSE, 6);
+        gtk_widget_show (actions_box);
+        gtk_container_add (GTK_CONTAINER (alignment), actions_box);
+
+        /* Add content */
+
+        have_icon = FALSE;
+        have_body = FALSE;
+        have_actions = FALSE;
+
+        /* image */
+        pixbuf = nd_notification_load_image (n, IMAGE_SIZE);
+        if (pixbuf != NULL) {
+                gtk_image_set_from_pixbuf (GTK_IMAGE (icon), pixbuf);
+
+                g_object_unref (G_OBJECT (pixbuf));
+                have_icon = TRUE;
+        }
+
+        /* summary */
+        quoted = g_markup_escape_text (nd_notification_get_summary (n), -1);
+        str = g_strdup_printf ("<b><big>%s</big></b>", quoted);
+        g_free (quoted);
+
+        gtk_label_set_markup (GTK_LABEL (summary_label), str);
+        g_free (str);
+
+        gtk_widget_size_request (close_button, &req);
+        /* -1: main_vbox border width
+           -10: vbox border width
+           -6: spacing for hbox */
+        summary_width = WIDTH - (1*2) - (10*2) - BODY_X_OFFSET - req.width - (6*2);
+
+        gtk_widget_set_size_request (summary_label,
+                                     summary_width,
+                                     -1);
+
+        /* body */
+        gtk_label_set_markup (GTK_LABEL (body_label), nd_notification_get_body (n));
+
+        if (str != NULL && *str != '\0') {
+                gtk_widget_set_size_request (body_label,
+                                             summary_width,
+                                             -1);
+                have_body = TRUE;
+        }
+
+        /* actions */
+        actions = nd_notification_get_actions (n);
+        for (i = 0; actions[i] != NULL; i += 2) {
+                char *l = actions[i + 1];
+
+                if (l == NULL) {
+                        g_warning ("Label not found for action %s. "
+                                   "The protocol specifies that a label must "
+                                   "follow an action in the actions array",
+                                   actions[i]);
+
+                        break;
+                }
+
+                if (strcasecmp (actions[i], "default") != 0) {
+                        GtkWidget *button;
+
+                        button = create_notification_action (queue,
+                                                             n,
+                                                             l,
+                                                             actions[i]);
+                        gtk_box_pack_start (GTK_BOX (actions_box), button, FALSE, FALSE, 0);
+
+                        have_actions = TRUE;
+                }
+        }
+
+        g_signal_connect (box, "button-release-event", G_CALLBACK (on_button_release), n);
+
+        if (have_icon || have_body || have_actions) {
+                gtk_widget_show (content_hbox);
+        } else {
+                gtk_widget_hide (content_hbox);
+        }
+
+        return box;
+}
+
+static void
+update_dock (NdQueue *queue)
+{
+        GtkWidget   *child;
+        GList       *list;
+        GList       *l;
+        int          min_height;
+        int          height;
+        int          monitor_num;
+        GdkScreen   *screen;
+        GdkRectangle area;
+
+        g_return_if_fail (queue);
+
+        child = gtk_bin_get_child (GTK_BIN (queue->priv->dock_scrolled_window));
+        if (child != NULL)
+                gtk_container_remove (GTK_CONTAINER (queue->priv->dock_scrolled_window), child);
+
+        child = gtk_vbox_new (FALSE, 6);
+        gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (queue->priv->dock_scrolled_window),
+                                               child);
+        list = g_hash_table_get_values (queue->priv->notifications);
+        list = g_list_sort (list, (GCompareFunc)collate_notifications);
+
+        for (l = list; l != NULL; l = l->next) {
+                NdNotification *n = l->data;
+                GtkWidget      *hbox;
+                GtkWidget      *sep;
+
+                hbox = create_notification_box (queue, n);
+                gtk_widget_show (hbox);
+                gtk_box_pack_start (GTK_BOX (child), hbox, FALSE, FALSE, 0);
+
+                sep = gtk_hseparator_new ();
+                gtk_widget_show (sep);
+                gtk_box_pack_start (GTK_BOX (child), sep, FALSE, FALSE, 0);
+        }
+        gtk_widget_show (child);
+        gtk_widget_get_preferred_height (child,
+                                         &min_height,
+                                         &height);
+
+        gtk_status_icon_get_geometry (GTK_STATUS_ICON (queue->priv->status_icon),
+                                      &screen,
+                                      &area,
+                                      NULL);
+        monitor_num = gdk_screen_get_monitor_at_point (screen, area.x, area.y);
+        gdk_screen_get_monitor_geometry (screen, monitor_num, &area);
+        height = MIN (height, (area.height / 2));
+        gtk_widget_set_size_request (queue->priv->dock_scrolled_window,
+                                     WIDTH,
+                                     height);
+
+        g_list_free (list);
+}
+
+static gboolean
+popup_dock (NdQueue *queue,
+            guint    time)
+{
+        GdkRectangle   area;
+        GtkOrientation orientation;
+        GdkDisplay    *display;
+        GdkScreen     *screen;
+        gboolean       res;
+        int            x;
+        int            y;
+        int            monitor_num;
+        GdkRectangle   monitor;
+        GtkRequisition dock_req;
+
+        update_dock (queue);
+
+        res = gtk_status_icon_get_geometry (GTK_STATUS_ICON (queue->priv->status_icon),
+                                            &screen,
+                                            &area,
+                                            &orientation);
+        if (! res) {
+                g_warning ("Unable to determine geometry of status icon");
+                return FALSE;
+        }
+
+        /* position roughly */
+        gtk_window_set_screen (GTK_WINDOW (queue->priv->dock), screen);
+
+        monitor_num = gdk_screen_get_monitor_at_point (screen, area.x, area.y);
+        gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor);
+
+        gtk_container_foreach (GTK_CONTAINER (queue->priv->dock),
+                               (GtkCallback) gtk_widget_show_all, NULL);
+        gtk_widget_get_preferred_size (queue->priv->dock, &dock_req, NULL);
+
+        if (orientation == GTK_ORIENTATION_VERTICAL) {
+                if (area.x + area.width + dock_req.width <= monitor.x + monitor.width) {
+                        x = area.x + area.width;
+                } else {
+                        x = area.x - dock_req.width;
+                }
+                if (area.y + dock_req.height <= monitor.y + monitor.height) {
+                        y = area.y;
+                } else {
+                        y = monitor.y + monitor.height - dock_req.height;
+                }
+        } else {
+                if (area.y + area.height + dock_req.height <= monitor.y + monitor.height) {
+                        y = area.y + area.height;
+                } else {
+                        y = area.y - dock_req.height;
+                }
+                if (area.x + dock_req.width <= monitor.x + monitor.width) {
+                        x = area.x;
+                } else {
+                        x = monitor.x + monitor.width - dock_req.width;
+                }
+        }
+
+        gtk_window_move (GTK_WINDOW (queue->priv->dock), x, y);
+
+        /* FIXME: without this, the popup window appears as a square
+         * after changing the orientation
+         */
+        gtk_window_resize (GTK_WINDOW (queue->priv->dock), 1, 1);
+
+        gtk_widget_show_all (queue->priv->dock);
+
+
+        /* grab focus */
+        gtk_grab_add (queue->priv->dock);
+
+        if (gdk_pointer_grab (gtk_widget_get_window (queue->priv->dock), TRUE,
+                              GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+                              GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK, NULL, NULL,
+                              time)
+            != GDK_GRAB_SUCCESS) {
+                gtk_grab_remove (queue->priv->dock);
+                gtk_widget_hide (queue->priv->dock);
+                return FALSE;
+        }
+
+        if (gdk_keyboard_grab (gtk_widget_get_window (queue->priv->dock), TRUE, time) != GDK_GRAB_SUCCESS) {
+                display = gtk_widget_get_display (queue->priv->dock);
+                gdk_display_pointer_ungrab (display, time);
+                gtk_grab_remove (queue->priv->dock);
+                gtk_widget_hide (queue->priv->dock);
+                return FALSE;
+        }
+
+        gtk_widget_grab_focus (queue->priv->dock);
+
+        return TRUE;
+}
+
+static void
+on_status_icon_activate (GtkStatusIcon *status_icon,
+                         NdQueue       *queue)
+{
+        popup_dock (queue, GDK_CURRENT_TIME);
+}
+
+static void
+on_status_icon_visible_notify (GtkStatusIcon *icon,
+                               GParamSpec    *pspec,
+                               NdQueue       *queue)
+{
+        gboolean visible;
+
+        g_object_get (icon, "visible", &visible, NULL);
+        if (! visible) {
+                if (queue->priv->dock != NULL) {
+                        gtk_widget_hide (queue->priv->dock);
+                }
+        }
+}
+
+static gboolean
+update_idle (NdQueue *queue)
+{
+        /* Show the status icon when their are stored notifications */
+        if (g_hash_table_size (queue->priv->notifications) > 0) {
+                if (queue->priv->status_icon == NULL) {
+                        queue->priv->status_icon = gtk_status_icon_new_from_icon_name ("mail-message-new");
+                        gtk_status_icon_set_title (GTK_STATUS_ICON (queue->priv->status_icon),
+                                                   _("Notifications"));
+                        g_signal_connect (queue->priv->status_icon,
+                                          "activate",
+                                          G_CALLBACK (on_status_icon_activate),
+                                          queue);
+                        g_signal_connect (queue->priv->status_icon,
+                                          "notify::visible",
+                                          G_CALLBACK (on_status_icon_visible_notify),
+                                          queue);
+                }
+                gtk_status_icon_set_visible (queue->priv->status_icon, TRUE);
+
+                maybe_show_notification (queue);
+        } else {
+                if (queue->priv->status_icon != NULL) {
+                        g_object_unref (queue->priv->status_icon);
+                        queue->priv->status_icon = NULL;
+                }
+        }
+
+        return FALSE;
+}
+
+static void
+queue_update (NdQueue *queue)
+{
+        if (queue->priv->update_id > 0) {
+                g_source_remove (queue->priv->update_id);
+        }
+
+        queue->priv->update_id = g_idle_add ((GSourceFunc)update_idle, queue);
+}
+
+void
+nd_queue_remove (NdQueue *queue,
+                 guint    id)
+{
+        g_return_if_fail (ND_IS_QUEUE (queue));
+        g_debug ("Removing id %u", id);
+        g_queue_remove (queue->priv->queue, GUINT_TO_POINTER (id));
+        g_hash_table_remove (queue->priv->notifications, GUINT_TO_POINTER (id));
+        /* FIXME: should probably only emit this when it really removes something */
+        g_signal_emit (queue, signals[CHANGED], 0);
+
+        queue_update (queue);
+}
+
+void
+nd_queue_add (NdQueue        *queue,
+              NdNotification *notification)
+{
+        guint id;
+
+        g_return_if_fail (ND_IS_QUEUE (queue));
+
+        id = nd_notification_get_id (notification);
+        g_debug ("Adding id %u", id);
+        g_hash_table_insert (queue->priv->notifications, GUINT_TO_POINTER (id), g_object_ref (notification));
+        g_queue_push_head (queue->priv->queue, GUINT_TO_POINTER (id));
+
+        /* FIXME: should probably only emit this when it really adds something */
+        g_signal_emit (queue, signals[CHANGED], 0);
+
+        queue_update (queue);
+}
+
+NdQueue *
+nd_queue_new (void)
+{
+        if (queue_object != NULL) {
+                g_object_ref (queue_object);
+        } else {
+                queue_object = g_object_new (ND_TYPE_QUEUE, NULL);
+                g_object_add_weak_pointer (queue_object,
+                                           (gpointer *) &queue_object);
+        }
+
+        return ND_QUEUE (queue_object);
+}
diff --git a/src/nd-queue.h b/src/nd-queue.h
new file mode 100644
index 0000000..876a2d8
--- /dev/null
+++ b/src/nd-queue.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __ND_QUEUE_H
+#define __ND_QUEUE_H
+
+#include <glib-object.h>
+
+#include "nd-notification.h"
+
+G_BEGIN_DECLS
+
+#define ND_TYPE_QUEUE         (nd_queue_get_type ())
+#define ND_QUEUE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), ND_TYPE_QUEUE, NdQueue))
+#define ND_QUEUE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), ND_TYPE_QUEUE, NdQueueClass))
+#define ND_IS_QUEUE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), ND_TYPE_QUEUE))
+#define ND_IS_QUEUE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), ND_TYPE_QUEUE))
+#define ND_QUEUE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), ND_TYPE_QUEUE, NdQueueClass))
+
+typedef struct NdQueuePrivate NdQueuePrivate;
+
+typedef struct
+{
+        GObject           parent;
+        NdQueuePrivate *priv;
+} NdQueue;
+
+typedef struct
+{
+        GObjectClass   parent_class;
+
+        void          (* changed) (NdQueue      *queue);
+} NdQueueClass;
+
+GType               nd_queue_get_type                       (void);
+
+NdQueue *           nd_queue_new                            (void);
+
+guint               nd_queue_length                         (NdQueue        *queue);
+
+NdNotification *    nd_queue_lookup                         (NdQueue        *queue,
+                                                             guint           id);
+
+void                nd_queue_add                            (NdQueue        *queue,
+                                                             NdNotification *notification);
+void                nd_queue_remove                         (NdQueue        *queue,
+                                                             guint           id);
+
+G_END_DECLS
+
+#endif /* __ND_QUEUE_H */
diff --git a/src/daemon/stack.c b/src/nd-stack.c
similarity index 52%
rename from src/daemon/stack.c
rename to src/nd-stack.c
index 936edbe..27fca03 100644
--- a/src/daemon/stack.c
+++ b/src/nd-stack.c
@@ -1,12 +1,11 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
  *
- * Copyright (C) 2006 Christian Hammond <chipx86 chipx86 com>
  * Copyright (C) 2010 Red Hat, Inc.
  *
  * 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 2, or (at your option)
- * any later version.
+ * the Free Software Foundation; either version 2 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
@@ -15,13 +14,15 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
- * 02111-1307, USA.
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
  */
 
 #include "config.h"
-#include "engines.h"
-#include "stack.h"
+
+#include <string.h>
+#include <strings.h>
+#include <glib.h>
 
 #include <X11/Xproto.h>
 #include <X11/Xlib.h>
@@ -29,27 +30,36 @@
 #include <X11/Xatom.h>
 #include <gdk/gdkx.h>
 
+#include "nd-stack.h"
+
+#define ND_STACK_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), ND_TYPE_STACK, NdStackPrivate))
+
 #define NOTIFY_STACK_SPACING 2
 #define WORKAREA_PADDING 6
 
-struct _NotifyStack
+struct NdStackPrivate
 {
-        NotifyDaemon       *daemon;
-        GdkScreen          *screen;
-        guint               monitor;
-        NotifyStackLocation location;
-        GList              *windows;
-        guint               update_id;
+        GdkScreen      *screen;
+        guint           monitor;
+        NdStackLocation location;
+        GList          *bubbles;
+        guint           update_id;
 };
 
+static void     nd_stack_class_init  (NdStackClass *klass);
+static void     nd_stack_init        (NdStack      *stack);
+static void     nd_stack_finalize    (GObject       *object);
+
+G_DEFINE_TYPE (NdStack, nd_stack, G_TYPE_OBJECT)
+
 GList *
-notify_stack_get_windows (NotifyStack *stack)
+nd_stack_get_bubbles (NdStack *stack)
 {
-        return stack->windows;
+        return stack->priv->bubbles;
 }
 
 static gboolean
-get_work_area (NotifyStack  *stack,
+get_work_area (NdStack      *stack,
                GdkRectangle *rect)
 {
         Atom            workarea;
@@ -63,22 +73,24 @@ get_work_area (NotifyStack  *stack,
         long           *workareas;
         int             result;
         int             disp_screen;
+        Display        *display;
 
-        workarea = XInternAtom (GDK_DISPLAY (), "_NET_WORKAREA", True);
+        display = GDK_DISPLAY_XDISPLAY (gdk_screen_get_display (stack->priv->screen));
+        workarea = XInternAtom (display, "_NET_WORKAREA", True);
 
-        disp_screen = GDK_SCREEN_XNUMBER (stack->screen);
+        disp_screen = GDK_SCREEN_XNUMBER (stack->priv->screen);
 
         /* Defaults in case of error */
         rect->x = 0;
         rect->y = 0;
-        rect->width = gdk_screen_get_width (stack->screen);
-        rect->height = gdk_screen_get_height (stack->screen);
+        rect->width = gdk_screen_get_width (stack->priv->screen);
+        rect->height = gdk_screen_get_height (stack->priv->screen);
 
         if (workarea == None)
                 return FALSE;
 
-        win = XRootWindow (GDK_DISPLAY (), disp_screen);
-        result = XGetWindowProperty (GDK_DISPLAY (),
+        win = XRootWindow (display, disp_screen);
+        result = XGetWindowProperty (display,
                                      win,
                                      workarea,
                                      0,
@@ -111,7 +123,7 @@ get_work_area (NotifyStack  *stack,
 }
 
 static void
-get_origin_coordinates (NotifyStackLocation stack_location,
+get_origin_coordinates (NdStackLocation stack_location,
                         GdkRectangle       *workarea,
                         gint               *x,
                         gint               *y,
@@ -121,24 +133,24 @@ get_origin_coordinates (NotifyStackLocation stack_location,
                         gint                height)
 {
         switch (stack_location) {
-        case NOTIFY_STACK_LOCATION_TOP_LEFT:
+        case ND_STACK_LOCATION_TOP_LEFT:
                 *x = workarea->x;
                 *y = workarea->y;
                 *shifty = height;
                 break;
 
-        case NOTIFY_STACK_LOCATION_TOP_RIGHT:
+        case ND_STACK_LOCATION_TOP_RIGHT:
                 *x = workarea->x + workarea->width - width;
                 *y = workarea->y;
                 *shifty = height;
                 break;
 
-        case NOTIFY_STACK_LOCATION_BOTTOM_LEFT:
+        case ND_STACK_LOCATION_BOTTOM_LEFT:
                 *x = workarea->x;
                 *y = workarea->y + workarea->height - height;
                 break;
 
-        case NOTIFY_STACK_LOCATION_BOTTOM_RIGHT:
+        case ND_STACK_LOCATION_BOTTOM_RIGHT:
                 *x = workarea->x + workarea->width - width;
                 *y = workarea->y + workarea->height - height;
                 break;
@@ -149,7 +161,7 @@ get_origin_coordinates (NotifyStackLocation stack_location,
 }
 
 static void
-translate_coordinates (NotifyStackLocation stack_location,
+translate_coordinates (NdStackLocation stack_location,
                        GdkRectangle       *workarea,
                        gint               *x,
                        gint               *y,
@@ -159,24 +171,24 @@ translate_coordinates (NotifyStackLocation stack_location,
                        gint                height)
 {
         switch (stack_location) {
-        case NOTIFY_STACK_LOCATION_TOP_LEFT:
+        case ND_STACK_LOCATION_TOP_LEFT:
                 *x = workarea->x;
                 *y += *shifty;
                 *shifty = height;
                 break;
 
-        case NOTIFY_STACK_LOCATION_TOP_RIGHT:
+        case ND_STACK_LOCATION_TOP_RIGHT:
                 *x = workarea->x + workarea->width - width;
                 *y += *shifty;
                 *shifty = height;
                 break;
 
-        case NOTIFY_STACK_LOCATION_BOTTOM_LEFT:
+        case ND_STACK_LOCATION_BOTTOM_LEFT:
                 *x = workarea->x;
                 *y -= height;
                 break;
 
-        case NOTIFY_STACK_LOCATION_BOTTOM_RIGHT:
+        case ND_STACK_LOCATION_BOTTOM_RIGHT:
                 *x = workarea->x + workarea->width - width;
                 *y -= height;
                 break;
@@ -186,48 +198,70 @@ translate_coordinates (NotifyStackLocation stack_location,
         }
 }
 
-NotifyStack *
-notify_stack_new (NotifyDaemon       *daemon,
-                  GdkScreen          *screen,
-                  guint               monitor,
-                  NotifyStackLocation location)
+static void
+nd_stack_class_init (NdStackClass *klass)
 {
-        NotifyStack    *stack;
+        GObjectClass   *object_class = G_OBJECT_CLASS (klass);
 
-        g_assert (daemon != NULL);
-        g_assert (screen != NULL && GDK_IS_SCREEN (screen));
-        g_assert (monitor < (guint)gdk_screen_get_n_monitors (screen));
-        g_assert (location != NOTIFY_STACK_LOCATION_UNKNOWN);
+        object_class->finalize = nd_stack_finalize;
 
-        stack = g_new0 (NotifyStack, 1);
-        stack->daemon = daemon;
-        stack->screen = screen;
-        stack->monitor = monitor;
-        stack->location = location;
+        g_type_class_add_private (klass, sizeof (NdStackPrivate));
+}
 
-        return stack;
+static void
+nd_stack_init (NdStack *stack)
+{
+        stack->priv = ND_STACK_GET_PRIVATE (stack);
+        stack->priv->location = ND_STACK_LOCATION_DEFAULT;
 }
 
-void
-notify_stack_destroy (NotifyStack *stack)
+static void
+nd_stack_finalize (GObject *object)
 {
-        g_assert (stack != NULL);
+        NdStack *stack;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (ND_IS_STACK (object));
 
-        if (stack->update_id != 0) {
-                g_source_remove (stack->update_id);
+        stack = ND_STACK (object);
+
+        g_return_if_fail (stack->priv != NULL);
+
+        if (stack->priv->update_id != 0) {
+                g_source_remove (stack->priv->update_id);
         }
 
-        g_list_free (stack->windows);
-        g_free (stack);
+        g_list_free (stack->priv->bubbles);
+
+        G_OBJECT_CLASS (nd_stack_parent_class)->finalize (object);
 }
 
 void
-notify_stack_set_location (NotifyStack        *stack,
-                           NotifyStackLocation location)
+nd_stack_set_location (NdStack        *stack,
+                       NdStackLocation location)
+{
+        g_return_if_fail (ND_IS_STACK (stack));
+
+        stack->priv->location = location;
+}
+
+NdStack *
+nd_stack_new (GdkScreen *screen,
+              guint      monitor)
 {
-        stack->location = location;
+        NdStack *stack;
+
+        g_assert (screen != NULL && GDK_IS_SCREEN (screen));
+        g_assert (monitor < (guint)gdk_screen_get_n_monitors (screen));
+
+        stack = g_object_new (ND_TYPE_STACK, NULL);
+        stack->priv->screen = screen;
+        stack->priv->monitor = monitor;
+
+        return stack;
 }
 
+
 static void
 add_padding_to_rect (GdkRectangle *rect)
 {
@@ -243,13 +277,13 @@ add_padding_to_rect (GdkRectangle *rect)
 }
 
 static void
-notify_stack_shift_notifications (NotifyStack *stack,
-                                  GtkWindow   *nw,
-                                  GList      **nw_l,
-                                  gint         init_width,
-                                  gint         init_height,
-                                  gint        *nw_x,
-                                  gint        *nw_y)
+nd_stack_shift_notifications (NdStack     *stack,
+                              NdBubble    *bubble,
+                              GList      **nw_l,
+                              gint         init_width,
+                              gint         init_height,
+                              gint        *nw_x,
+                              gint        *nw_y)
 {
         GdkRectangle    workarea;
         GdkRectangle    monitor;
@@ -262,17 +296,17 @@ notify_stack_shift_notifications (NotifyStack *stack,
         int             n_wins;
 
         get_work_area (stack, &workarea);
-        gdk_screen_get_monitor_geometry (stack->screen,
-                                         stack->monitor,
+        gdk_screen_get_monitor_geometry (stack->priv->screen,
+                                         stack->priv->monitor,
                                          &monitor);
         gdk_rectangle_intersect (&monitor, &workarea, &workarea);
 
         add_padding_to_rect (&workarea);
 
-        n_wins = g_list_length (stack->windows);
+        n_wins = g_list_length (stack->priv->bubbles);
         positions = g_new0 (GdkRectangle, n_wins);
 
-        get_origin_coordinates (stack->location,
+        get_origin_coordinates (stack->priv->location,
                                 &workarea,
                                 &x, &y,
                                 &shiftx,
@@ -286,14 +320,14 @@ notify_stack_shift_notifications (NotifyStack *stack,
         if (nw_y != NULL)
                 *nw_y = y;
 
-        for (i = 0, l = stack->windows; l != NULL; i++, l = l->next) {
-                GtkWindow      *nw2 = GTK_WINDOW (l->data);
+        for (i = 0, l = stack->priv->bubbles; l != NULL; i++, l = l->next) {
+                NdBubble       *nw2 = ND_BUBBLE (l->data);
                 GtkRequisition  req;
 
-                if (nw == NULL || nw2 != nw) {
+                if (bubble == NULL || nw2 != bubble) {
                         gtk_widget_size_request (GTK_WIDGET (nw2), &req);
 
-                        translate_coordinates (stack->location,
+                        translate_coordinates (stack->priv->location,
                                                &workarea,
                                                &x,
                                                &y,
@@ -312,11 +346,11 @@ notify_stack_shift_notifications (NotifyStack *stack,
 
         /* move bubbles at the bottom of the stack first
            to avoid overlapping */
-        for (i = n_wins - 1, l = g_list_last (stack->windows); l != NULL; i--, l = l->prev) {
-                GtkWindow *nw2 = GTK_WINDOW (l->data);
+        for (i = n_wins - 1, l = g_list_last (stack->priv->bubbles); l != NULL; i--, l = l->prev) {
+                NdBubble *nw2 = ND_BUBBLE (l->data);
 
-                if (nw == NULL || nw2 != nw) {
-                        theme_move_notification (nw2, positions[i].x, positions[i].y);
+                if (bubble == NULL || nw2 != bubble) {
+                        gtk_window_move (GTK_WINDOW (nw2), positions[i].x, positions[i].y);
                 }
         }
 
@@ -324,80 +358,81 @@ notify_stack_shift_notifications (NotifyStack *stack,
 }
 
 static void
-update_position (NotifyStack *stack)
+update_position (NdStack *stack)
 {
-        notify_stack_shift_notifications (stack,
-                                          NULL, /* window */
-                                          NULL, /* list pointer */
-                                          0, /* init width */
-                                          0, /* init height */
-                                          NULL, /* out window x */
-                                          NULL); /* out window y */
+        nd_stack_shift_notifications (stack,
+                                      NULL, /* window */
+                                      NULL, /* list pointer */
+                                      0, /* init width */
+                                      0, /* init height */
+                                      NULL, /* out window x */
+                                      NULL); /* out window y */
 }
 
 static gboolean
-update_position_idle (NotifyStack *stack)
+update_position_idle (NdStack *stack)
 {
         update_position (stack);
 
-        stack->update_id = 0;
+        stack->priv->update_id = 0;
         return FALSE;
 }
 
 void
-notify_stack_queue_update_position (NotifyStack *stack)
+nd_stack_queue_update_position (NdStack *stack)
 {
-        if (stack->update_id != 0) {
+        if (stack->priv->update_id != 0) {
                 return;
         }
 
-        stack->update_id = g_idle_add ((GSourceFunc) update_position_idle, stack);
+        stack->priv->update_id = g_idle_add ((GSourceFunc) update_position_idle, stack);
 }
 
 void
-notify_stack_add_window (NotifyStack *stack,
-                         GtkWindow   *nw,
-                         gboolean     new_notification)
+nd_stack_add_bubble (NdStack  *stack,
+                     NdBubble *bubble,
+                     gboolean  new_notification)
 {
         GtkRequisition  req;
-        gint            x, y;
-
-        gtk_widget_size_request (GTK_WIDGET (nw), &req);
-        notify_stack_shift_notifications (stack,
-                                          nw,
-                                          NULL,
-                                          req.width,
-                                          req.height + NOTIFY_STACK_SPACING,
-                                          &x,
-                                          &y);
-        theme_move_notification (nw, x, y);
+        int             x, y;
+
+        gtk_widget_size_request (GTK_WIDGET (bubble), &req);
+        nd_stack_shift_notifications (stack,
+                                      bubble,
+                                      NULL,
+                                      req.width,
+                                      req.height + NOTIFY_STACK_SPACING,
+                                      &x,
+                                      &y);
+        gtk_widget_show (bubble);
+        gtk_window_move (GTK_WINDOW (bubble), x, y);
 
         if (new_notification) {
-                g_signal_connect_swapped (G_OBJECT (nw),
+                g_signal_connect_swapped (G_OBJECT (bubble),
                                           "destroy",
-                                          G_CALLBACK (notify_stack_remove_window),
+                                          G_CALLBACK (nd_stack_remove_bubble),
                                           stack);
-                stack->windows = g_list_prepend (stack->windows, nw);
+                stack->priv->bubbles = g_list_prepend (stack->priv->bubbles, bubble);
         }
 }
 
 void
-notify_stack_remove_window (NotifyStack *stack,
-                            GtkWindow   *nw)
+nd_stack_remove_bubble (NdStack  *stack,
+                        NdBubble *bubble)
 {
         GList *remove_l = NULL;
 
-        notify_stack_shift_notifications (stack,
-                                          nw,
-                                          &remove_l,
-                                          0,
-                                          0,
-                                          NULL,
-                                          NULL);
+        nd_stack_shift_notifications (stack,
+                                      bubble,
+                                      &remove_l,
+                                      0,
+                                      0,
+                                      NULL,
+                                      NULL);
 
         if (remove_l != NULL)
-                stack->windows = g_list_delete_link (stack->windows, remove_l);
+                stack->priv->bubbles = g_list_delete_link (stack->priv->bubbles, remove_l);
 
-        if (GTK_WIDGET_REALIZED (GTK_WIDGET (nw)))
-                gtk_widget_unrealize (GTK_WIDGET (nw));
+        if (gtk_widget_get_realized (GTK_WIDGET (bubble)))
+                gtk_widget_unrealize (GTK_WIDGET (bubble));
 }
diff --git a/src/nd-stack.h b/src/nd-stack.h
new file mode 100644
index 0000000..4f1dedf
--- /dev/null
+++ b/src/nd-stack.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+
+#ifndef __ND_STACK_H
+#define __ND_STACK_H
+
+#include <gtk/gtk.h>
+#include "nd-bubble.h"
+
+G_BEGIN_DECLS
+
+#define ND_TYPE_STACK         (nd_stack_get_type ())
+#define ND_STACK(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), ND_TYPE_STACK, NdStack))
+#define ND_STACK_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), ND_TYPE_STACK, NdStackClass))
+#define ND_IS_STACK(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), ND_TYPE_STACK))
+#define ND_IS_STACK_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), ND_TYPE_STACK))
+#define ND_STACK_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), ND_TYPE_STACK, NdStackClass))
+
+typedef struct NdStackPrivate NdStackPrivate;
+
+typedef struct
+{
+        GObject         parent;
+        NdStackPrivate *priv;
+} NdStack;
+
+typedef struct
+{
+        GObjectClass   parent_class;
+} NdStackClass;
+
+typedef enum
+{
+        ND_STACK_LOCATION_UNKNOWN = -1,
+        ND_STACK_LOCATION_TOP_LEFT,
+        ND_STACK_LOCATION_TOP_RIGHT,
+        ND_STACK_LOCATION_BOTTOM_LEFT,
+        ND_STACK_LOCATION_BOTTOM_RIGHT,
+        ND_STACK_LOCATION_DEFAULT = ND_STACK_LOCATION_TOP_RIGHT
+} NdStackLocation;
+
+GType           nd_stack_get_type              (void);
+
+NdStack *       nd_stack_new                   (GdkScreen      *screen,
+                                                guint           monitor);
+
+void            nd_stack_set_location          (NdStack        *stack,
+                                                NdStackLocation location);
+void            nd_stack_add_bubble            (NdStack        *stack,
+                                                NdBubble       *bubble,
+                                                gboolean        new_notification);
+void            nd_stack_remove_bubble         (NdStack        *stack,
+                                                NdBubble       *bubble);
+GList *         nd_stack_get_bubbles           (NdStack        *stack);
+void            nd_stack_queue_update_position (NdStack        *stack);
+
+G_END_DECLS
+
+#endif /* __ND_STACK_H */
diff --git a/src/daemon/notificationdaemon.xml b/src/notificationdaemon.xml
similarity index 100%
rename from src/daemon/notificationdaemon.xml
rename to src/notificationdaemon.xml
diff --git a/src/daemon/sound.c b/src/sound.c
similarity index 100%
rename from src/daemon/sound.c
rename to src/sound.c
diff --git a/src/daemon/sound.h b/src/sound.h
similarity index 100%
rename from src/daemon/sound.h
rename to src/sound.h



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]