1 | /* gtktooltip.c |
2 | * |
3 | * Copyright (C) 2006-2007 Imendio AB |
4 | * Contact: Kristian Rietveld <kris@imendio.com> |
5 | * |
6 | * This library is free software; you can redistribute it and/or |
7 | * modify it under the terms of the GNU Library General Public |
8 | * License as published by the Free Software Foundation; either |
9 | * version 2 of the License, or (at your option) any later version. |
10 | * |
11 | * This library is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | * Library General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU Library General Public |
17 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtktooltip.h" |
23 | #include "gtktooltipprivate.h" |
24 | |
25 | #include <math.h> |
26 | #include <string.h> |
27 | |
28 | #include "gtkintl.h" |
29 | #include "gtkwindow.h" |
30 | #include "gtkmain.h" |
31 | #include "gtksettings.h" |
32 | #include "gtksizerequest.h" |
33 | #include "gtktooltipwindowprivate.h" |
34 | #include "gtkwindowprivate.h" |
35 | #include "gtkwidgetprivate.h" |
36 | #include "gtknative.h" |
37 | #include "gtkprivate.h" |
38 | |
39 | /** |
40 | * GtkTooltip: |
41 | * |
42 | * `GtkTooltip` is an object representing a widget tooltip. |
43 | * |
44 | * Basic tooltips can be realized simply by using |
45 | * [method@Gtk.Widget.set_tooltip_text] or |
46 | * [method@Gtk.Widget.set_tooltip_markup] without |
47 | * any explicit tooltip object. |
48 | * |
49 | * When you need a tooltip with a little more fancy contents, |
50 | * like adding an image, or you want the tooltip to have different |
51 | * contents per `GtkTreeView` row or cell, you will have to do a |
52 | * little more work: |
53 | * |
54 | * - Set the [property@Gtk.Widget:has-tooltip] property to %TRUE. |
55 | * This will make GTK monitor the widget for motion and related events |
56 | * which are needed to determine when and where to show a tooltip. |
57 | * |
58 | * - Connect to the [signal@Gtk.Widget::query-tooltip] signal. |
59 | * This signal will be emitted when a tooltip is supposed to be shown. |
60 | * One of the arguments passed to the signal handler is a `GtkTooltip` |
61 | * object. This is the object that we are about to display as a tooltip, |
62 | * and can be manipulated in your callback using functions like |
63 | * [method@Gtk.Tooltip.set_icon]. There are functions for setting |
64 | * the tooltip’s markup, setting an image from a named icon, or even |
65 | * putting in a custom widget. |
66 | * |
67 | * - Return %TRUE from your ::query-tooltip handler. This causes the tooltip |
68 | * to be show. If you return %FALSE, it will not be shown. |
69 | */ |
70 | |
71 | |
72 | #define HOVER_TIMEOUT 500 |
73 | #define BROWSE_TIMEOUT 60 |
74 | #define BROWSE_DISABLE_TIMEOUT 500 |
75 | |
76 | #define GTK_TOOLTIP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_TOOLTIP, GtkTooltipClass)) |
77 | #define GTK_IS_TOOLTIP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_TOOLTIP)) |
78 | #define GTK_TOOLTIP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_TOOLTIP, GtkTooltipClass)) |
79 | |
80 | /* We keep a single GtkTooltip object per display. The tooltip object |
81 | * owns a GtkTooltipWindow widget, which is using a popup surface, similar |
82 | * to what a GtkPopover does. It gets reparented to the right native widget |
83 | * whenever a tooltip is to be shown. The tooltip object keeps a weak |
84 | * ref on the native in order to remove the tooltip window when the |
85 | * native goes away. |
86 | */ |
87 | typedef struct _GtkTooltipClass GtkTooltipClass; |
88 | |
89 | struct _GtkTooltip |
90 | { |
91 | GObject parent_instance; |
92 | |
93 | GtkWidget *window; |
94 | |
95 | GtkWidget *tooltip_widget; |
96 | |
97 | GtkWidget *native; |
98 | |
99 | guint timeout_id; |
100 | guint browse_mode_timeout_id; |
101 | |
102 | GdkRectangle tip_area; |
103 | |
104 | guint browse_mode_enabled : 1; |
105 | guint tip_area_set : 1; |
106 | guint custom_was_reset : 1; |
107 | }; |
108 | |
109 | struct _GtkTooltipClass |
110 | { |
111 | GObjectClass parent_class; |
112 | }; |
113 | |
114 | #define GTK_TOOLTIP_VISIBLE(tooltip) ((tooltip)->window && gtk_widget_get_visible (GTK_WIDGET((tooltip)->window))) |
115 | |
116 | static void gtk_tooltip_dispose (GObject *object); |
117 | |
118 | static void gtk_tooltip_window_hide (GtkWidget *widget, |
119 | gpointer user_data); |
120 | static void gtk_tooltip_display_closed (GdkDisplay *display, |
121 | gboolean was_error, |
122 | GtkTooltip *tooltip); |
123 | static void gtk_tooltip_set_surface (GtkTooltip *tooltip, |
124 | GdkSurface *surface); |
125 | |
126 | static void gtk_tooltip_handle_event_internal (GdkEventType event_type, |
127 | GdkSurface *surface, |
128 | GtkWidget *target_widget, |
129 | double dx, |
130 | double dy); |
131 | |
132 | static GQuark quark_current_tooltip; |
133 | |
134 | G_DEFINE_TYPE (GtkTooltip, gtk_tooltip, G_TYPE_OBJECT); |
135 | |
136 | static void |
137 | gtk_tooltip_class_init (GtkTooltipClass *klass) |
138 | { |
139 | GObjectClass *object_class; |
140 | |
141 | quark_current_tooltip = g_quark_from_static_string (string: "gdk-display-current-tooltip" ); |
142 | |
143 | object_class = G_OBJECT_CLASS (klass); |
144 | |
145 | object_class->dispose = gtk_tooltip_dispose; |
146 | } |
147 | |
148 | static void |
149 | gtk_tooltip_init (GtkTooltip *tooltip) |
150 | { |
151 | tooltip->timeout_id = 0; |
152 | tooltip->browse_mode_timeout_id = 0; |
153 | |
154 | tooltip->browse_mode_enabled = FALSE; |
155 | |
156 | tooltip->tooltip_widget = NULL; |
157 | |
158 | tooltip->native = NULL; |
159 | |
160 | tooltip->window = gtk_tooltip_window_new (); |
161 | g_object_ref_sink (tooltip->window); |
162 | g_signal_connect (tooltip->window, "hide" , |
163 | G_CALLBACK (gtk_tooltip_window_hide), |
164 | tooltip); |
165 | } |
166 | |
167 | static void |
168 | gtk_tooltip_dispose (GObject *object) |
169 | { |
170 | GtkTooltip *tooltip = GTK_TOOLTIP (object); |
171 | |
172 | if (tooltip->timeout_id) |
173 | { |
174 | g_source_remove (tag: tooltip->timeout_id); |
175 | tooltip->timeout_id = 0; |
176 | } |
177 | |
178 | if (tooltip->browse_mode_timeout_id) |
179 | { |
180 | g_source_remove (tag: tooltip->browse_mode_timeout_id); |
181 | tooltip->browse_mode_timeout_id = 0; |
182 | } |
183 | |
184 | gtk_tooltip_set_custom (tooltip, NULL); |
185 | gtk_tooltip_set_surface (tooltip, NULL); |
186 | |
187 | if (tooltip->window) |
188 | { |
189 | GdkDisplay *display; |
190 | |
191 | display = gtk_widget_get_display (widget: tooltip->window); |
192 | g_signal_handlers_disconnect_by_func (display, |
193 | gtk_tooltip_display_closed, |
194 | tooltip); |
195 | gtk_tooltip_window_set_relative_to (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), NULL); |
196 | g_clear_object (&tooltip->window); |
197 | } |
198 | |
199 | G_OBJECT_CLASS (gtk_tooltip_parent_class)->dispose (object); |
200 | } |
201 | |
202 | /* public API */ |
203 | |
204 | /** |
205 | * gtk_tooltip_set_markup: |
206 | * @tooltip: a `GtkTooltip` |
207 | * @markup: (nullable): a string with Pango markup or %NLL |
208 | * |
209 | * Sets the text of the tooltip to be @markup. |
210 | * |
211 | * The string must be marked up with Pango markup. |
212 | * If @markup is %NULL, the label will be hidden. |
213 | */ |
214 | void |
215 | gtk_tooltip_set_markup (GtkTooltip *tooltip, |
216 | const char *markup) |
217 | { |
218 | g_return_if_fail (GTK_IS_TOOLTIP (tooltip)); |
219 | |
220 | gtk_tooltip_window_set_label_markup (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), markup); |
221 | } |
222 | |
223 | /** |
224 | * gtk_tooltip_set_text: |
225 | * @tooltip: a `GtkTooltip` |
226 | * @text: (nullable): a text string |
227 | * |
228 | * Sets the text of the tooltip to be @text. |
229 | * |
230 | * If @text is %NULL, the label will be hidden. |
231 | * See also [method@Gtk.Tooltip.set_markup]. |
232 | */ |
233 | void |
234 | gtk_tooltip_set_text (GtkTooltip *tooltip, |
235 | const char *text) |
236 | { |
237 | g_return_if_fail (GTK_IS_TOOLTIP (tooltip)); |
238 | |
239 | gtk_tooltip_window_set_label_text (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), text); |
240 | } |
241 | |
242 | /** |
243 | * gtk_tooltip_set_icon: |
244 | * @tooltip: a `GtkTooltip` |
245 | * @paintable: (nullable): a `GdkPaintable` |
246 | * |
247 | * Sets the icon of the tooltip (which is in front of the text) to be |
248 | * @paintable. If @paintable is %NULL, the image will be hidden. |
249 | */ |
250 | void |
251 | gtk_tooltip_set_icon (GtkTooltip *tooltip, |
252 | GdkPaintable *paintable) |
253 | { |
254 | g_return_if_fail (GTK_IS_TOOLTIP (tooltip)); |
255 | g_return_if_fail (paintable == NULL || GDK_IS_PAINTABLE (paintable)); |
256 | |
257 | gtk_tooltip_window_set_image_icon (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), paintable); |
258 | } |
259 | |
260 | /** |
261 | * gtk_tooltip_set_icon_from_icon_name: |
262 | * @tooltip: a `GtkTooltip` |
263 | * @icon_name: (nullable): an icon name |
264 | * |
265 | * Sets the icon of the tooltip (which is in front of the text) to be |
266 | * the icon indicated by @icon_name with the size indicated |
267 | * by @size. If @icon_name is %NULL, the image will be hidden. |
268 | */ |
269 | void |
270 | gtk_tooltip_set_icon_from_icon_name (GtkTooltip *tooltip, |
271 | const char *icon_name) |
272 | { |
273 | g_return_if_fail (GTK_IS_TOOLTIP (tooltip)); |
274 | |
275 | gtk_tooltip_window_set_image_icon_from_name (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), |
276 | icon_name); |
277 | } |
278 | |
279 | /** |
280 | * gtk_tooltip_set_icon_from_gicon: |
281 | * @tooltip: a `GtkTooltip` |
282 | * @gicon: (nullable): a `GIcon` representing the icon |
283 | * |
284 | * Sets the icon of the tooltip (which is in front of the text) |
285 | * to be the icon indicated by @gicon with the size indicated |
286 | * by @size. If @gicon is %NULL, the image will be hidden. |
287 | */ |
288 | void |
289 | gtk_tooltip_set_icon_from_gicon (GtkTooltip *tooltip, |
290 | GIcon *gicon) |
291 | { |
292 | g_return_if_fail (GTK_IS_TOOLTIP (tooltip)); |
293 | |
294 | gtk_tooltip_window_set_image_icon_from_gicon (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), |
295 | gicon); |
296 | } |
297 | |
298 | /** |
299 | * gtk_tooltip_set_custom: |
300 | * @tooltip: a `GtkTooltip` |
301 | * @custom_widget: (nullable): a `GtkWidget`, or %NULL to unset the old custom widget. |
302 | * |
303 | * Replaces the widget packed into the tooltip with |
304 | * @custom_widget. @custom_widget does not get destroyed when the tooltip goes |
305 | * away. |
306 | * By default a box with a `GtkImage` and `GtkLabel` is embedded in |
307 | * the tooltip, which can be configured using gtk_tooltip_set_markup() |
308 | * and gtk_tooltip_set_icon(). |
309 | */ |
310 | void |
311 | gtk_tooltip_set_custom (GtkTooltip *tooltip, |
312 | GtkWidget *custom_widget) |
313 | { |
314 | g_return_if_fail (GTK_IS_TOOLTIP (tooltip)); |
315 | g_return_if_fail (custom_widget == NULL || GTK_IS_WIDGET (custom_widget)); |
316 | |
317 | /* The custom widget has been updated from the query-tooltip |
318 | * callback, so we do not want to reset the custom widget later on. |
319 | */ |
320 | tooltip->custom_was_reset = TRUE; |
321 | |
322 | gtk_tooltip_window_set_custom_widget (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), custom_widget); |
323 | } |
324 | |
325 | /** |
326 | * gtk_tooltip_set_tip_area: |
327 | * @tooltip: a `GtkTooltip` |
328 | * @rect: a `GdkRectangle` |
329 | * |
330 | * Sets the area of the widget, where the contents of this tooltip apply, |
331 | * to be @rect (in widget coordinates). This is especially useful for |
332 | * properly setting tooltips on `GtkTreeView` rows and cells, `GtkIconViews`, |
333 | * etc. |
334 | * |
335 | * For setting tooltips on `GtkTreeView`, please refer to the convenience |
336 | * functions for this: gtk_tree_view_set_tooltip_row() and |
337 | * gtk_tree_view_set_tooltip_cell(). |
338 | */ |
339 | void |
340 | gtk_tooltip_set_tip_area (GtkTooltip *tooltip, |
341 | const GdkRectangle *rect) |
342 | { |
343 | g_return_if_fail (GTK_IS_TOOLTIP (tooltip)); |
344 | |
345 | if (!rect) |
346 | tooltip->tip_area_set = FALSE; |
347 | else |
348 | { |
349 | tooltip->tip_area_set = TRUE; |
350 | tooltip->tip_area = *rect; |
351 | } |
352 | } |
353 | |
354 | /* |
355 | * gtk_tooltip_trigger_tooltip_query: |
356 | * @display: a `GdkDisplay` |
357 | * |
358 | * Triggers a new tooltip query on @display, in order to update the current |
359 | * visible tooltip, or to show/hide the current tooltip. This function is |
360 | * useful to call when, for example, the state of the widget changed by a |
361 | * key press. |
362 | */ |
363 | void |
364 | gtk_tooltip_trigger_tooltip_query (GtkWidget *widget) |
365 | { |
366 | GdkDisplay *display; |
367 | GdkSeat *seat; |
368 | GdkDevice *device; |
369 | GdkSurface *surface; |
370 | double x, y; |
371 | GtkWidget *toplevel; |
372 | |
373 | g_return_if_fail (GTK_IS_WIDGET (widget)); |
374 | |
375 | display = gtk_widget_get_display (widget); |
376 | |
377 | /* Trigger logic as if the mouse moved */ |
378 | seat = gdk_display_get_default_seat (display); |
379 | if (seat) |
380 | device = gdk_seat_get_pointer (seat); |
381 | else |
382 | device = NULL; |
383 | if (device) |
384 | surface = gdk_device_get_surface_at_position (device, win_x: &x, win_y: &y); |
385 | else |
386 | surface = NULL; |
387 | if (!surface) |
388 | return; |
389 | |
390 | toplevel = GTK_WIDGET (gtk_widget_get_root (widget)); |
391 | |
392 | if (toplevel == NULL) |
393 | return; |
394 | |
395 | if (gtk_native_get_surface (self: GTK_NATIVE (ptr: toplevel)) != surface) |
396 | return; |
397 | |
398 | gtk_widget_translate_coordinates (src_widget: toplevel, dest_widget: widget, src_x: x, src_y: y, dest_x: &x, dest_y: &y); |
399 | |
400 | gtk_tooltip_handle_event_internal (event_type: GDK_MOTION_NOTIFY, surface, target_widget: widget, dx: x, dy: y); |
401 | } |
402 | |
403 | static void |
404 | gtk_tooltip_window_hide (GtkWidget *widget, |
405 | gpointer user_data) |
406 | { |
407 | GtkTooltip *tooltip = GTK_TOOLTIP (user_data); |
408 | |
409 | gtk_tooltip_set_custom (tooltip, NULL); |
410 | } |
411 | |
412 | GtkWidget * |
413 | _gtk_widget_find_at_coords (GdkSurface *surface, |
414 | int surface_x, |
415 | int surface_y, |
416 | int *widget_x, |
417 | int *widget_y) |
418 | { |
419 | GtkWidget *event_widget; |
420 | GtkWidget *picked_widget; |
421 | double x, y; |
422 | double native_x, native_y; |
423 | |
424 | g_return_val_if_fail (GDK_IS_SURFACE (surface), NULL); |
425 | |
426 | event_widget = GTK_WIDGET (gtk_native_get_for_surface (surface)); |
427 | |
428 | if (!event_widget) |
429 | return NULL; |
430 | |
431 | gtk_native_get_surface_transform (self: GTK_NATIVE (ptr: event_widget), x: &native_x, y: &native_y); |
432 | x = surface_x - native_x; |
433 | y = surface_y - native_y; |
434 | |
435 | picked_widget = gtk_widget_pick (widget: event_widget, x, y, flags: GTK_PICK_INSENSITIVE); |
436 | |
437 | if (picked_widget != NULL) |
438 | gtk_widget_translate_coordinates (src_widget: event_widget, dest_widget: picked_widget, src_x: x, src_y: y, dest_x: &x, dest_y: &y); |
439 | |
440 | *widget_x = x; |
441 | *widget_y = y; |
442 | |
443 | return picked_widget; |
444 | } |
445 | |
446 | static int |
447 | tooltip_browse_mode_expired (gpointer data) |
448 | { |
449 | GtkTooltip *tooltip; |
450 | GdkDisplay *display; |
451 | |
452 | tooltip = GTK_TOOLTIP (data); |
453 | |
454 | tooltip->browse_mode_enabled = FALSE; |
455 | tooltip->browse_mode_timeout_id = 0; |
456 | |
457 | if (tooltip->timeout_id) |
458 | { |
459 | g_source_remove (tag: tooltip->timeout_id); |
460 | tooltip->timeout_id = 0; |
461 | } |
462 | |
463 | /* destroy tooltip */ |
464 | display = gtk_widget_get_display (widget: tooltip->window); |
465 | g_object_set_qdata (G_OBJECT (display), quark: quark_current_tooltip, NULL); |
466 | |
467 | return FALSE; |
468 | } |
469 | |
470 | static void |
471 | gtk_tooltip_display_closed (GdkDisplay *display, |
472 | gboolean was_error, |
473 | GtkTooltip *tooltip) |
474 | { |
475 | if (tooltip->timeout_id) |
476 | { |
477 | g_source_remove (tag: tooltip->timeout_id); |
478 | tooltip->timeout_id = 0; |
479 | } |
480 | |
481 | g_object_set_qdata (G_OBJECT (display), quark: quark_current_tooltip, NULL); |
482 | } |
483 | |
484 | static void |
485 | native_weak_notify (gpointer data, GObject *former_object) |
486 | { |
487 | GtkTooltip *tooltip = data; |
488 | |
489 | gtk_tooltip_window_set_relative_to (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), NULL); |
490 | tooltip->native = NULL; |
491 | } |
492 | |
493 | static void |
494 | gtk_tooltip_set_surface (GtkTooltip *tooltip, |
495 | GdkSurface *surface) |
496 | { |
497 | GtkWidget *native; |
498 | |
499 | if (surface) |
500 | native = GTK_WIDGET (gtk_native_get_for_surface (surface)); |
501 | else |
502 | native = NULL; |
503 | |
504 | if (tooltip->native == native) |
505 | return; |
506 | |
507 | if (GTK_IS_TOOLTIP_WINDOW (ptr: native)) |
508 | return; |
509 | |
510 | if (tooltip->native) |
511 | g_object_weak_unref (G_OBJECT (tooltip->native), notify: native_weak_notify, data: tooltip); |
512 | |
513 | tooltip->native = native; |
514 | |
515 | if (tooltip->native) |
516 | g_object_weak_ref (G_OBJECT (tooltip->native), notify: native_weak_notify, data: tooltip); |
517 | |
518 | if (native) |
519 | gtk_tooltip_window_set_relative_to (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), relative_to: native); |
520 | else |
521 | gtk_tooltip_window_set_relative_to (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), NULL); |
522 | } |
523 | |
524 | static gboolean |
525 | gtk_tooltip_run_requery (GtkWidget **widget, |
526 | GtkTooltip *tooltip, |
527 | int *x, |
528 | int *y) |
529 | { |
530 | gboolean has_tooltip = FALSE; |
531 | gboolean return_value = FALSE; |
532 | |
533 | /* Reset tooltip */ |
534 | gtk_tooltip_set_markup (tooltip, NULL); |
535 | gtk_tooltip_set_icon (tooltip, NULL); |
536 | gtk_tooltip_set_tip_area (tooltip, NULL); |
537 | |
538 | /* See if the custom widget is again set from the query-tooltip |
539 | * callback. |
540 | */ |
541 | tooltip->custom_was_reset = FALSE; |
542 | |
543 | do |
544 | { |
545 | has_tooltip = gtk_widget_get_has_tooltip (widget: *widget); |
546 | |
547 | if (has_tooltip) |
548 | return_value = gtk_widget_query_tooltip (widget: *widget, x: *x, y: *y, FALSE, tooltip); |
549 | |
550 | if (!return_value) |
551 | { |
552 | GtkWidget *parent = gtk_widget_get_parent (widget: *widget); |
553 | |
554 | if (parent) |
555 | { |
556 | double xx = *x; |
557 | double yy = *y; |
558 | |
559 | if (gtk_widget_get_native (widget: parent) != gtk_widget_get_native (widget: *widget)) |
560 | break; |
561 | |
562 | gtk_widget_translate_coordinates (src_widget: *widget, dest_widget: parent, src_x: xx, src_y: yy, dest_x: &xx, dest_y: &yy); |
563 | |
564 | *x = xx; |
565 | *y = yy; |
566 | } |
567 | |
568 | *widget = parent; |
569 | } |
570 | else |
571 | break; |
572 | } |
573 | while (*widget); |
574 | |
575 | /* If the custom widget was not reset in the query-tooltip |
576 | * callback, we clear it here. |
577 | */ |
578 | if (!tooltip->custom_was_reset) |
579 | gtk_tooltip_set_custom (tooltip, NULL); |
580 | |
581 | return return_value; |
582 | } |
583 | |
584 | static void |
585 | gtk_tooltip_position (GtkTooltip *tooltip, |
586 | GdkDisplay *display, |
587 | GtkWidget *new_tooltip_widget, |
588 | GdkDevice *device) |
589 | { |
590 | GtkSettings *settings; |
591 | graphene_rect_t anchor_bounds; |
592 | GdkRectangle anchor_rect; |
593 | GdkSurface *effective_toplevel; |
594 | GtkWidget *toplevel; |
595 | int rect_anchor_dx = 0; |
596 | int cursor_size; |
597 | int anchor_rect_padding; |
598 | double native_x, native_y; |
599 | |
600 | gtk_widget_realize (GTK_WIDGET (tooltip->window)); |
601 | |
602 | tooltip->tooltip_widget = new_tooltip_widget; |
603 | |
604 | toplevel = GTK_WIDGET (gtk_widget_get_native (new_tooltip_widget)); |
605 | gtk_native_get_surface_transform (self: GTK_NATIVE (ptr: toplevel), x: &native_x, y: &native_y); |
606 | |
607 | if (gtk_widget_compute_bounds (widget: new_tooltip_widget, target: toplevel, out_bounds: &anchor_bounds)) |
608 | { |
609 | anchor_rect = (GdkRectangle) { |
610 | floorf (x: anchor_bounds.origin.x + native_x), |
611 | floorf (x: anchor_bounds.origin.y + native_y), |
612 | ceilf (x: anchor_bounds.size.width), |
613 | ceilf (x: anchor_bounds.size.height) |
614 | }; |
615 | } |
616 | else |
617 | { |
618 | anchor_rect = (GdkRectangle) { 0, 0, 0, 0 }; |
619 | } |
620 | |
621 | settings = gtk_settings_get_for_display (display); |
622 | g_object_get (object: settings, |
623 | first_property_name: "gtk-cursor-theme-size" , &cursor_size, |
624 | NULL); |
625 | |
626 | if (cursor_size == 0) |
627 | cursor_size = 16; |
628 | |
629 | if (device) |
630 | anchor_rect_padding = MAX (4, cursor_size - 32); |
631 | else |
632 | anchor_rect_padding = 4; |
633 | |
634 | anchor_rect.x -= anchor_rect_padding; |
635 | anchor_rect.y -= anchor_rect_padding; |
636 | anchor_rect.width += anchor_rect_padding * 2; |
637 | anchor_rect.height += anchor_rect_padding * 2; |
638 | |
639 | if (device) |
640 | { |
641 | const int max_x_distance = 32; |
642 | /* Max 48x48 icon + default padding */ |
643 | const int max_anchor_rect_height = 48 + 8; |
644 | double px, py; |
645 | int pointer_x, pointer_y; |
646 | |
647 | /* |
648 | * For pointer position triggered tooltips, implement the following |
649 | * semantics: |
650 | * |
651 | * If the anchor rectangle is too tall (meaning if we'd be constrained |
652 | * and flip, it'd flip too far away), rely only on the pointer position |
653 | * to position the tooltip. The approximate pointer cursorrectangle is |
654 | * used as an anchor rectangle. |
655 | * |
656 | * If the anchor rectangle isn't to tall, make sure the tooltip isn't too |
657 | * far away from the pointer position. |
658 | */ |
659 | effective_toplevel = gtk_native_get_surface (self: GTK_NATIVE (ptr: toplevel)); |
660 | gdk_surface_get_device_position (surface: effective_toplevel, device, x: &px, y: &py, NULL); |
661 | pointer_x = round (x: px); |
662 | pointer_y = round (x: py); |
663 | |
664 | if (anchor_rect.height > max_anchor_rect_height) |
665 | { |
666 | anchor_rect.x = pointer_x - 4; |
667 | anchor_rect.y = pointer_y - 4; |
668 | anchor_rect.width = cursor_size; |
669 | anchor_rect.height = cursor_size; |
670 | } |
671 | else |
672 | { |
673 | int anchor_point_x; |
674 | int x_distance; |
675 | |
676 | anchor_point_x = anchor_rect.x + anchor_rect.width / 2; |
677 | x_distance = pointer_x - anchor_point_x; |
678 | |
679 | if (x_distance > max_x_distance) |
680 | rect_anchor_dx = x_distance - max_x_distance; |
681 | else if (x_distance < -max_x_distance) |
682 | rect_anchor_dx = x_distance + max_x_distance; |
683 | } |
684 | } |
685 | |
686 | gtk_tooltip_window_position (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window), |
687 | rect: &anchor_rect, |
688 | rect_anchor: GDK_GRAVITY_SOUTH, |
689 | surface_anchor: GDK_GRAVITY_NORTH, |
690 | anchor_hints: GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_SLIDE_X, |
691 | dx: rect_anchor_dx, dy: 0); |
692 | } |
693 | |
694 | static void |
695 | gtk_tooltip_show_tooltip (GdkDisplay *display) |
696 | { |
697 | double px, py; |
698 | int x, y; |
699 | GdkSurface *surface; |
700 | GtkWidget *tooltip_widget; |
701 | GdkSeat *seat; |
702 | GdkDevice *device; |
703 | GtkTooltip *tooltip; |
704 | gboolean return_value = FALSE; |
705 | |
706 | tooltip = g_object_get_qdata (G_OBJECT (display), quark: quark_current_tooltip); |
707 | |
708 | if (!tooltip->native) |
709 | return; |
710 | |
711 | surface = gtk_native_get_surface (self: GTK_NATIVE (ptr: tooltip->native)); |
712 | |
713 | seat = gdk_display_get_default_seat (display); |
714 | if (seat) |
715 | device = gdk_seat_get_pointer (seat); |
716 | else |
717 | device = NULL; |
718 | |
719 | if (device) |
720 | gdk_surface_get_device_position (surface, device, x: &px, y: &py, NULL); |
721 | else |
722 | px = py = 0; |
723 | |
724 | x = round (x: px); |
725 | y = round (x: py); |
726 | |
727 | tooltip_widget = _gtk_widget_find_at_coords (surface, surface_x: x, surface_y: y, widget_x: &x, widget_y: &y); |
728 | |
729 | if (!tooltip_widget) |
730 | return; |
731 | |
732 | return_value = gtk_tooltip_run_requery (widget: &tooltip_widget, tooltip, x: &x, y: &y); |
733 | if (!return_value) |
734 | return; |
735 | |
736 | /* FIXME: should use tooltip->window iso tooltip->window */ |
737 | if (display != gtk_widget_get_display (widget: tooltip->window)) |
738 | { |
739 | g_signal_handlers_disconnect_by_func (display, |
740 | gtk_tooltip_display_closed, |
741 | tooltip); |
742 | |
743 | gtk_window_set_display (GTK_WINDOW (tooltip->window), display); |
744 | |
745 | g_signal_connect (display, "closed" , |
746 | G_CALLBACK (gtk_tooltip_display_closed), tooltip); |
747 | } |
748 | |
749 | gtk_tooltip_position (tooltip, display, new_tooltip_widget: tooltip_widget, device); |
750 | |
751 | gtk_widget_show (GTK_WIDGET (tooltip->window)); |
752 | |
753 | /* Now a tooltip is visible again on the display, make sure browse |
754 | * mode is enabled. |
755 | */ |
756 | tooltip->browse_mode_enabled = TRUE; |
757 | if (tooltip->browse_mode_timeout_id) |
758 | { |
759 | g_source_remove (tag: tooltip->browse_mode_timeout_id); |
760 | tooltip->browse_mode_timeout_id = 0; |
761 | } |
762 | } |
763 | |
764 | static void |
765 | gtk_tooltip_hide_tooltip (GtkTooltip *tooltip) |
766 | { |
767 | guint timeout = BROWSE_DISABLE_TIMEOUT; |
768 | |
769 | if (!tooltip) |
770 | return; |
771 | |
772 | if (tooltip->timeout_id) |
773 | { |
774 | g_source_remove (tag: tooltip->timeout_id); |
775 | tooltip->timeout_id = 0; |
776 | } |
777 | |
778 | if (!GTK_TOOLTIP_VISIBLE (tooltip)) |
779 | return; |
780 | |
781 | tooltip->tooltip_widget = NULL; |
782 | |
783 | /* The tooltip is gone, after (by default, should be configurable) 500ms |
784 | * we want to turn off browse mode |
785 | */ |
786 | if (!tooltip->browse_mode_timeout_id) |
787 | { |
788 | tooltip->browse_mode_timeout_id = |
789 | g_timeout_add_full (priority: 0, interval: timeout, |
790 | function: tooltip_browse_mode_expired, |
791 | g_object_ref (tooltip), |
792 | notify: g_object_unref); |
793 | gdk_source_set_static_name_by_id (tag: tooltip->browse_mode_timeout_id, name: "[gtk] tooltip_browse_mode_expired" ); |
794 | } |
795 | |
796 | if (tooltip->window) |
797 | gtk_widget_hide (widget: tooltip->window); |
798 | } |
799 | |
800 | static int |
801 | (gpointer data) |
802 | { |
803 | GdkDisplay *display; |
804 | GtkTooltip *tooltip; |
805 | |
806 | display = GDK_DISPLAY (data); |
807 | tooltip = g_object_get_qdata (G_OBJECT (display), quark: quark_current_tooltip); |
808 | |
809 | /* This usually does not happen. However, it does occur in language |
810 | * bindings were reference counting of objects behaves differently. |
811 | */ |
812 | if (!tooltip) |
813 | return FALSE; |
814 | |
815 | gtk_tooltip_show_tooltip (display); |
816 | |
817 | tooltip->timeout_id = 0; |
818 | |
819 | return FALSE; |
820 | } |
821 | |
822 | static void |
823 | gtk_tooltip_start_delay (GdkDisplay *display) |
824 | { |
825 | guint timeout; |
826 | GtkTooltip *tooltip; |
827 | |
828 | tooltip = g_object_get_qdata (G_OBJECT (display), quark: quark_current_tooltip); |
829 | |
830 | if (!tooltip || GTK_TOOLTIP_VISIBLE (tooltip)) |
831 | return; |
832 | |
833 | if (tooltip->timeout_id) |
834 | g_source_remove (tag: tooltip->timeout_id); |
835 | |
836 | if (tooltip->browse_mode_enabled) |
837 | timeout = BROWSE_TIMEOUT; |
838 | else |
839 | timeout = HOVER_TIMEOUT; |
840 | |
841 | tooltip->timeout_id = g_timeout_add_full (priority: 0, interval: timeout, |
842 | function: tooltip_popup_timeout, |
843 | g_object_ref (display), |
844 | notify: g_object_unref); |
845 | gdk_source_set_static_name_by_id (tag: tooltip->timeout_id, name: "[gtk] tooltip_popup_timeout" ); |
846 | } |
847 | |
848 | void |
849 | _gtk_tooltip_hide (GtkWidget *widget) |
850 | { |
851 | GdkDisplay *display; |
852 | GtkTooltip *tooltip; |
853 | |
854 | display = gtk_widget_get_display (widget); |
855 | tooltip = g_object_get_qdata (G_OBJECT (display), quark: quark_current_tooltip); |
856 | |
857 | if (!tooltip || !tooltip->window || !tooltip->tooltip_widget) |
858 | return; |
859 | |
860 | if (widget == tooltip->tooltip_widget) |
861 | gtk_tooltip_hide_tooltip (tooltip); |
862 | } |
863 | |
864 | static gboolean |
865 | tooltips_enabled (GdkEvent *event) |
866 | { |
867 | GdkDevice *source_device; |
868 | GdkInputSource source; |
869 | GdkModifierType event_state = 0; |
870 | |
871 | switch ((guint)gdk_event_get_event_type (event)) |
872 | { |
873 | case GDK_ENTER_NOTIFY: |
874 | case GDK_LEAVE_NOTIFY: |
875 | case GDK_BUTTON_PRESS: |
876 | case GDK_KEY_PRESS: |
877 | case GDK_DRAG_ENTER: |
878 | case GDK_GRAB_BROKEN: |
879 | case GDK_MOTION_NOTIFY: |
880 | case GDK_TOUCH_UPDATE: |
881 | case GDK_SCROLL: |
882 | break; /* OK */ |
883 | |
884 | default: |
885 | return FALSE; |
886 | } |
887 | |
888 | event_state = gdk_event_get_modifier_state (event); |
889 | if ((event_state & |
890 | (GDK_BUTTON1_MASK | |
891 | GDK_BUTTON2_MASK | |
892 | GDK_BUTTON3_MASK | |
893 | GDK_BUTTON4_MASK | |
894 | GDK_BUTTON5_MASK)) != 0) |
895 | return FALSE; |
896 | |
897 | source_device = gdk_event_get_device (event); |
898 | |
899 | if (!source_device) |
900 | return FALSE; |
901 | |
902 | source = gdk_device_get_source (device: source_device); |
903 | |
904 | if (source != GDK_SOURCE_TOUCHSCREEN) |
905 | return TRUE; |
906 | |
907 | return FALSE; |
908 | } |
909 | |
910 | void |
911 | _gtk_tooltip_handle_event (GtkWidget *target, |
912 | GdkEvent *event) |
913 | { |
914 | GdkEventType event_type; |
915 | GdkSurface *surface; |
916 | double x, y; |
917 | double nx, ny; |
918 | GtkNative *native; |
919 | |
920 | if (!tooltips_enabled (event)) |
921 | return; |
922 | |
923 | native = gtk_widget_get_native (widget: target); |
924 | if (!native) |
925 | return; |
926 | |
927 | event_type = gdk_event_get_event_type (event); |
928 | surface = gdk_event_get_surface (event); |
929 | gdk_event_get_position (event, x: &x, y: &y); |
930 | |
931 | /* ignore synthetic motion events */ |
932 | if (event_type == GDK_MOTION_NOTIFY && |
933 | gdk_event_get_time (event) == GDK_CURRENT_TIME) |
934 | return; |
935 | |
936 | gtk_native_get_surface_transform (self: native, x: &nx, y: &ny); |
937 | gtk_widget_translate_coordinates (GTK_WIDGET (native), dest_widget: target, src_x: x - nx, src_y: y - ny, dest_x: &x, dest_y: &y); |
938 | gtk_tooltip_handle_event_internal (event_type, surface, target_widget: target, dx: x, dy: y); |
939 | } |
940 | |
941 | /* dx/dy must be in @target_widget's coordinates */ |
942 | static void |
943 | gtk_tooltip_handle_event_internal (GdkEventType event_type, |
944 | GdkSurface *surface, |
945 | GtkWidget *target_widget, |
946 | double dx, |
947 | double dy) |
948 | { |
949 | int x = dx, y = dy; |
950 | GdkDisplay *display; |
951 | GtkTooltip *tooltip; |
952 | |
953 | display = gdk_surface_get_display (surface); |
954 | tooltip = g_object_get_qdata (G_OBJECT (display), quark: quark_current_tooltip); |
955 | |
956 | if (tooltip) |
957 | gtk_tooltip_set_surface (tooltip, surface); |
958 | |
959 | /* Hide the tooltip when there's no new tooltip widget */ |
960 | if (!target_widget) |
961 | { |
962 | if (tooltip) |
963 | gtk_tooltip_hide_tooltip (tooltip); |
964 | |
965 | return; |
966 | } |
967 | |
968 | switch ((guint) event_type) |
969 | { |
970 | case GDK_BUTTON_PRESS: |
971 | case GDK_KEY_PRESS: |
972 | case GDK_DRAG_ENTER: |
973 | case GDK_GRAB_BROKEN: |
974 | case GDK_SCROLL: |
975 | gtk_tooltip_hide_tooltip (tooltip); |
976 | break; |
977 | |
978 | case GDK_MOTION_NOTIFY: |
979 | case GDK_ENTER_NOTIFY: |
980 | case GDK_LEAVE_NOTIFY: |
981 | if (tooltip) |
982 | { |
983 | gboolean tip_area_set; |
984 | GdkRectangle tip_area; |
985 | gboolean hide_tooltip; |
986 | |
987 | tip_area_set = tooltip->tip_area_set; |
988 | tip_area = tooltip->tip_area; |
989 | |
990 | gtk_tooltip_run_requery (widget: &target_widget, tooltip, x: &x, y: &y); |
991 | |
992 | /* Leave notify should override the query function */ |
993 | hide_tooltip = (event_type == GDK_LEAVE_NOTIFY); |
994 | |
995 | /* Is the pointer above another widget now? */ |
996 | if (GTK_TOOLTIP_VISIBLE (tooltip)) |
997 | hide_tooltip |= target_widget != tooltip->tooltip_widget; |
998 | |
999 | /* Did the pointer move out of the previous "context area"? */ |
1000 | if (tip_area_set) |
1001 | hide_tooltip |= !gdk_rectangle_contains_point (rect: &tip_area, x, y); |
1002 | |
1003 | if (hide_tooltip) |
1004 | gtk_tooltip_hide_tooltip (tooltip); |
1005 | else |
1006 | gtk_tooltip_start_delay (display); |
1007 | } |
1008 | else |
1009 | { |
1010 | /* Need a new tooltip for this display */ |
1011 | tooltip = g_object_new (GTK_TYPE_TOOLTIP, NULL); |
1012 | g_object_set_qdata_full (G_OBJECT (display), quark: quark_current_tooltip, |
1013 | data: tooltip, destroy: g_object_unref); |
1014 | g_signal_connect (display, "closed" , |
1015 | G_CALLBACK (gtk_tooltip_display_closed), tooltip); |
1016 | |
1017 | gtk_tooltip_set_surface (tooltip, surface); |
1018 | |
1019 | gtk_tooltip_start_delay (display); |
1020 | } |
1021 | break; |
1022 | |
1023 | default: |
1024 | break; |
1025 | } |
1026 | } |
1027 | |
1028 | void |
1029 | gtk_tooltip_maybe_allocate (GtkNative *native) |
1030 | { |
1031 | GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (native)); |
1032 | GtkTooltip *tooltip; |
1033 | |
1034 | tooltip = g_object_get_qdata (G_OBJECT (display), quark: quark_current_tooltip); |
1035 | if (!tooltip || GTK_NATIVE (ptr: tooltip->native) != native) |
1036 | return; |
1037 | |
1038 | gtk_tooltip_window_present (window: GTK_TOOLTIP_WINDOW (ptr: tooltip->window)); |
1039 | } |
1040 | |
1041 | void |
1042 | gtk_tooltip_unset_surface (GtkNative *native) |
1043 | { |
1044 | GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (native)); |
1045 | GtkTooltip *tooltip; |
1046 | |
1047 | tooltip = g_object_get_qdata (G_OBJECT (display), quark: quark_current_tooltip); |
1048 | if (!tooltip || GTK_NATIVE (ptr: tooltip->native) != native) |
1049 | return; |
1050 | |
1051 | gtk_tooltip_set_surface (tooltip, NULL); |
1052 | } |
1053 | |
1054 | |