mail-notification

Fork of Jean-Yves Lefort's mail-notification, a tray icon to notify of new mail
git clone https://code.djc.id.au/git/mail-notification/

src/mn-tooltips.gob (16645B) - raw

      1 /*
      2  * MNTooltips - a tooltips implementation allowing to use an arbitrary
      3  * widget as tooltip. Update: this functionality is now supported by
      4  * GTK+ (as of version 2.12), but unfortunately it is broken
      5  * (http://bugzilla.gnome.org/show_bug.cgi?id=504087).
      6  *
      7  * Heavily based on GtkTooltips,
      8  * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
      9  *
     10  * Mail Notification
     11  * Copyright (C) 2003-2008 Jean-Yves Lefort <jylefort@brutele.be>
     12  *
     13  * This program is free software; you can redistribute it and/or modify
     14  * it under the terms of the GNU General Public License as published by
     15  * the Free Software Foundation; either version 3 of the License, or
     16  * (at your option) any later version.
     17  *
     18  * This program is distributed in the hope that it will be useful,
     19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     21  * GNU General Public License for more details.
     22  *
     23  * You should have received a copy of the GNU General Public License along
     24  * with this program; if not, write to the Free Software Foundation, Inc.,
     25  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
     26  */
     27 
     28 %headertop{
     29 #include <gtk/gtk.h>
     30 %}
     31 
     32 %privateheader{
     33 typedef struct
     34 {
     35   MNTooltips	*self;
     36   GtkWidget	*widget;
     37   GtkWidget	*tip_widget;
     38 } TooltipsData;
     39 %}
     40 
     41 %{
     42 #include "mn-util.h"
     43 
     44 #define TOOLTIPS_DATA			"mn-tooltips-data"
     45 #define TOOLTIPS_INFO			"mn-tooltips-info"
     46 #define TOOLTIPS_KEYBOARD_MODE		"gtk-tooltips-keyboard-mode" /* compatible with GtkTooltips */
     47 
     48 #define DELAY 500			/* Default delay in ms */
     49 #define STICKY_DELAY 0			/* Delay before popping up next tip
     50 					 * if we're sticky
     51 					 */
     52 #define STICKY_REVERT_DELAY 1000	/* Delay before sticky tooltips revert
     53 					 * to normal
     54 					 */
     55 
     56 /* The private flags that are used in the private_flags member of GtkWidget.
     57  */
     58 typedef enum
     59 {
     60   PRIVATE_GTK_LEAVE_PENDING	= 1 <<  4
     61 } GtkPrivateFlags;
     62 
     63 /* Macros for extracting a widgets private_flags from GtkWidget.
     64  */
     65 #define GTK_PRIVATE_FLAGS(wid)            (GTK_WIDGET (wid)->private_flags)
     66 
     67 /* Macros for setting and clearing private widget flags.
     68  * we use a preprocessor string concatenation here for a clear
     69  * flags/private_flags distinction at the cost of single flag operations.
     70  */
     71 #define GTK_PRIVATE_SET_FLAG(wid,flag)    G_STMT_START{ (GTK_PRIVATE_FLAGS (wid) |= (PRIVATE_ ## flag)); }G_STMT_END
     72 %}
     73 
     74 class MN:Tooltips from G:Object
     75 {
     76   private GtkWidget *window;
     77   private TooltipsData *active_data;
     78   private GSList *data_list;
     79 
     80   private gboolean use_sticky_delay;
     81   private GTimeVal last_popdown;
     82   private unsigned int timeout_id;
     83 
     84   private int border_width = 4;
     85 
     86   finalize (self)
     87   {
     88     GSList *l;
     89 
     90     if (selfp->timeout_id)
     91       g_source_remove(selfp->timeout_id);
     92 
     93     MN_LIST_FOREACH(l, selfp->data_list)
     94       {
     95 	TooltipsData *data = l->data;
     96 	self_widget_remove(data->widget, data);
     97       }
     98 
     99     self_unset_window(self);
    100   }
    101 
    102   private void
    103     destroy_data (TooltipsData *data)
    104   {
    105     g_object_disconnect(data->widget,
    106 			"any-signal", self_event_after_h, data,
    107 			"any-signal", self_widget_unmap, data,
    108 			"any-signal", self_widget_remove, data,
    109 			NULL);
    110 
    111     g_object_set_data(G_OBJECT(data->widget), TOOLTIPS_DATA, NULL);
    112     g_object_unref(data->widget);
    113     g_object_unref(data->tip_widget);
    114     g_free(data);
    115   }
    116 
    117   private void
    118     display_closed_h (GdkDisplay *display,
    119 		      gboolean is_error,
    120 		      gpointer user_data)
    121   {
    122     Self *self = SELF(user_data);
    123     self_unset_window(self);
    124   }
    125 
    126   private void
    127     disconnect_display_closed (self)
    128   {
    129     g_signal_handlers_disconnect_by_func(gtk_widget_get_display(selfp->window),
    130 					 self_display_closed_h,
    131 					 self);
    132   }
    133 
    134   private void
    135     unset_window (self)
    136   {
    137     if (selfp->window)
    138       {
    139 	self_disconnect_display_closed(self);
    140 	gtk_widget_destroy(selfp->window);
    141       }
    142   }
    143 
    144   private void
    145     update_screen (self, gboolean new_window)
    146   {
    147     gboolean screen_changed = FALSE;
    148 
    149     if (selfp->active_data && selfp->active_data->widget)
    150       {
    151 	GdkScreen *screen = gtk_widget_get_screen(selfp->active_data->widget);
    152 
    153 	screen_changed = (screen != gtk_widget_get_screen(selfp->window));
    154 
    155 	if (screen_changed)
    156 	  {
    157 	    if (! new_window)
    158 	      self_disconnect_display_closed(self);
    159 
    160 	    gtk_window_set_screen(GTK_WINDOW(selfp->window), screen);
    161 	  }
    162       }
    163 
    164     if (screen_changed || new_window)
    165       g_signal_connect(gtk_widget_get_display(selfp->window),
    166 		       "closed",
    167 		       G_CALLBACK(self_display_closed_h),
    168 		       self);
    169   }
    170 
    171   private void
    172     force_window (self)
    173   {
    174     if (! selfp->window)
    175       {
    176 	selfp->window = gtk_window_new(GTK_WINDOW_POPUP);
    177 	self_update_screen(self, TRUE);
    178 	gtk_widget_set_app_paintable(selfp->window, TRUE);
    179 	gtk_window_set_resizable(GTK_WINDOW(selfp->window), FALSE);
    180 	gtk_widget_set_name(selfp->window, "gtk-tooltips");
    181 	gtk_container_set_border_width(GTK_CONTAINER(selfp->window), selfp->border_width);
    182 
    183 	g_signal_connect_swapped(selfp->window,
    184 				 "expose-event",
    185 				 G_CALLBACK(self_paint_window),
    186 				 self);
    187 
    188 	mn_add_weak_pointer(&selfp->window);
    189       }
    190   }
    191 
    192   private TooltipsData *
    193     get_data (Gtk:Widget *widget (check null type))
    194   {
    195     return g_object_get_data(G_OBJECT(widget), TOOLTIPS_DATA);
    196   }
    197 
    198   private void
    199     set_tip_widget_real (self,
    200 			 Gtk:Widget *widget (check null type),
    201 			 Gtk:Widget *tip_widget,
    202 			 int border_width)
    203   {
    204     TooltipsData *data;
    205 
    206     data = self_get_data(widget);
    207 
    208     if (! tip_widget)
    209       {
    210 	if (data)
    211 	  self_widget_remove(data->widget, data);
    212 	return;
    213       }
    214 
    215     if (selfp->active_data
    216 	&& selfp->active_data->widget == widget
    217 	&& GTK_WIDGET_DRAWABLE(selfp->active_data->widget))
    218       {
    219 	if (data->tip_widget)
    220 	  g_object_unref(data->tip_widget);
    221 
    222 	data->tip_widget = tip_widget;
    223 
    224 	if (data->tip_widget)
    225 	  g_object_ref_sink(data->tip_widget);
    226 
    227 	self_draw_tips(self);
    228       }
    229     else
    230       {
    231 	g_object_ref(widget);
    232 
    233 	if (data)
    234 	  self_widget_remove(data->widget, data);
    235 
    236 	data = g_new0(TooltipsData, 1);
    237 	data->self = self;
    238 	data->widget = widget;
    239 	data->tip_widget = tip_widget;
    240 
    241 	if (data->tip_widget)
    242 	  g_object_ref_sink(data->tip_widget);
    243 
    244 	selfp->data_list = g_slist_append(selfp->data_list, data);
    245 	g_signal_connect_after(widget, "event-after", G_CALLBACK(self_event_after_h), data);
    246 
    247 	g_object_set_data(G_OBJECT(widget), TOOLTIPS_DATA, data);
    248 
    249 	g_object_connect(widget,
    250 			 "signal::unmap", self_widget_unmap, data,
    251 			 "signal::unrealize", self_widget_unmap, data,
    252 			 "signal::destroy", self_widget_remove, data,
    253 			 NULL);
    254       }
    255 
    256     selfp->border_width = border_width;
    257     if (selfp->window)
    258       gtk_container_set_border_width(GTK_CONTAINER(selfp->window), border_width);
    259   }
    260 
    261   public void
    262     set_tip (self,
    263 	     Gtk:Widget *widget (check null type),
    264 	     const char *tip_text)
    265   {
    266     GtkWidget *label = NULL;
    267 
    268     if (tip_text)
    269       {
    270 	label = gtk_label_new(tip_text);
    271 	gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
    272 	gtk_misc_set_alignment(GTK_MISC(label), 0.5, 0.5);
    273 	gtk_widget_show(label);
    274       }
    275 
    276     self_set_tip_widget_real(self, widget, label, 4);
    277   }
    278 
    279   public void
    280     set_tip_widget (self,
    281 		    Gtk:Widget *widget (check null type),
    282 		    Gtk:Widget *tip_widget)
    283   {
    284     self_set_tip_widget_real(self, widget, tip_widget, 12);
    285   }
    286 
    287   private gboolean
    288     paint_window (self)
    289   {
    290     GtkRequisition req;
    291 
    292     gtk_widget_size_request(selfp->window, &req);
    293     gtk_paint_flat_box(selfp->window->style,
    294 		       selfp->window->window,
    295 		       GTK_STATE_NORMAL,
    296 		       GTK_SHADOW_OUT,
    297 		       NULL,
    298 		       selfp->window,
    299 		       "tooltip",
    300 		       0,
    301 		       0,
    302 		       req.width,
    303 		       req.height);
    304 
    305     return FALSE;
    306   }
    307 
    308   private void
    309     draw_tips (self)
    310   {
    311     GtkRequisition requisition;
    312     GtkWidget *widget;
    313     gint x, y, w, h;
    314     TooltipsData *data;
    315     GtkWidget *child;
    316     gboolean keyboard_mode;
    317     GdkScreen *screen;
    318     GdkScreen *pointer_screen;
    319     gint monitor_num, px, py;
    320     GdkRectangle monitor;
    321     int screen_width;
    322 
    323     if (! selfp->window)
    324       self_force_window(self);
    325     else if (GTK_WIDGET_VISIBLE(selfp->window))
    326       g_get_current_time(&selfp->last_popdown);
    327 
    328     gtk_widget_ensure_style(selfp->window);
    329 
    330     widget = selfp->active_data->widget;
    331     g_object_set_data(G_OBJECT(selfp->window), TOOLTIPS_INFO, self);
    332 
    333     keyboard_mode = self_get_keyboard_mode(widget);
    334 
    335     self_update_screen(self, FALSE);
    336 
    337     screen = gtk_widget_get_screen(widget);
    338 
    339     data = selfp->active_data;
    340 
    341     child = GTK_BIN(selfp->window)->child;
    342     if (child)
    343       gtk_container_remove(GTK_CONTAINER(selfp->window), child);
    344 
    345     if (data->tip_widget)
    346       {
    347 	gtk_container_add(GTK_CONTAINER(selfp->window), data->tip_widget);
    348 	gtk_widget_show(data->tip_widget);
    349       }
    350 
    351     gtk_widget_size_request(selfp->window, &requisition);
    352     w = requisition.width;
    353     h = requisition.height;
    354 
    355     gdk_window_get_origin(widget->window, &x, &y);
    356     if (GTK_WIDGET_NO_WINDOW(widget))
    357       {
    358 	x += widget->allocation.x;
    359 	y += widget->allocation.y;
    360       }
    361 
    362     x += widget->allocation.width / 2;
    363 
    364     if (! keyboard_mode)
    365       gdk_window_get_pointer(gdk_screen_get_root_window(screen), &x, NULL, NULL);
    366 
    367     x -= (w / 2 + 4);
    368 
    369     gdk_display_get_pointer(gdk_screen_get_display(screen), &pointer_screen, &px, &py, NULL);
    370     if (pointer_screen != screen)
    371       {
    372 	px = x;
    373 	py = y;
    374       }
    375     monitor_num = gdk_screen_get_monitor_at_point(screen, px, py);
    376     gdk_screen_get_monitor_geometry(screen, monitor_num, &monitor);
    377 
    378     if ((x + w) > monitor.x + monitor.width)
    379       x -= (x + w) - (monitor.x + monitor.width);
    380     else if (x < monitor.x)
    381       x = monitor.x;
    382 
    383     if ((y + h + widget->allocation.height + 4) > monitor.y + monitor.height
    384 	&& (y - 4) > monitor.y)
    385       y = y - h - 4;
    386     else
    387       y = y + widget->allocation.height + 4;
    388 
    389     /*
    390      * The following block is not part of GTK+ and has been added to
    391      * make sure that the tooltip will not go beyond the screen edges
    392      * (horizontally).
    393      */
    394     screen_width = gdk_screen_get_width(screen);
    395     if (x < 0 || x + w > screen_width)
    396       {
    397 	x = 0;
    398 	gtk_widget_set_size_request(selfp->window, MIN(w, screen_width), -1);
    399       }
    400 
    401     /*
    402      * The following block ensures that the top of the tooltip is
    403      * visible, but it corrupts the tip widget (the mail summary is
    404      * not properly positioned). A fix is welcome.
    405      */
    406 /*
    407     if (y < 0)
    408       {
    409 	gtk_widget_set_size_request(selfp->window, -1, y + h);
    410 	y = 0;
    411       }
    412 */
    413 
    414     gtk_window_move(GTK_WINDOW(selfp->window), x, y);
    415     gtk_widget_show(selfp->window);
    416   }
    417 
    418   private gboolean
    419     timeout_cb (gpointer data)
    420   {
    421     Self *self = SELF(data);
    422 
    423     if (selfp->active_data && GTK_WIDGET_DRAWABLE(selfp->active_data->widget))
    424       self_draw_tips(self);
    425 
    426     selfp->timeout_id = 0;
    427     return FALSE;		/* remove timeout */
    428   }
    429 
    430   private void
    431     set_active_widget (self, Gtk:Widget *widget)
    432   {
    433     if (selfp->window)
    434       {
    435 	if (GTK_WIDGET_VISIBLE(selfp->window))
    436 	  g_get_current_time(&selfp->last_popdown);
    437 	gtk_widget_hide(selfp->window);
    438       }
    439 
    440     mn_source_clear(&selfp->timeout_id);
    441 
    442     selfp->active_data = NULL;
    443 
    444     if (widget)
    445       {
    446 	GSList *l;
    447 
    448 	MN_LIST_FOREACH(l, selfp->data_list)
    449 	{
    450 	  TooltipsData *data = l->data;
    451 
    452 	  if (data->widget == widget && GTK_WIDGET_DRAWABLE(widget))
    453 	    {
    454 	      selfp->active_data = data;
    455 	      break;
    456 	    }
    457 	}
    458       }
    459     else
    460       selfp->use_sticky_delay = FALSE;
    461   }
    462 
    463   private void
    464     show_tip (Gtk:Widget *widget (check null type))
    465   {
    466     TooltipsData *data;
    467 
    468     data = self_get_data(widget);
    469 
    470     if (data &&
    471 	(! data->self->_priv->active_data ||
    472 	 data->self->_priv->active_data->widget != widget))
    473       {
    474 	self_set_active_widget(data->self, widget);
    475 	self_draw_tips(data->self);
    476       }
    477   }
    478 
    479   private void
    480     hide_tip (Gtk:Widget *widget (check null type))
    481   {
    482     TooltipsData *data;
    483 
    484     data = self_get_data(widget);
    485 
    486     if (data &&
    487 	(data->self->_priv->active_data &&
    488 	 data->self->_priv->active_data->widget == widget))
    489       self_set_active_widget(data->self, NULL);
    490   }
    491 
    492   private gboolean
    493     recently_shown (self)
    494   {
    495     GTimeVal now;
    496     glong msec;
    497 
    498     g_get_current_time (&now);
    499     msec = (now.tv_sec - selfp->last_popdown.tv_sec) * 1000 +
    500       (now.tv_usec - selfp->last_popdown.tv_usec) / 1000;
    501     return (msec < STICKY_REVERT_DELAY);
    502   }
    503 
    504   private gboolean
    505     get_keyboard_mode (Gtk:Widget *widget (check null type))
    506   {
    507     GtkWidget *toplevel = gtk_widget_get_toplevel(widget);
    508 
    509     if (GTK_IS_WINDOW(toplevel))
    510       return GPOINTER_TO_INT(g_object_get_data(G_OBJECT(toplevel), TOOLTIPS_KEYBOARD_MODE));
    511     else
    512       return FALSE;
    513   }
    514 
    515   private void
    516     start_keyboard_mode (Gtk:Widget *widget (check null type))
    517   {
    518     GtkWidget *toplevel = gtk_widget_get_toplevel(widget);
    519 
    520     if (GTK_IS_WINDOW(toplevel))
    521       {
    522 	GtkWidget *focus = GTK_WINDOW(toplevel)->focus_widget;
    523 
    524 	g_object_set_data(G_OBJECT(toplevel), TOOLTIPS_KEYBOARD_MODE, GINT_TO_POINTER(TRUE));
    525 
    526 	if (focus)
    527 	  self_show_tip(focus);
    528       }
    529   }
    530 
    531   private void
    532     stop_keyboard_mode (Gtk:Widget *widget (check null type))
    533   {
    534     GtkWidget *toplevel = gtk_widget_get_toplevel(widget);
    535 
    536     if (GTK_IS_WINDOW(toplevel))
    537       {
    538 	GtkWidget *focus = GTK_WINDOW(toplevel)->focus_widget;
    539 
    540 	if (focus)
    541 	  self_hide_tip(focus);
    542 
    543 	g_object_set_data(G_OBJECT(toplevel), TOOLTIPS_KEYBOARD_MODE, GINT_TO_POINTER(FALSE));
    544       }
    545   }
    546 
    547   private void
    548     start_delay (self, Gtk:Widget *widget)
    549   {
    550     TooltipsData *old_data;
    551 
    552     old_data = selfp->active_data;
    553     if (! old_data || old_data->widget != widget)
    554       {
    555 	self_set_active_widget(self, widget);
    556 	selfp->timeout_id = gdk_threads_add_timeout((selfp->use_sticky_delay && self_recently_shown(self)) ? STICKY_DELAY : DELAY,
    557 						    self_timeout_cb,
    558 						    self);
    559       }
    560   }
    561 
    562   private void
    563     event_after_h (GtkWidget *widget, GdkEvent *event, gpointer user_data)
    564   {
    565     Self *self;
    566     TooltipsData *old_data;
    567     GtkWidget *event_widget;
    568     gboolean keyboard_mode = self_get_keyboard_mode(widget);
    569 
    570     if ((event->type == GDK_LEAVE_NOTIFY || event->type == GDK_ENTER_NOTIFY) &&
    571 	event->crossing.detail == GDK_NOTIFY_INFERIOR)
    572       return;
    573 
    574     old_data = self_get_data(widget);
    575     self = old_data->self;
    576 
    577     if (keyboard_mode)
    578       {
    579 	switch (event->type)
    580 	  {
    581 	  case GDK_FOCUS_CHANGE:
    582 	    if (event->focus_change.in)
    583 	      self_show_tip(widget);
    584 	    else
    585 	      self_hide_tip(widget);
    586 	    break;
    587 
    588 	  default:
    589 	    break;
    590 	  }
    591       }
    592     else
    593       {
    594 	if (event->type != GDK_KEY_PRESS && event->type != GDK_KEY_RELEASE)
    595 	  {
    596 	    event_widget = gtk_get_event_widget(event);
    597 	    if (event_widget != widget)
    598 	      return;
    599 	  }
    600 
    601 	switch (event->type)
    602 	  {
    603 	  case GDK_EXPOSE:
    604 	    /* do nothing */
    605 	    break;
    606 
    607 	  case GDK_ENTER_NOTIFY:
    608 	    if (! (GTK_IS_MENU_ITEM(widget) && GTK_MENU_ITEM(widget)->submenu))
    609 	      self_start_delay(self, widget);
    610 	    break;
    611 
    612 	  case GDK_LEAVE_NOTIFY:
    613 	    self_set_active_widget(self, NULL);
    614 	    selfp->use_sticky_delay = selfp->window && GTK_WIDGET_VISIBLE(selfp->window);
    615 	    break;
    616 
    617 	  case GDK_MOTION_NOTIFY:
    618 	    /* Handle menu items specially ... pend popup for each motion
    619 	     * on other widgets, we ignore motion.
    620 	     */
    621 	    if (GTK_IS_MENU_ITEM(widget) && ! GTK_MENU_ITEM(widget)->submenu)
    622 	      {
    623 		/* Completely evil hack to make sure we get the LEAVE_NOTIFY
    624 		 */
    625 		GTK_PRIVATE_SET_FLAG(widget, GTK_LEAVE_PENDING);
    626 		self_set_active_widget(self, NULL);
    627 		self_start_delay(self, widget);
    628 		break;
    629 	      }
    630 	    break;		/* ignore */
    631 
    632 	  case GDK_BUTTON_PRESS:
    633 	  case GDK_BUTTON_RELEASE:
    634 	  case GDK_KEY_PRESS:
    635 	  case GDK_KEY_RELEASE:
    636 	  case GDK_PROXIMITY_IN:
    637 	  case GDK_SCROLL:
    638 	    self_set_active_widget(self, NULL);
    639 	    break;
    640 
    641 	  default:
    642 	    break;
    643 	  }
    644       }
    645   }
    646 
    647   private void
    648     widget_unmap (Gtk:Widget *widget (check null type), gpointer user_data)
    649   {
    650     TooltipsData *data = user_data;
    651     Self *self = data->self;
    652 
    653     if (selfp->active_data &&
    654 	(selfp->active_data->widget == widget))
    655       self_set_active_widget(self, NULL);
    656   }
    657 
    658   private void
    659     widget_remove (Gtk:Widget *widget (check null type), gpointer user_data)
    660   {
    661     TooltipsData *data = user_data;
    662     Self *self = data->self;
    663 
    664     self_widget_unmap(widget, user_data);
    665     selfp->data_list = g_slist_remove(selfp->data_list, data);
    666     self_destroy_data(data);
    667   }
    668 
    669   public void
    670     toggle_keyboard_mode (Gtk:Widget *widget (check null type))
    671   {
    672     if (self_get_keyboard_mode(widget))
    673       self_stop_keyboard_mode(widget);
    674     else
    675       self_start_keyboard_mode(widget);
    676   }
    677 
    678   public MNTooltips *
    679     new (void)
    680   {
    681     return GET_NEW;
    682   }
    683 }