1 /* LIBGIMP - The GIMP Library
2  * Copyright (C) 1995 Spencer Kimball and Peter Mattis
3  *
4  * gimphelpui.c
5  * Copyright (C) 2000-2003 Michael Natterer <mitch@gimp.org>
6  *
7  * This library is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 3 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library.  If not, see
19  * <https://www.gnu.org/licenses/>.
20  */
21 
22 #include "config.h"
23 
24 #include <gegl.h>
25 #include <gtk/gtk.h>
26 #include <gdk/gdkkeysyms.h>
27 
28 #include "gimpwidgets.h"
29 #include "gimpwidgets-private.h"
30 
31 #include "libgimp/libgimp-intl.h"
32 
33 
34 /**
35  * SECTION: gimphelpui
36  * @title: GimpHelpUI
37  * @short_description: Functions for setting tooltip and help identifier
38  *                     used by the GIMP help system.
39  *
40  * Functions for setting tooltip and help identifier used by the GIMP
41  * help system.
42  **/
43 
44 
45 typedef enum
46 {
47   GIMP_WIDGET_HELP_TOOLTIP    = GTK_WIDGET_HELP_TOOLTIP,
48   GIMP_WIDGET_HELP_WHATS_THIS = GTK_WIDGET_HELP_WHATS_THIS,
49   GIMP_WIDGET_HELP_TYPE_HELP  = 0xff
50 } GimpWidgetHelpType;
51 
52 
53 /*  local variables  */
54 
55 static gboolean tooltips_enabled       = TRUE;
56 static gboolean tooltips_enable_called = FALSE;
57 
58 
59 /*  local function prototypes  */
60 
61 static const gchar * gimp_help_get_help_data        (GtkWidget      *widget,
62                                                      GtkWidget     **help_widget,
63                                                      gpointer       *ret_data);
64 static gboolean   gimp_help_callback                (GtkWidget      *widget,
65                                                      GimpWidgetHelpType help_type,
66                                                      GimpHelpFunc    help_func);
67 
68 static void       gimp_help_menu_item_set_tooltip   (GtkWidget      *widget,
69                                                      const gchar    *tooltip,
70                                                      const gchar    *help_id);
71 static gboolean   gimp_help_menu_item_query_tooltip (GtkWidget      *widget,
72                                                      gint            x,
73                                                      gint            y,
74                                                      gboolean        keyboard_mode,
75                                                      GtkTooltip     *tooltip);
76 static gboolean   gimp_context_help_idle_start      (gpointer        widget);
77 static gboolean   gimp_context_help_button_press    (GtkWidget      *widget,
78                                                      GdkEventButton *bevent,
79                                                      gpointer        data);
80 static gboolean   gimp_context_help_key_press       (GtkWidget      *widget,
81                                                      GdkEventKey    *kevent,
82                                                      gpointer        data);
83 static gboolean   gimp_context_help_idle_show_help  (gpointer        data);
84 
85 
86 /*  public functions  */
87 
88 /**
89  * gimp_help_enable_tooltips:
90  *
91  * Enable tooltips to be shown in the GIMP user interface.
92  *
93  * As a plug-in author, you don't need to care about this as this
94  * function is called for you from gimp_ui_init(). This ensures that
95  * the user setting from the GIMP preferences dialog is respected in
96  * all plug-in dialogs.
97  **/
98 void
gimp_help_enable_tooltips(void)99 gimp_help_enable_tooltips (void)
100 {
101   if (! tooltips_enable_called)
102     {
103       tooltips_enable_called = TRUE;
104       tooltips_enabled       = TRUE;
105     }
106 }
107 
108 /**
109  * gimp_help_disable_tooltips:
110  *
111  * Disable tooltips to be shown in the GIMP user interface.
112  *
113  * As a plug-in author, you don't need to care about this as this
114  * function is called for you from gimp_ui_init(). This ensures that
115  * the user setting from the GIMP preferences dialog is respected in
116  * all plug-in dialogs.
117  **/
118 void
gimp_help_disable_tooltips(void)119 gimp_help_disable_tooltips (void)
120 {
121   if (! tooltips_enable_called)
122     {
123       tooltips_enable_called = TRUE;
124       tooltips_enabled       = FALSE;
125     }
126 }
127 
128 /**
129  * gimp_standard_help_func:
130  * @help_id:   A unique help identifier.
131  * @help_data: The @help_data passed to gimp_help_connect().
132  *
133  * This is the standard GIMP help function which does nothing but calling
134  * gimp_help(). It is the right function to use in almost all cases.
135  **/
136 void
gimp_standard_help_func(const gchar * help_id,gpointer help_data)137 gimp_standard_help_func (const gchar *help_id,
138                          gpointer     help_data)
139 {
140   if (! _gimp_standard_help_func)
141     {
142       g_warning ("%s: you must call gimp_widgets_init() before using "
143                  "the help system", G_STRFUNC);
144       return;
145     }
146 
147   (* _gimp_standard_help_func) (help_id, help_data);
148 }
149 
150 /**
151  * gimp_help_connect:
152  * @widget: The widget you want to connect the help accelerator for. Will
153  *          be a #GtkWindow in most cases.
154  * @help_func: The function which will be called if the user presses "F1".
155  * @help_id:   The @help_id which will be passed to @help_func.
156  * @help_data: The @help_data pointer which will be passed to @help_func.
157  *
158  * Note that this function is automatically called by all libgimp dialog
159  * constructors. You only have to call it for windows/dialogs you created
160  * "manually".
161  **/
162 void
gimp_help_connect(GtkWidget * widget,GimpHelpFunc help_func,const gchar * help_id,gpointer help_data)163 gimp_help_connect (GtkWidget    *widget,
164                    GimpHelpFunc  help_func,
165                    const gchar  *help_id,
166                    gpointer      help_data)
167 {
168   static gboolean initialized = FALSE;
169 
170   g_return_if_fail (GTK_IS_WIDGET (widget));
171   g_return_if_fail (help_func != NULL);
172 
173   /*  set up the help signals
174    */
175   if (! initialized)
176     {
177       GtkBindingSet *binding_set;
178 
179       binding_set =
180         gtk_binding_set_by_class (g_type_class_peek (GTK_TYPE_WIDGET));
181 
182       gtk_binding_entry_add_signal (binding_set, GDK_KEY_F1, 0,
183                                     "show-help", 1,
184                                     GTK_TYPE_WIDGET_HELP_TYPE,
185                                     GIMP_WIDGET_HELP_TYPE_HELP);
186       gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_F1, 0,
187                                     "show-help", 1,
188                                     GTK_TYPE_WIDGET_HELP_TYPE,
189                                     GIMP_WIDGET_HELP_TYPE_HELP);
190 
191       initialized = TRUE;
192     }
193 
194   gimp_help_set_help_data (widget, NULL, help_id);
195 
196   g_object_set_data (G_OBJECT (widget), "gimp-help-data", help_data);
197 
198   g_signal_connect (widget, "show-help",
199                     G_CALLBACK (gimp_help_callback),
200                     help_func);
201 
202   gtk_widget_add_events (widget, GDK_BUTTON_PRESS_MASK);
203 }
204 
205 /**
206  * gimp_help_set_help_data:
207  * @widget:  The #GtkWidget you want to set a @tooltip and/or @help_id for.
208  * @tooltip: The text for this widget's tooltip (or %NULL).
209  * @help_id: The @help_id for the #GtkTipsQuery tooltips inspector.
210  *
211  * The reason why we don't use gtk_widget_set_tooltip_text() is that
212  * elements in the GIMP user interface should, if possible, also have
213  * a @help_id set for context-sensitive help.
214  *
215  * This function can be called with #NULL for @tooltip. Use this feature
216  * if you want to set a help link for a widget which shouldn't have
217  * a visible tooltip.
218  **/
219 void
gimp_help_set_help_data(GtkWidget * widget,const gchar * tooltip,const gchar * help_id)220 gimp_help_set_help_data (GtkWidget   *widget,
221                          const gchar *tooltip,
222                          const gchar *help_id)
223 {
224   g_return_if_fail (GTK_IS_WIDGET (widget));
225 
226   if (tooltips_enabled)
227     {
228       gtk_widget_set_tooltip_text (widget, tooltip);
229 
230       if (GTK_IS_MENU_ITEM (widget))
231         gimp_help_menu_item_set_tooltip (widget, tooltip, help_id);
232     }
233 
234   g_object_set_qdata (G_OBJECT (widget), GIMP_HELP_ID, (gpointer) help_id);
235 }
236 
237 /**
238  * gimp_help_set_help_data_with_markup:
239  * @widget:  The #GtkWidget you want to set a @tooltip and/or @help_id for.
240  * @tooltip: The markup for this widget's tooltip (or %NULL).
241  * @help_id: The @help_id for the #GtkTipsQuery tooltips inspector.
242  *
243  * Just like gimp_help_set_help_data(), but supports to pass text
244  * which is marked up with <link linkend="PangoMarkupFormat">Pango
245  * text markup language</link>.
246  *
247  * Since: 2.6
248  **/
249 void
gimp_help_set_help_data_with_markup(GtkWidget * widget,const gchar * tooltip,const gchar * help_id)250 gimp_help_set_help_data_with_markup (GtkWidget   *widget,
251                                      const gchar *tooltip,
252                                      const gchar *help_id)
253 {
254   g_return_if_fail (GTK_IS_WIDGET (widget));
255 
256   if (tooltips_enabled)
257     {
258       gtk_widget_set_tooltip_markup (widget, tooltip);
259 
260       if (GTK_IS_MENU_ITEM (widget))
261         gimp_help_menu_item_set_tooltip (widget, tooltip, help_id);
262     }
263 
264   g_object_set_qdata (G_OBJECT (widget), GIMP_HELP_ID, (gpointer) help_id);
265 }
266 
267 /**
268  * gimp_context_help:
269  * @widget: Any #GtkWidget on the screen.
270  *
271  * This function invokes the context help inspector.
272  *
273  * The mouse cursor will turn turn into a question mark and the user can
274  * click on any widget of the application which started the inspector.
275  *
276  * If the widget the user clicked on has a @help_id string attached
277  * (see gimp_help_set_help_data()), the corresponding help page will
278  * be displayed. Otherwise the help system will ascend the widget hierarchy
279  * until it finds an attached @help_id string (which should be the
280  * case at least for every window/dialog).
281  **/
282 void
gimp_context_help(GtkWidget * widget)283 gimp_context_help (GtkWidget *widget)
284 {
285   g_return_if_fail (GTK_IS_WIDGET (widget));
286 
287   gimp_help_callback (widget, GIMP_WIDGET_HELP_WHATS_THIS, NULL);
288 }
289 
290 /**
291  * gimp_help_id_quark:
292  *
293  * This function returns the #GQuark which should be used as key when
294  * attaching help IDs to widgets and objects.
295  *
296  * Return value: The #GQuark.
297  *
298  * Since: 2.2
299  **/
300 GQuark
gimp_help_id_quark(void)301 gimp_help_id_quark (void)
302 {
303   static GQuark quark = 0;
304 
305   if (! quark)
306     quark = g_quark_from_static_string ("gimp-help-id");
307 
308   return quark;
309 }
310 
311 
312 /*  private functions  */
313 
314 static const gchar *
gimp_help_get_help_data(GtkWidget * widget,GtkWidget ** help_widget,gpointer * ret_data)315 gimp_help_get_help_data (GtkWidget  *widget,
316                          GtkWidget **help_widget,
317                          gpointer   *ret_data)
318 {
319   const gchar *help_id   = NULL;
320   gpointer     help_data = NULL;
321 
322   for (; widget; widget = gtk_widget_get_parent (widget))
323     {
324       help_id   = g_object_get_qdata (G_OBJECT (widget), GIMP_HELP_ID);
325       help_data = g_object_get_data (G_OBJECT (widget), "gimp-help-data");
326 
327       if (help_id)
328         {
329           if (help_widget)
330             *help_widget = widget;
331 
332           if (ret_data)
333             *ret_data = help_data;
334 
335           return help_id;
336         }
337     }
338 
339   if (help_widget)
340     *help_widget = NULL;
341 
342   if (ret_data)
343     *ret_data = NULL;
344 
345   return NULL;
346 }
347 
348 static gboolean
gimp_help_callback(GtkWidget * widget,GimpWidgetHelpType help_type,GimpHelpFunc help_func)349 gimp_help_callback (GtkWidget          *widget,
350                     GimpWidgetHelpType  help_type,
351                     GimpHelpFunc        help_func)
352 {
353   switch (help_type)
354     {
355     case GIMP_WIDGET_HELP_TYPE_HELP:
356       if (help_func)
357         {
358           help_func (g_object_get_qdata (G_OBJECT (widget), GIMP_HELP_ID),
359                      g_object_get_data (G_OBJECT (widget), "gimp-help-data"));
360         }
361       return TRUE;
362 
363     case GIMP_WIDGET_HELP_WHATS_THIS:
364       g_idle_add (gimp_context_help_idle_start, widget);
365       return TRUE;
366 
367     default:
368       break;
369     }
370 
371   return FALSE;
372 }
373 
374 static void
gimp_help_menu_item_set_tooltip(GtkWidget * widget,const gchar * tooltip,const gchar * help_id)375 gimp_help_menu_item_set_tooltip (GtkWidget   *widget,
376                                  const gchar *tooltip,
377                                  const gchar *help_id)
378 {
379   g_return_if_fail (GTK_IS_MENU_ITEM (widget));
380 
381   if (tooltip && help_id)
382     {
383       g_object_set (widget, "has-tooltip", TRUE, NULL);
384 
385       g_signal_connect (widget, "query-tooltip",
386                         G_CALLBACK (gimp_help_menu_item_query_tooltip),
387                         NULL);
388     }
389   else if (! tooltip)
390     {
391       g_object_set (widget, "has-tooltip", FALSE, NULL);
392 
393       g_signal_handlers_disconnect_by_func (widget,
394                                             gimp_help_menu_item_query_tooltip,
395                                             NULL);
396     }
397 }
398 
399 static gboolean
gimp_help_menu_item_query_tooltip(GtkWidget * widget,gint x,gint y,gboolean keyboard_mode,GtkTooltip * tooltip)400 gimp_help_menu_item_query_tooltip (GtkWidget  *widget,
401                                    gint        x,
402                                    gint        y,
403                                    gboolean    keyboard_mode,
404                                    GtkTooltip *tooltip)
405 {
406   GtkWidget *vbox;
407   GtkWidget *label;
408   gchar     *text;
409   gboolean   use_markup = TRUE;
410 
411   text = gtk_widget_get_tooltip_markup (widget);
412 
413   if (! text)
414     {
415       text = gtk_widget_get_tooltip_text (widget);
416       use_markup = FALSE;
417     }
418 
419   if (! text)
420     return FALSE;
421 
422   vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
423 
424   label = gtk_label_new (text);
425   gtk_label_set_use_markup (GTK_LABEL (label), use_markup);
426   gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
427   gtk_label_set_xalign (GTK_LABEL (label), 0.0);
428   gtk_box_pack_start (GTK_BOX (vbox), label, TRUE, TRUE, 0);
429   gtk_widget_show (label);
430 
431   g_free (text);
432 
433   label = gtk_label_new (_("Press F1 for more help"));
434   gimp_label_set_attributes (GTK_LABEL (label),
435                              PANGO_ATTR_STYLE, PANGO_STYLE_ITALIC,
436                              PANGO_ATTR_SCALE, PANGO_SCALE_SMALL,
437                              -1);
438   gtk_label_set_xalign (GTK_LABEL (label), 1.0);
439   gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
440   gtk_widget_show (label);
441 
442   gtk_tooltip_set_custom (tooltip, vbox);
443 
444   return TRUE;
445 }
446 
447 
448 /*  Do all the actual context help calls in idle functions and check for
449  *  some widget holding a grab before starting the query because strange
450  *  things happen if (1) the help browser pops up while the query has
451  *  grabbed the pointer or (2) the query grabs the pointer while some
452  *  other part of GIMP has grabbed it (e.g. a tool, eek)
453  */
454 
455 static gboolean
gimp_context_help_idle_start(gpointer widget)456 gimp_context_help_idle_start (gpointer widget)
457 {
458   if (! gtk_grab_get_current ())
459     {
460       GtkWidget     *invisible;
461       GdkCursor     *cursor;
462       GdkGrabStatus  status;
463 
464       invisible = gtk_invisible_new_for_screen (gtk_widget_get_screen (widget));
465       gtk_widget_show (invisible);
466 
467       cursor = gdk_cursor_new_for_display (gtk_widget_get_display (invisible),
468                                            GDK_QUESTION_ARROW);
469 
470       status = gdk_pointer_grab (gtk_widget_get_window (invisible), TRUE,
471                                  GDK_BUTTON_PRESS_MASK   |
472                                  GDK_BUTTON_RELEASE_MASK |
473                                  GDK_ENTER_NOTIFY_MASK   |
474                                  GDK_LEAVE_NOTIFY_MASK,
475                                  NULL, cursor,
476                                  GDK_CURRENT_TIME);
477 
478       gdk_cursor_unref (cursor);
479 
480       if (status != GDK_GRAB_SUCCESS)
481         {
482           gtk_widget_destroy (invisible);
483           return FALSE;
484         }
485 
486       if (gdk_keyboard_grab (gtk_widget_get_window (invisible), TRUE,
487                              GDK_CURRENT_TIME) != GDK_GRAB_SUCCESS)
488         {
489           gdk_display_pointer_ungrab (gtk_widget_get_display (invisible),
490                                       GDK_CURRENT_TIME);
491           gtk_widget_destroy (invisible);
492           return FALSE;
493         }
494 
495       gtk_grab_add (invisible);
496 
497       g_signal_connect (invisible, "button-press-event",
498                         G_CALLBACK (gimp_context_help_button_press),
499                         NULL);
500       g_signal_connect (invisible, "key-press-event",
501                         G_CALLBACK (gimp_context_help_key_press),
502                         NULL);
503     }
504 
505   return FALSE;
506 }
507 
508 static gboolean
gimp_context_help_button_press(GtkWidget * widget,GdkEventButton * bevent,gpointer data)509 gimp_context_help_button_press (GtkWidget      *widget,
510                                 GdkEventButton *bevent,
511                                 gpointer        data)
512 {
513   GtkWidget *event_widget = gtk_get_event_widget ((GdkEvent *) bevent);
514 
515   if (event_widget && bevent->button == 1 && bevent->type == GDK_BUTTON_PRESS)
516     {
517       GdkDisplay *display = gtk_widget_get_display (widget);
518 
519       gtk_grab_remove (widget);
520       gdk_display_keyboard_ungrab (display, bevent->time);
521       gdk_display_pointer_ungrab (display, bevent->time);
522       gtk_widget_destroy (widget);
523 
524       if (event_widget != widget)
525         g_idle_add (gimp_context_help_idle_show_help, event_widget);
526     }
527 
528   return TRUE;
529 }
530 
531 static gboolean
gimp_context_help_key_press(GtkWidget * widget,GdkEventKey * kevent,gpointer data)532 gimp_context_help_key_press (GtkWidget   *widget,
533                              GdkEventKey *kevent,
534                              gpointer     data)
535 {
536   if (kevent->keyval == GDK_KEY_Escape)
537     {
538       GdkDisplay *display = gtk_widget_get_display (widget);
539 
540       gtk_grab_remove (widget);
541       gdk_display_keyboard_ungrab (display, kevent->time);
542       gdk_display_pointer_ungrab (display, kevent->time);
543       gtk_widget_destroy (widget);
544     }
545 
546   return TRUE;
547 }
548 
549 static gboolean
gimp_context_help_idle_show_help(gpointer data)550 gimp_context_help_idle_show_help (gpointer data)
551 {
552   GtkWidget   *help_widget;
553   const gchar *help_id   = NULL;
554   gpointer     help_data = NULL;
555 
556   help_id = gimp_help_get_help_data (GTK_WIDGET (data), &help_widget,
557                                      &help_data);
558 
559   if (help_id)
560     gimp_standard_help_func (help_id, help_data);
561 
562   return FALSE;
563 }
564