/* EggNotificationBubble * Copyright (C) 2005 Colin Walters * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #include #include #include #include #include "eggnotificationbubble.h" #define DEFAULT_DELAY 500 /* Default delay in ms */ #define STICKY_DELAY 0 /* Delay before popping up next bubble * if we're sticky */ #define STICKY_REVERT_DELAY 1000 /* Delay before sticky bubble revert * to normal */ #define BORDER_SIZE 15 static void egg_notification_bubble_class_init (EggNotificationBubbleClass *klass); static void egg_notification_bubble_init (EggNotificationBubble *bubble); static void egg_notification_bubble_destroy (GtkObject *object); static void egg_notification_bubble_detach (EggNotificationBubble *bubble); static void egg_notification_bubble_event_handler (GtkWidget *widget, GdkEvent *event, gpointer user_data); static gint egg_notification_bubble_paint_window (EggNotificationBubble *bubble); static void egg_notification_bubble_unset_bubble_window (EggNotificationBubble *bubble); static GtkObjectClass *parent_class; enum { NOTIFICATION_CLICKED, NOTIFICATION_TIMEOUT, LAST_SIGNAL }; static guint egg_notification_bubble_signals[LAST_SIGNAL] = { 0 }; GType egg_notification_bubble_get_type (void) { static GType bubble_type = 0; if (!bubble_type) { static const GTypeInfo bubble_info = { sizeof (EggNotificationBubbleClass), NULL, /* base_init */ NULL, /* base_finalize */ (GClassInitFunc) egg_notification_bubble_class_init, NULL, /* class_finalize */ NULL, /* class_data */ sizeof (EggNotificationBubble), 0, /* n_preallocs */ (GInstanceInitFunc) egg_notification_bubble_init, }; bubble_type = g_type_register_static (GTK_TYPE_OBJECT, "EggNotificationBubble", &bubble_info, 0); } return bubble_type; } static void egg_notification_bubble_class_init (EggNotificationBubbleClass *class) { GtkObjectClass *object_class; object_class = (GtkObjectClass*) class; parent_class = g_type_class_peek_parent (class); object_class->destroy = egg_notification_bubble_destroy; egg_notification_bubble_signals[NOTIFICATION_CLICKED] = g_signal_new ("clicked", EGG_TYPE_NOTIFICATION_BUBBLE, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EggNotificationBubbleClass, clicked), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); egg_notification_bubble_signals[NOTIFICATION_TIMEOUT] = g_signal_new ("timeout", EGG_TYPE_NOTIFICATION_BUBBLE, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EggNotificationBubbleClass, timeout), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void egg_notification_bubble_init (EggNotificationBubble *bubble) { bubble->bubble_window = NULL; } static void bubble_window_display_closed (GdkDisplay *display, gboolean was_error, EggNotificationBubble *bubble) { egg_notification_bubble_unset_bubble_window (bubble); } static void disconnect_bubble_window_display_closed (EggNotificationBubble *bubble) { g_signal_handlers_disconnect_by_func (gtk_widget_get_display (bubble->bubble_window), (gpointer) bubble_window_display_closed, bubble); } static void egg_notification_bubble_unset_bubble_window (EggNotificationBubble *bubble) { if (bubble->bubble_window) { disconnect_bubble_window_display_closed (bubble); gtk_widget_destroy (bubble->bubble_window); bubble->bubble_window = NULL; } } static void egg_notification_bubble_destroy (GtkObject *object) { EggNotificationBubble *bubble = EGG_NOTIFICATION_BUBBLE (object); g_return_if_fail (bubble != NULL); if (bubble->timeout_id) { g_source_remove (bubble->timeout_id); bubble->timeout_id = 0; } egg_notification_bubble_detach (bubble); egg_notification_bubble_unset_bubble_window (bubble); GTK_OBJECT_CLASS (parent_class)->destroy (object); } static void force_window (EggNotificationBubble *bubble) { g_return_if_fail (EGG_IS_NOTIFICATION_BUBBLE (bubble)); if (!bubble->bubble_window) { GtkWidget *vbox; bubble->bubble_window = gtk_window_new (GTK_WINDOW_POPUP); gtk_widget_add_events (bubble->bubble_window, GDK_BUTTON_PRESS_MASK); gtk_widget_set_app_paintable (bubble->bubble_window, TRUE); gtk_window_set_resizable (GTK_WINDOW (bubble->bubble_window), FALSE); gtk_widget_set_name (bubble->bubble_window, "gtk-tooltips"); gtk_container_set_border_width (GTK_CONTAINER (bubble->bubble_window), BORDER_SIZE + 5); g_signal_connect_swapped (bubble->bubble_window, "expose_event", G_CALLBACK (egg_notification_bubble_paint_window), bubble); bubble->bubble_header_label = gtk_label_new (NULL); bubble->bubble_body_label = gtk_label_new (NULL); gtk_label_set_line_wrap (GTK_LABEL (bubble->bubble_header_label), TRUE); gtk_label_set_line_wrap (GTK_LABEL (bubble->bubble_body_label), TRUE); gtk_misc_set_alignment (GTK_MISC (bubble->bubble_header_label), 0.5, 0.5); gtk_misc_set_alignment (GTK_MISC (bubble->bubble_body_label), 0.5, 0.5); gtk_widget_show (bubble->bubble_header_label); gtk_widget_show (bubble->bubble_body_label); bubble->main_hbox = gtk_hbox_new (FALSE, 10); gtk_container_add (GTK_CONTAINER (bubble->main_hbox), bubble->bubble_body_label); vbox = gtk_vbox_new (FALSE, 5); gtk_container_add (GTK_CONTAINER (vbox), bubble->bubble_header_label); gtk_container_add (GTK_CONTAINER (vbox), bubble->main_hbox); gtk_container_add (GTK_CONTAINER (bubble->bubble_window), vbox); g_signal_connect (bubble->bubble_window, "destroy", G_CALLBACK (gtk_widget_destroyed), &bubble->bubble_window); g_signal_connect_after (bubble->bubble_window, "event-after", G_CALLBACK (egg_notification_bubble_event_handler), bubble); } } void egg_notification_bubble_attach (EggNotificationBubble *bubble, GtkWidget *widget) { bubble->widget = widget; g_signal_connect_object (widget, "destroy", G_CALLBACK (g_object_unref), bubble, G_CONNECT_SWAPPED); } void egg_notification_bubble_set (EggNotificationBubble *bubble, const gchar *bubble_header_text, GtkWidget *icon, const gchar *bubble_body_text) { g_return_if_fail (EGG_IS_NOTIFICATION_BUBBLE (bubble)); g_free (bubble->bubble_header_text); g_free (bubble->bubble_body_text); if (bubble->icon) { if (bubble->active) gtk_container_remove (GTK_CONTAINER (bubble->main_hbox), bubble->icon); g_object_unref (G_OBJECT (bubble->icon)); bubble->icon = NULL; } bubble->bubble_header_text = g_strdup (bubble_header_text); bubble->bubble_body_text = g_strdup (bubble_body_text); if (icon) bubble->icon = g_object_ref (G_OBJECT (icon)); } static gint egg_notification_bubble_paint_window (EggNotificationBubble *bubble) { GtkRequisition req; gtk_widget_size_request (bubble->bubble_window, &req); gtk_paint_flat_box (bubble->bubble_window->style, bubble->bubble_window->window, GTK_STATE_NORMAL, GTK_SHADOW_OUT, NULL, GTK_WIDGET (bubble->bubble_window), "notification", 0, 0, req.width, req.height); return FALSE; } static void subtract_rectangle (GdkRegion *region, GdkRectangle *rectangle) { GdkRegion *temp_region; temp_region = gdk_region_rectangle (rectangle); gdk_region_subtract (region, temp_region); gdk_region_destroy (temp_region); } static GdkRegion * add_bevels_to_rectangle (GdkRectangle *rectangle) { GdkRectangle temp_rect; GdkRegion *region = gdk_region_rectangle (rectangle); temp_rect.width = 5; temp_rect.height = 1; /* Top left */ temp_rect.x = rectangle->x; temp_rect.y = rectangle->y; subtract_rectangle (region, &temp_rect); temp_rect.y += 1; temp_rect.width -= 2; subtract_rectangle (region, &temp_rect); temp_rect.y += 1; temp_rect.width -= 1; subtract_rectangle (region, &temp_rect); temp_rect.y += 1; temp_rect.width -= 1; temp_rect.height = 2; subtract_rectangle (region, &temp_rect); /* Top right */ temp_rect.width = 5; temp_rect.height = 1; temp_rect.x = (rectangle->x + rectangle->width) - temp_rect.width; temp_rect.y = rectangle->y; subtract_rectangle (region, &temp_rect); temp_rect.y += 1; temp_rect.x += 2; subtract_rectangle (region, &temp_rect); temp_rect.y += 1; temp_rect.x += 1; subtract_rectangle (region, &temp_rect); temp_rect.y += 1; temp_rect.x += 1; temp_rect.height = 2; subtract_rectangle (region, &temp_rect); /* Bottom right */ temp_rect.width = 5; temp_rect.height = 1; temp_rect.x = (rectangle->x + rectangle->width) - temp_rect.width; temp_rect.y = (rectangle->y + rectangle->height) - temp_rect.height; subtract_rectangle (region, &temp_rect); temp_rect.y -= 1; temp_rect.x += 2; subtract_rectangle (region, &temp_rect); temp_rect.y -= 1; temp_rect.x += 1; subtract_rectangle (region, &temp_rect); temp_rect.y -= 1; temp_rect.x += 1; temp_rect.height = 2; subtract_rectangle (region, &temp_rect); /* Bottom left */ temp_rect.width = 5; temp_rect.height = 1; temp_rect.x = rectangle->x; temp_rect.y = rectangle->y + rectangle->height; subtract_rectangle (region, &temp_rect); temp_rect.y -= 1; temp_rect.width -= 2; subtract_rectangle (region, &temp_rect); temp_rect.y -= 1; temp_rect.width -= 1; subtract_rectangle (region, &temp_rect); temp_rect.y -= 1; temp_rect.width -= 1; temp_rect.height = 2; subtract_rectangle (region, &temp_rect); return region; } static gboolean idle_notification_expired (gpointer data) { EggNotificationBubble *bubble = data; GDK_THREADS_ENTER (); g_signal_emit (bubble, egg_notification_bubble_signals[NOTIFICATION_TIMEOUT], 0); egg_notification_bubble_hide (bubble); GDK_THREADS_LEAVE (); return FALSE; } static void draw_bubble (EggNotificationBubble *bubble, guint timeout) { GtkRequisition requisition; GtkWidget *widget; GtkStyle *style; gint x, y, w, h; GdkScreen *screen; gint monitor_num; GdkRectangle monitor; GdkPoint triangle_points[3]; char *markuptext; char *markupquoted; GdkRectangle rectangle; GdkRegion *region; GdkRegion *triangle_region; enum { ORIENT_TOP = 0, ORIENT_BOTTOM = 1 } orient; guint rectangle_border; guint triangle_offset; if (!bubble->bubble_window) force_window (bubble); gtk_widget_ensure_style (bubble->bubble_window); style = bubble->bubble_window->style; widget = bubble->widget; screen = gtk_widget_get_screen (widget); if (bubble->icon) { gtk_box_pack_start_defaults (GTK_BOX (bubble->main_hbox), bubble->icon); gtk_box_reorder_child (GTK_BOX (bubble->main_hbox), bubble->icon, 0); } markupquoted = g_markup_escape_text (bubble->bubble_header_text, -1); markuptext = g_strdup_printf ("%s", markupquoted); gtk_label_set_markup (GTK_LABEL (bubble->bubble_header_label), markuptext); g_free (markuptext); g_free (markupquoted); gtk_label_set_text (GTK_LABEL (bubble->bubble_body_label), bubble->bubble_body_text); gtk_window_move (GTK_WINDOW (bubble->bubble_window), 0, 2 * gdk_screen_get_height (screen)); gtk_widget_show_all (bubble->bubble_window); gtk_widget_size_request (bubble->bubble_window, &requisition); w = requisition.width; h = requisition.height; gdk_window_get_origin (widget->window, &x, &y); if (GTK_WIDGET_NO_WINDOW (widget)) { x += widget->allocation.x; y += widget->allocation.y; } orient = ORIENT_BOTTOM; triangle_offset = 20; x -= triangle_offset; monitor_num = gdk_screen_get_monitor_at_window (screen, widget->window); gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor); if ((x + w) > monitor.x + monitor.width) { gint offset = (x + w) - (monitor.x + monitor.width); triangle_offset += offset; x -= offset; } else if (x < monitor.x) { gint offset = monitor.x - x; triangle_offset -= offset; x += offset; } if ((y + h + widget->allocation.height + 4) > monitor.y + monitor.height) { y -= (h - 4); orient = ORIENT_TOP; } else y = y + widget->allocation.height + 4; /* Overlap the arrow with the object slightly */ if (orient == ORIENT_BOTTOM) y -= 5; else y += 5; rectangle_border = BORDER_SIZE-2; rectangle.x = rectangle_border; rectangle.y = rectangle_border; rectangle.width = w - (rectangle_border * 2); rectangle.height = h - (rectangle_border * 2); region = add_bevels_to_rectangle (&rectangle); triangle_points[0].x = triangle_offset; triangle_points[0].y = orient == ORIENT_BOTTOM ? BORDER_SIZE : h - BORDER_SIZE; triangle_points[1].x = triangle_points[0].x + 20; triangle_points[1].y = triangle_points[0].y; triangle_points[2].x = (triangle_points[1].x + triangle_points[0].x) /2; triangle_points[2].y = orient == ORIENT_BOTTOM ? 0 : h; triangle_region = gdk_region_polygon (triangle_points, 3, GDK_WINDING_RULE); gdk_region_union (region, triangle_region); gdk_region_destroy (triangle_region); gdk_window_shape_combine_region (bubble->bubble_window->window, region, 0, 0); gtk_window_move (GTK_WINDOW (bubble->bubble_window), x, y); bubble->active = TRUE; if (bubble->timeout_id) { g_source_remove (bubble->timeout_id); bubble->timeout_id = 0; } if (timeout > 0) bubble->timeout_id = g_timeout_add (timeout, idle_notification_expired, bubble); } void egg_notification_bubble_show (EggNotificationBubble *bubble, guint timeout) { draw_bubble (bubble, timeout); } void egg_notification_bubble_hide (EggNotificationBubble *bubble) { if (bubble->bubble_window) gtk_widget_hide (bubble->bubble_window); if (bubble->timeout_id) { g_source_remove (bubble->timeout_id); bubble->timeout_id = 0; } } EggNotificationBubble* egg_notification_bubble_new (void) { return g_object_new (EGG_TYPE_NOTIFICATION_BUBBLE, NULL); } static void egg_notification_bubble_event_handler (GtkWidget *widget, GdkEvent *event, gpointer user_data) { EggNotificationBubble *bubble; bubble = EGG_NOTIFICATION_BUBBLE (user_data); switch (event->type) { case GDK_BUTTON_PRESS: g_signal_emit (bubble, egg_notification_bubble_signals[NOTIFICATION_CLICKED], 0); break; default: break; } } static void egg_notification_bubble_detach (EggNotificationBubble *bubble) { g_return_if_fail (bubble->widget); g_object_unref (bubble->widget); }