xref: /vim-8.2.3635/src/gui_beval.c (revision 723d165c)
1 /* vi:set ts=8 sts=4 sw=4 noet:
2  *
3  * VIM - Vi IMproved	by Bram Moolenaar
4  *			Visual Workshop integration by Gordon Prieur
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
10 
11 #include "vim.h"
12 
13 #if defined(FEAT_BEVAL_GUI) || defined(PROTO)
14 
15 /* on Win32 only get_beval_info() is required */
16 #if !defined(FEAT_GUI_MSWIN) || defined(PROTO)
17 
18 #ifdef FEAT_GUI_GTK
19 # if GTK_CHECK_VERSION(3,0,0)
20 #  include <gdk/gdkkeysyms-compat.h>
21 # else
22 #  include <gdk/gdkkeysyms.h>
23 # endif
24 # include <gtk/gtk.h>
25 #else
26 # include <X11/keysym.h>
27 # ifdef FEAT_GUI_MOTIF
28 #  include <Xm/PushB.h>
29 #  include <Xm/Separator.h>
30 #  include <Xm/List.h>
31 #  include <Xm/Label.h>
32 #  include <Xm/AtomMgr.h>
33 #  include <Xm/Protocols.h>
34 # else
35    /* Assume Athena */
36 #  include <X11/Shell.h>
37 #  ifdef FEAT_GUI_NEXTAW
38 #   include <X11/neXtaw/Label.h>
39 #  else
40 #   include <X11/Xaw/Label.h>
41 #  endif
42 # endif
43 #endif
44 
45 #ifndef FEAT_GUI_GTK
46 extern Widget vimShell;
47 
48 /*
49  * Currently, we assume that there can be only one BalloonEval showing
50  * on-screen at any given moment.  This variable will hold the currently
51  * showing BalloonEval or NULL if none is showing.
52  */
53 static BalloonEval *current_beval = NULL;
54 #endif
55 
56 #ifdef FEAT_GUI_GTK
57 static void addEventHandler(GtkWidget *, BalloonEval *);
58 static void removeEventHandler(BalloonEval *);
59 static gint target_event_cb(GtkWidget *, GdkEvent *, gpointer);
60 static gint mainwin_event_cb(GtkWidget *, GdkEvent *, gpointer);
61 static void pointer_event(BalloonEval *, int, int, unsigned);
62 static void key_event(BalloonEval *, unsigned, int);
63 static gboolean timeout_cb(gpointer);
64 # if GTK_CHECK_VERSION(3,0,0)
65 static gboolean balloon_draw_event_cb (GtkWidget *, cairo_t *, gpointer);
66 # else
67 static gint balloon_expose_event_cb (GtkWidget *, GdkEventExpose *, gpointer);
68 # endif
69 #else
70 static void addEventHandler(Widget, BalloonEval *);
71 static void removeEventHandler(BalloonEval *);
72 static void pointerEventEH(Widget, XtPointer, XEvent *, Boolean *);
73 static void pointerEvent(BalloonEval *, XEvent *);
74 static void timerRoutine(XtPointer, XtIntervalId *);
75 #endif
76 static void cancelBalloon(BalloonEval *);
77 static void requestBalloon(BalloonEval *);
78 static void drawBalloon(BalloonEval *);
79 static void undrawBalloon(BalloonEval *beval);
80 static void createBalloonEvalWindow(BalloonEval *);
81 
82 /*
83  * Create a balloon-evaluation area for a Widget.
84  * There can be either a "mesg" for a fixed string or "mesgCB" to generate a
85  * message by calling this callback function.
86  * When "mesg" is not NULL it must remain valid for as long as the balloon is
87  * used.  It is not freed here.
88  * Returns a pointer to the resulting object (NULL when out of memory).
89  */
90     BalloonEval *
91 gui_mch_create_beval_area(
92     void	*target,
93     char_u	*mesg,
94     void	(*mesgCB)(BalloonEval *, int),
95     void	*clientData)
96 {
97 #ifndef FEAT_GUI_GTK
98     char	*display_name;	    /* get from gui.dpy */
99     int		screen_num;
100     char	*p;
101 #endif
102     BalloonEval	*beval;
103 
104     if (mesg != NULL && mesgCB != NULL)
105     {
106 	iemsg(_("E232: Cannot create BalloonEval with both message and callback"));
107 	return NULL;
108     }
109 
110     beval = (BalloonEval *)alloc_clear(sizeof(BalloonEval));
111     if (beval != NULL)
112     {
113 #ifdef FEAT_GUI_GTK
114 	beval->target = GTK_WIDGET(target);
115 #else
116 	beval->target = (Widget)target;
117 	beval->appContext = XtWidgetToApplicationContext((Widget)target);
118 #endif
119 	beval->showState = ShS_NEUTRAL;
120 	beval->msg = mesg;
121 	beval->msgCB = mesgCB;
122 	beval->clientData = clientData;
123 
124 	/*
125 	 * Set up event handler which will keep its eyes on the pointer,
126 	 * and when the pointer rests in a certain spot for a given time
127 	 * interval, show the beval.
128 	 */
129 	addEventHandler(beval->target, beval);
130 	createBalloonEvalWindow(beval);
131 
132 #ifndef FEAT_GUI_GTK
133 	/*
134 	 * Now create and save the screen width and height. Used in drawing.
135 	 */
136 	display_name = DisplayString(gui.dpy);
137 	p = strrchr(display_name, '.');
138 	if (p != NULL)
139 	    screen_num = atoi(++p);
140 	else
141 	    screen_num = 0;
142 	beval->screen_width = DisplayWidth(gui.dpy, screen_num);
143 	beval->screen_height = DisplayHeight(gui.dpy, screen_num);
144 #endif
145     }
146 
147     return beval;
148 }
149 
150 #if defined(FEAT_BEVAL_TIP) || defined(PROTO)
151 /*
152  * Destroy a balloon-eval and free its associated memory.
153  */
154     void
155 gui_mch_destroy_beval_area(BalloonEval *beval)
156 {
157     cancelBalloon(beval);
158     removeEventHandler(beval);
159     /* Children will automatically be destroyed */
160 # ifdef FEAT_GUI_GTK
161     gtk_widget_destroy(beval->balloonShell);
162 # else
163     XtDestroyWidget(beval->balloonShell);
164 # endif
165 # ifdef FEAT_VARTABS
166     if (beval->vts)
167 	vim_free(beval->vts);
168 # endif
169     vim_free(beval);
170 }
171 #endif
172 
173     void
174 gui_mch_enable_beval_area(BalloonEval *beval)
175 {
176     if (beval != NULL)
177 	addEventHandler(beval->target, beval);
178 }
179 
180     void
181 gui_mch_disable_beval_area(BalloonEval *beval)
182 {
183     if (beval != NULL)
184 	removeEventHandler(beval);
185 }
186 
187 #if defined(FEAT_BEVAL_TIP) || defined(PROTO)
188 /*
189  * This function returns the BalloonEval * associated with the currently
190  * displayed tooltip.  Returns NULL if there is no tooltip currently showing.
191  *
192  * Assumption: Only one tooltip can be shown at a time.
193  */
194     BalloonEval *
195 gui_mch_currently_showing_beval(void)
196 {
197     return current_beval;
198 }
199 #endif
200 #endif /* !FEAT_GUI_MSWIN */
201 
202 #if defined(FEAT_NETBEANS_INTG) || defined(FEAT_EVAL) || defined(PROTO)
203 # if !defined(FEAT_GUI_MSWIN) || defined(PROTO)
204 
205 /*
206  * Show a balloon with "mesg".
207  */
208     void
209 gui_mch_post_balloon(BalloonEval *beval, char_u *mesg)
210 {
211     beval->msg = mesg;
212     if (mesg != NULL)
213 	drawBalloon(beval);
214     else
215 	undrawBalloon(beval);
216 }
217 # endif /* !FEAT_GUI_MSWIN */
218 #endif /* FEAT_NETBEANS_INTG || PROTO */
219 
220 #if !defined(FEAT_GUI_MSWIN) || defined(PROTO)
221 #if defined(FEAT_BEVAL_TIP) || defined(PROTO)
222 /*
223  * Hide the given balloon.
224  */
225     void
226 gui_mch_unpost_balloon(BalloonEval *beval)
227 {
228     undrawBalloon(beval);
229 }
230 #endif
231 
232 #ifdef FEAT_GUI_GTK
233     static void
234 addEventHandler(GtkWidget *target, BalloonEval *beval)
235 {
236     /*
237      * Connect to the generic "event" signal instead of the individual
238      * signals for each event type, because the former is emitted earlier.
239      * This allows us to catch events independently of the signal handlers
240      * in gui_gtk_x11.c.
241      */
242     g_signal_connect(G_OBJECT(target), "event",
243 		     G_CALLBACK(target_event_cb),
244 		     beval);
245     /*
246      * Nasty:  Key press events go to the main window thus the drawing area
247      * will never see them.  This means we have to connect to the main window
248      * as well in order to catch those events.
249      */
250     if (gtk_socket_id == 0 && gui.mainwin != NULL
251 	    && gtk_widget_is_ancestor(target, gui.mainwin))
252     {
253 	g_signal_connect(G_OBJECT(gui.mainwin), "event",
254 			 G_CALLBACK(mainwin_event_cb),
255 			 beval);
256     }
257 }
258 
259     static void
260 removeEventHandler(BalloonEval *beval)
261 {
262     g_signal_handlers_disconnect_by_func(G_OBJECT(beval->target),
263 					 FUNC2GENERIC(target_event_cb),
264 					 beval);
265 
266     if (gtk_socket_id == 0 && gui.mainwin != NULL
267 	    && gtk_widget_is_ancestor(beval->target, gui.mainwin))
268     {
269 	g_signal_handlers_disconnect_by_func(G_OBJECT(gui.mainwin),
270 					     FUNC2GENERIC(mainwin_event_cb),
271 					     beval);
272     }
273 }
274 
275     static gint
276 target_event_cb(GtkWidget *widget, GdkEvent *event, gpointer data)
277 {
278     BalloonEval *beval = (BalloonEval *)data;
279 
280     switch (event->type)
281     {
282 	case GDK_ENTER_NOTIFY:
283 	    pointer_event(beval, (int)event->crossing.x,
284 				 (int)event->crossing.y,
285 				 event->crossing.state);
286 	    break;
287 	case GDK_MOTION_NOTIFY:
288 	    if (event->motion.is_hint)
289 	    {
290 		int		x;
291 		int		y;
292 		GdkModifierType	state;
293 		/*
294 		 * GDK_POINTER_MOTION_HINT_MASK is set, thus we cannot obtain
295 		 * the coordinates from the GdkEventMotion struct directly.
296 		 */
297 # if GTK_CHECK_VERSION(3,0,0)
298 		{
299 		    GdkWindow * const win = gtk_widget_get_window(widget);
300 		    GdkDisplay * const dpy = gdk_window_get_display(win);
301 #  if GTK_CHECK_VERSION(3,20,0)
302 		    GdkSeat * const seat = gdk_display_get_default_seat(dpy);
303 		    GdkDevice * const dev = gdk_seat_get_pointer(seat);
304 #  else
305 		    GdkDeviceManager * const mngr = gdk_display_get_device_manager(dpy);
306 		    GdkDevice * const dev = gdk_device_manager_get_client_pointer(mngr);
307 #  endif
308 		    gdk_window_get_device_position(win, dev , &x, &y, &state);
309 		}
310 # else
311 		gdk_window_get_pointer(widget->window, &x, &y, &state);
312 # endif
313 		pointer_event(beval, x, y, (unsigned int)state);
314 	    }
315 	    else
316 	    {
317 		pointer_event(beval, (int)event->motion.x,
318 				     (int)event->motion.y,
319 				     event->motion.state);
320 	    }
321 	    break;
322 	case GDK_LEAVE_NOTIFY:
323 	    /*
324 	     * Ignore LeaveNotify events that are not "normal".
325 	     * Apparently we also get it when somebody else grabs focus.
326 	     */
327 	    if (event->crossing.mode == GDK_CROSSING_NORMAL)
328 		cancelBalloon(beval);
329 	    break;
330 	case GDK_BUTTON_PRESS:
331 	case GDK_SCROLL:
332 	    cancelBalloon(beval);
333 	    break;
334 	case GDK_KEY_PRESS:
335 	    key_event(beval, event->key.keyval, TRUE);
336 	    break;
337 	case GDK_KEY_RELEASE:
338 	    key_event(beval, event->key.keyval, FALSE);
339 	    break;
340 	default:
341 	    break;
342     }
343 
344     return FALSE; /* continue emission */
345 }
346 
347     static gint
348 mainwin_event_cb(GtkWidget *widget UNUSED, GdkEvent *event, gpointer data)
349 {
350     BalloonEval *beval = (BalloonEval *)data;
351 
352     switch (event->type)
353     {
354 	case GDK_KEY_PRESS:
355 	    key_event(beval, event->key.keyval, TRUE);
356 	    break;
357 	case GDK_KEY_RELEASE:
358 	    key_event(beval, event->key.keyval, FALSE);
359 	    break;
360 	default:
361 	    break;
362     }
363 
364     return FALSE; /* continue emission */
365 }
366 
367     static void
368 pointer_event(BalloonEval *beval, int x, int y, unsigned state)
369 {
370     int distance;
371 
372     distance = ABS(x - beval->x) + ABS(y - beval->y);
373 
374     if (distance > 4)
375     {
376 	/*
377 	 * Moved out of the balloon location: cancel it.
378 	 * Remember button state
379 	 */
380 	beval->state = state;
381 	cancelBalloon(beval);
382 
383 	/* Mouse buttons are pressed - no balloon now */
384 	if (!(state & ((int)GDK_BUTTON1_MASK | (int)GDK_BUTTON2_MASK
385 						    | (int)GDK_BUTTON3_MASK)))
386 	{
387 	    beval->x = x;
388 	    beval->y = y;
389 
390 	    if (state & (int)GDK_MOD1_MASK)
391 	    {
392 		/*
393 		 * Alt is pressed -- enter super-evaluate-mode,
394 		 * where there is no time delay
395 		 */
396 		if (beval->msgCB != NULL)
397 		{
398 		    beval->showState = ShS_PENDING;
399 		    (*beval->msgCB)(beval, state);
400 		}
401 	    }
402 	    else
403 	    {
404 		beval->timerID = g_timeout_add((guint)p_bdlay,
405 					       &timeout_cb, beval);
406 	    }
407 	}
408     }
409 }
410 
411     static void
412 key_event(BalloonEval *beval, unsigned keyval, int is_keypress)
413 {
414     if (beval->showState == ShS_SHOWING && beval->msgCB != NULL)
415     {
416 	switch (keyval)
417 	{
418 	    case GDK_Shift_L:
419 	    case GDK_Shift_R:
420 		beval->showState = ShS_UPDATE_PENDING;
421 		(*beval->msgCB)(beval, (is_keypress)
422 						   ? (int)GDK_SHIFT_MASK : 0);
423 		break;
424 	    case GDK_Control_L:
425 	    case GDK_Control_R:
426 		beval->showState = ShS_UPDATE_PENDING;
427 		(*beval->msgCB)(beval, (is_keypress)
428 						 ? (int)GDK_CONTROL_MASK : 0);
429 		break;
430 	    default:
431 		/* Don't do this for key release, we apparently get these with
432 		 * focus changes in some GTK version. */
433 		if (is_keypress)
434 		    cancelBalloon(beval);
435 		break;
436 	}
437     }
438     else
439 	cancelBalloon(beval);
440 }
441 
442     static gboolean
443 timeout_cb(gpointer data)
444 {
445     BalloonEval *beval = (BalloonEval *)data;
446 
447     beval->timerID = 0;
448     /*
449      * If the timer event happens then the mouse has stopped long enough for
450      * a request to be started. The request will only send to the debugger if
451      * there the mouse is pointing at real data.
452      */
453     requestBalloon(beval);
454 
455     return FALSE; /* don't call me again */
456 }
457 
458 # if GTK_CHECK_VERSION(3,0,0)
459     static gboolean
460 balloon_draw_event_cb(GtkWidget *widget,
461 		      cairo_t	*cr,
462 		      gpointer	 data UNUSED)
463 {
464     GtkStyleContext *context = NULL;
465     gint width = -1, height = -1;
466 
467     if (widget == NULL)
468 	return TRUE;
469 
470     context = gtk_widget_get_style_context(widget);
471     width = gtk_widget_get_allocated_width(widget);
472     height = gtk_widget_get_allocated_height(widget);
473 
474     gtk_style_context_save(context);
475 
476     gtk_style_context_add_class(context, "tooltip");
477     gtk_style_context_set_state(context, GTK_STATE_FLAG_NORMAL);
478 
479     cairo_save(cr);
480     gtk_render_frame(context, cr, 0, 0, width, height);
481     gtk_render_background(context, cr, 0, 0, width, height);
482     cairo_restore(cr);
483 
484     gtk_style_context_restore(context);
485 
486     return FALSE;
487 }
488 # else
489     static gint
490 balloon_expose_event_cb(GtkWidget *widget,
491 			GdkEventExpose *event,
492 			gpointer data UNUSED)
493 {
494     gtk_paint_flat_box(widget->style, widget->window,
495 		       GTK_STATE_NORMAL, GTK_SHADOW_OUT,
496 		       &event->area, widget, "tooltip",
497 		       0, 0, -1, -1);
498 
499     return FALSE; /* continue emission */
500 }
501 # endif /* !GTK_CHECK_VERSION(3,0,0) */
502 
503 #else /* !FEAT_GUI_GTK */
504 
505     static void
506 addEventHandler(Widget target, BalloonEval *beval)
507 {
508     XtAddEventHandler(target,
509 			PointerMotionMask | EnterWindowMask |
510 			LeaveWindowMask | ButtonPressMask | KeyPressMask |
511 			KeyReleaseMask,
512 			False,
513 			pointerEventEH, (XtPointer)beval);
514 }
515 
516     static void
517 removeEventHandler(BalloonEval *beval)
518 {
519     XtRemoveEventHandler(beval->target,
520 			PointerMotionMask | EnterWindowMask |
521 			LeaveWindowMask | ButtonPressMask | KeyPressMask |
522 			KeyReleaseMask,
523 			False,
524 			pointerEventEH, (XtPointer)beval);
525 }
526 
527 
528 /*
529  * The X event handler. All it does is call the real event handler.
530  */
531     static void
532 pointerEventEH(
533     Widget	w UNUSED,
534     XtPointer	client_data,
535     XEvent	*event,
536     Boolean	*unused UNUSED)
537 {
538     BalloonEval *beval = (BalloonEval *)client_data;
539     pointerEvent(beval, event);
540 }
541 
542 
543 /*
544  * The real event handler. Called by pointerEventEH() whenever an event we are
545  * interested in occurs.
546  */
547 
548     static void
549 pointerEvent(BalloonEval *beval, XEvent *event)
550 {
551     Position	distance;	    /* a measure of how much the pointer moved */
552     Position	delta;		    /* used to compute distance */
553 
554     switch (event->type)
555     {
556 	case EnterNotify:
557 	case MotionNotify:
558 	    delta = event->xmotion.x - beval->x;
559 	    if (delta < 0)
560 		delta = -delta;
561 	    distance = delta;
562 	    delta = event->xmotion.y - beval->y;
563 	    if (delta < 0)
564 		delta = -delta;
565 	    distance += delta;
566 	    if (distance > 4)
567 	    {
568 		/*
569 		 * Moved out of the balloon location: cancel it.
570 		 * Remember button state
571 		 */
572 		beval->state = event->xmotion.state;
573 		if (beval->state & (Button1Mask|Button2Mask|Button3Mask))
574 		{
575 		    /* Mouse buttons are pressed - no balloon now */
576 		    cancelBalloon(beval);
577 		}
578 		else if (beval->state & (Mod1Mask|Mod2Mask|Mod3Mask))
579 		{
580 		    /*
581 		     * Alt is pressed -- enter super-evaluate-mode,
582 		     * where there is no time delay
583 		     */
584 		    beval->x = event->xmotion.x;
585 		    beval->y = event->xmotion.y;
586 		    beval->x_root = event->xmotion.x_root;
587 		    beval->y_root = event->xmotion.y_root;
588 		    cancelBalloon(beval);
589 		    if (beval->msgCB != NULL)
590 		    {
591 			beval->showState = ShS_PENDING;
592 			(*beval->msgCB)(beval, beval->state);
593 		    }
594 		}
595 		else
596 		{
597 		    beval->x = event->xmotion.x;
598 		    beval->y = event->xmotion.y;
599 		    beval->x_root = event->xmotion.x_root;
600 		    beval->y_root = event->xmotion.y_root;
601 		    cancelBalloon(beval);
602 		    beval->timerID = XtAppAddTimeOut( beval->appContext,
603 					(long_u)p_bdlay, timerRoutine, beval);
604 		}
605 	    }
606 	    break;
607 
608 	/*
609 	 * Motif and Athena version: Keystrokes will be caught by the
610 	 * "textArea" widget, and handled in gui_x11_key_hit_cb().
611 	 */
612 	case KeyPress:
613 	    if (beval->showState == ShS_SHOWING && beval->msgCB != NULL)
614 	    {
615 		Modifiers   modifier;
616 		KeySym	    keysym;
617 
618 		XtTranslateKeycode(gui.dpy,
619 				       event->xkey.keycode, event->xkey.state,
620 				       &modifier, &keysym);
621 		if (keysym == XK_Shift_L || keysym == XK_Shift_R)
622 		{
623 		    beval->showState = ShS_UPDATE_PENDING;
624 		    (*beval->msgCB)(beval, ShiftMask);
625 		}
626 		else if (keysym == XK_Control_L || keysym == XK_Control_R)
627 		{
628 		    beval->showState = ShS_UPDATE_PENDING;
629 		    (*beval->msgCB)(beval, ControlMask);
630 		}
631 		else
632 		    cancelBalloon(beval);
633 	    }
634 	    else
635 		cancelBalloon(beval);
636 	    break;
637 
638 	case KeyRelease:
639 	    if (beval->showState == ShS_SHOWING && beval->msgCB != NULL)
640 	    {
641 		Modifiers modifier;
642 		KeySym keysym;
643 
644 		XtTranslateKeycode(gui.dpy, event->xkey.keycode,
645 				event->xkey.state, &modifier, &keysym);
646 		if ((keysym == XK_Shift_L) || (keysym == XK_Shift_R)) {
647 		    beval->showState = ShS_UPDATE_PENDING;
648 		    (*beval->msgCB)(beval, 0);
649 		}
650 		else if ((keysym == XK_Control_L) || (keysym == XK_Control_R))
651 		{
652 		    beval->showState = ShS_UPDATE_PENDING;
653 		    (*beval->msgCB)(beval, 0);
654 		}
655 		else
656 		    cancelBalloon(beval);
657 	    }
658 	    else
659 		cancelBalloon(beval);
660 	    break;
661 
662 	case LeaveNotify:
663 		/* Ignore LeaveNotify events that are not "normal".
664 		 * Apparently we also get it when somebody else grabs focus.
665 		 * Happens for me every two seconds (some clipboard tool?) */
666 		if (event->xcrossing.mode == NotifyNormal)
667 		    cancelBalloon(beval);
668 		break;
669 
670 	case ButtonPress:
671 		cancelBalloon(beval);
672 		break;
673 
674 	default:
675 	    break;
676     }
677 }
678 
679     static void
680 timerRoutine(XtPointer dx, XtIntervalId *id UNUSED)
681 {
682     BalloonEval *beval = (BalloonEval *)dx;
683 
684     beval->timerID = (XtIntervalId)NULL;
685 
686     /*
687      * If the timer event happens then the mouse has stopped long enough for
688      * a request to be started. The request will only send to the debugger if
689      * there the mouse is pointing at real data.
690      */
691     requestBalloon(beval);
692 }
693 
694 #endif /* !FEAT_GUI_GTK */
695 
696     static void
697 requestBalloon(BalloonEval *beval)
698 {
699     if (beval->showState != ShS_PENDING)
700     {
701 	/* Determine the beval to display */
702 	if (beval->msgCB != NULL)
703 	{
704 	    beval->showState = ShS_PENDING;
705 	    (*beval->msgCB)(beval, beval->state);
706 	}
707 	else if (beval->msg != NULL)
708 	    drawBalloon(beval);
709     }
710 }
711 
712 #ifdef FEAT_GUI_GTK
713 /*
714  * Convert the string to UTF-8 if 'encoding' is not "utf-8".
715  * Replace any non-printable characters and invalid bytes sequences with
716  * "^X" or "<xx>" escapes, and apply SpecialKey highlighting to them.
717  * TAB and NL are passed through unscathed.
718  */
719 # define IS_NONPRINTABLE(c) (((c) < 0x20 && (c) != TAB && (c) != NL) \
720 			      || (c) == DEL)
721     static void
722 set_printable_label_text(GtkLabel *label, char_u *text)
723 {
724     char_u	    *convbuf = NULL;
725     char_u	    *buf;
726     char_u	    *p;
727     char_u	    *pdest;
728     unsigned int    len;
729     int		    charlen;
730     int		    uc;
731     PangoAttrList   *attr_list;
732 
733     /* Convert to UTF-8 if it isn't already */
734     if (output_conv.vc_type != CONV_NONE)
735     {
736 	convbuf = string_convert(&output_conv, text, NULL);
737 	if (convbuf != NULL)
738 	    text = convbuf;
739     }
740 
741     /* First let's see how much we need to allocate */
742     len = 0;
743     for (p = text; *p != NUL; p += charlen)
744     {
745 	if ((*p & 0x80) == 0)	/* be quick for ASCII */
746 	{
747 	    charlen = 1;
748 	    len += IS_NONPRINTABLE(*p) ? 2 : 1;	/* nonprintable: ^X */
749 	}
750 	else
751 	{
752 	    charlen = utf_ptr2len(p);
753 	    uc = utf_ptr2char(p);
754 
755 	    if (charlen != utf_char2len(uc))
756 		charlen = 1; /* reject overlong sequences */
757 
758 	    if (charlen == 1 || uc < 0xa0)	/* illegal byte or    */
759 		len += 4;			/* control char: <xx> */
760 	    else if (!utf_printable(uc))
761 		/* Note: we assume here that utf_printable() doesn't
762 		 * care about characters outside the BMP. */
763 		len += 6;			/* nonprintable: <xxxx> */
764 	    else
765 		len += charlen;
766 	}
767     }
768 
769     attr_list = pango_attr_list_new();
770     buf = alloc(len + 1);
771 
772     /* Now go for the real work */
773     if (buf != NULL)
774     {
775 	attrentry_T	*aep;
776 	PangoAttribute	*attr;
777 	guicolor_T	pixel;
778 #if GTK_CHECK_VERSION(3,0,0)
779 	GdkRGBA		color = { 0.0, 0.0, 0.0, 1.0 };
780 # if PANGO_VERSION_CHECK(1,38,0)
781 	PangoAttribute  *attr_alpha;
782 # endif
783 #else
784 	GdkColor	color = { 0, 0, 0, 0 };
785 #endif
786 
787 	/* Look up the RGB values of the SpecialKey foreground color. */
788 	aep = syn_gui_attr2entry(HL_ATTR(HLF_8));
789 	pixel = (aep != NULL) ? aep->ae_u.gui.fg_color : INVALCOLOR;
790 	if (pixel != INVALCOLOR)
791 # if GTK_CHECK_VERSION(3,0,0)
792 	{
793 	    color.red = ((pixel & 0xff0000) >> 16) / 255.0;
794 	    color.green = ((pixel & 0xff00) >> 8) / 255.0;
795 	    color.blue = (pixel & 0xff) / 255.0;
796 	    color.alpha = 1.0;
797 	}
798 # else
799 	    gdk_colormap_query_color(gtk_widget_get_colormap(gui.drawarea),
800 				     (unsigned long)pixel, &color);
801 # endif
802 
803 	pdest = buf;
804 	p = text;
805 	while (*p != NUL)
806 	{
807 	    /* Be quick for ASCII */
808 	    if ((*p & 0x80) == 0 && !IS_NONPRINTABLE(*p))
809 	    {
810 		*pdest++ = *p++;
811 	    }
812 	    else
813 	    {
814 		charlen = utf_ptr2len(p);
815 		uc = utf_ptr2char(p);
816 
817 		if (charlen != utf_char2len(uc))
818 		    charlen = 1; /* reject overlong sequences */
819 
820 		if (charlen == 1 || uc < 0xa0 || !utf_printable(uc))
821 		{
822 		    int	outlen;
823 
824 		    /* Careful: we can't just use transchar_byte() here,
825 		     * since 'encoding' is not necessarily set to "utf-8". */
826 		    if (*p & 0x80 && charlen == 1)
827 		    {
828 			transchar_hex(pdest, *p);	/* <xx> */
829 			outlen = 4;
830 		    }
831 		    else if (uc >= 0x80)
832 		    {
833 			/* Note: we assume here that utf_printable() doesn't
834 			 * care about characters outside the BMP. */
835 			transchar_hex(pdest, uc);	/* <xx> or <xxxx> */
836 			outlen = (uc < 0x100) ? 4 : 6;
837 		    }
838 		    else
839 		    {
840 			transchar_nonprint(pdest, *p);	/* ^X */
841 			outlen = 2;
842 		    }
843 		    if (pixel != INVALCOLOR)
844 		    {
845 #if GTK_CHECK_VERSION(3,0,0)
846 # define DOUBLE2UINT16(val) ((guint16)((val) * 65535 + 0.5))
847 			attr = pango_attr_foreground_new(
848 				DOUBLE2UINT16(color.red),
849 				DOUBLE2UINT16(color.green),
850 				DOUBLE2UINT16(color.blue));
851 # if PANGO_VERSION_CHECK(1,38,0)
852 			attr_alpha = pango_attr_foreground_alpha_new(
853 				DOUBLE2UINT16(color.alpha));
854 # endif
855 # undef DOUBLE2UINT16
856 #else
857 			attr = pango_attr_foreground_new(
858 				color.red, color.green, color.blue);
859 #endif
860 			attr->start_index = pdest - buf;
861 			attr->end_index   = pdest - buf + outlen;
862 			pango_attr_list_insert(attr_list, attr);
863 #if GTK_CHECK_VERSION(3,0,0)
864 # if PANGO_VERSION_CHECK(1,38,0)
865 			attr_alpha->start_index = pdest - buf;
866 			attr_alpha->end_index   = pdest - buf + outlen;
867 			pango_attr_list_insert(attr_list, attr_alpha);
868 # endif
869 #endif
870 		    }
871 		    pdest += outlen;
872 		    p += charlen;
873 		}
874 		else
875 		{
876 		    do
877 			*pdest++ = *p++;
878 		    while (--charlen != 0);
879 		}
880 	    }
881 	}
882 	*pdest = NUL;
883     }
884 
885     vim_free(convbuf);
886 
887     gtk_label_set_text(label, (const char *)buf);
888     vim_free(buf);
889 
890     gtk_label_set_attributes(label, attr_list);
891     pango_attr_list_unref(attr_list);
892 }
893 # undef IS_NONPRINTABLE
894 
895 /*
896  * Draw a balloon.
897  */
898     static void
899 drawBalloon(BalloonEval *beval)
900 {
901     if (beval->msg != NULL)
902     {
903 	GtkRequisition	requisition;
904 	int		screen_w;
905 	int		screen_h;
906 	int		screen_x;
907 	int		screen_y;
908 	int		x;
909 	int		y;
910 	int		x_offset = EVAL_OFFSET_X;
911 	int		y_offset = EVAL_OFFSET_Y;
912 	PangoLayout	*layout;
913 
914 # if !GTK_CHECK_VERSION(3,22,2)
915 	GdkScreen	*screen;
916 
917 	screen = gtk_widget_get_screen(beval->target);
918 	gtk_window_set_screen(GTK_WINDOW(beval->balloonShell), screen);
919 # endif
920 	gui_gtk_get_screen_geom_of_win(beval->target,
921 				    &screen_x, &screen_y, &screen_w, &screen_h);
922 # if !GTK_CHECK_VERSION(3,0,0)
923 	gtk_widget_ensure_style(beval->balloonShell);
924 	gtk_widget_ensure_style(beval->balloonLabel);
925 # endif
926 
927 	set_printable_label_text(GTK_LABEL(beval->balloonLabel), beval->msg);
928 	/*
929 	 * Dirty trick:  Enable wrapping mode on the label's layout behind its
930 	 * back.  This way GtkLabel won't try to constrain the wrap width to a
931 	 * builtin maximum value of about 65 Latin characters.
932 	 */
933 	layout = gtk_label_get_layout(GTK_LABEL(beval->balloonLabel));
934 # ifdef PANGO_WRAP_WORD_CHAR
935 	pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
936 # else
937 	pango_layout_set_wrap(layout, PANGO_WRAP_WORD);
938 # endif
939 	pango_layout_set_width(layout,
940 		/* try to come up with some reasonable width */
941 		PANGO_SCALE * CLAMP(gui.num_cols * gui.char_width,
942 				    screen_w / 2,
943 				    MAX(20, screen_w - 20)));
944 
945 	/* Calculate the balloon's width and height. */
946 # if GTK_CHECK_VERSION(3,0,0)
947 	gtk_widget_get_preferred_size(beval->balloonShell, &requisition, NULL);
948 # else
949 	gtk_widget_size_request(beval->balloonShell, &requisition);
950 # endif
951 
952 	/* Compute position of the balloon area */
953 	gdk_window_get_origin(gtk_widget_get_window(beval->target), &x, &y);
954 	x += beval->x;
955 	y += beval->y;
956 
957 	/* Get out of the way of the mouse pointer */
958 	if (x + x_offset + requisition.width > screen_x + screen_w)
959 	    y_offset += 15;
960 	if (y + y_offset + requisition.height > screen_y + screen_h)
961 	    y_offset = -requisition.height - EVAL_OFFSET_Y;
962 
963 	/* Sanitize values */
964 	x = CLAMP(x + x_offset, 0,
965 			    MAX(0, screen_x + screen_w - requisition.width));
966 	y = CLAMP(y + y_offset, 0,
967 			    MAX(0, screen_y + screen_h - requisition.height));
968 
969 	/* Show the balloon */
970 # if GTK_CHECK_VERSION(3,0,0)
971 	gtk_window_move(GTK_WINDOW(beval->balloonShell), x, y);
972 # else
973 	gtk_widget_set_uposition(beval->balloonShell, x, y);
974 # endif
975 	gtk_widget_show(beval->balloonShell);
976 
977 	beval->showState = ShS_SHOWING;
978     }
979 }
980 
981 /*
982  * Undraw a balloon.
983  */
984     static void
985 undrawBalloon(BalloonEval *beval)
986 {
987     if (beval->balloonShell != NULL)
988 	gtk_widget_hide(beval->balloonShell);
989     beval->showState = ShS_NEUTRAL;
990 }
991 
992     static void
993 cancelBalloon(BalloonEval *beval)
994 {
995     if (beval->showState == ShS_SHOWING
996 	    || beval->showState == ShS_UPDATE_PENDING)
997 	undrawBalloon(beval);
998 
999     if (beval->timerID != 0)
1000     {
1001 	g_source_remove(beval->timerID);
1002 	beval->timerID = 0;
1003     }
1004     beval->showState = ShS_NEUTRAL;
1005 }
1006 
1007     static void
1008 createBalloonEvalWindow(BalloonEval *beval)
1009 {
1010     beval->balloonShell = gtk_window_new(GTK_WINDOW_POPUP);
1011 
1012     gtk_widget_set_app_paintable(beval->balloonShell, TRUE);
1013     gtk_window_set_resizable(GTK_WINDOW(beval->balloonShell), FALSE);
1014     gtk_widget_set_name(beval->balloonShell, "gtk-tooltips");
1015     gtk_container_set_border_width(GTK_CONTAINER(beval->balloonShell), 4);
1016 
1017 # if GTK_CHECK_VERSION(3,0,0)
1018     g_signal_connect(G_OBJECT(beval->balloonShell), "draw",
1019 		     G_CALLBACK(balloon_draw_event_cb), NULL);
1020 # else
1021     gtk_signal_connect((GtkObject*)(beval->balloonShell), "expose_event",
1022 		       GTK_SIGNAL_FUNC(balloon_expose_event_cb), NULL);
1023 # endif
1024     beval->balloonLabel = gtk_label_new(NULL);
1025 
1026     gtk_label_set_line_wrap(GTK_LABEL(beval->balloonLabel), FALSE);
1027     gtk_label_set_justify(GTK_LABEL(beval->balloonLabel), GTK_JUSTIFY_LEFT);
1028 # if GTK_CHECK_VERSION(3,16,0)
1029     gtk_label_set_xalign(GTK_LABEL(beval->balloonLabel), 0.5);
1030     gtk_label_set_yalign(GTK_LABEL(beval->balloonLabel), 0.5);
1031 # elif GTK_CHECK_VERSION(3,14,0)
1032     GValue align_val = G_VALUE_INIT;
1033     g_value_init(&align_val, G_TYPE_FLOAT);
1034     g_value_set_float(&align_val, 0.5);
1035     g_object_set_property(G_OBJECT(beval->balloonLabel), "xalign", &align_val);
1036     g_object_set_property(G_OBJECT(beval->balloonLabel), "yalign", &align_val);
1037     g_value_unset(&align_val);
1038 # else
1039     gtk_misc_set_alignment(GTK_MISC(beval->balloonLabel), 0.5f, 0.5f);
1040 # endif
1041     gtk_widget_set_name(beval->balloonLabel, "vim-balloon-label");
1042     gtk_widget_show(beval->balloonLabel);
1043 
1044     gtk_container_add(GTK_CONTAINER(beval->balloonShell), beval->balloonLabel);
1045 }
1046 
1047 #else /* !FEAT_GUI_GTK */
1048 
1049 /*
1050  * Draw a balloon.
1051  */
1052     static void
1053 drawBalloon(BalloonEval *beval)
1054 {
1055     Dimension	w;
1056     Dimension	h;
1057     Position tx;
1058     Position ty;
1059 
1060     if (beval->msg != NULL)
1061     {
1062 	/* Show the Balloon */
1063 
1064 	/* Calculate the label's width and height */
1065 #ifdef FEAT_GUI_MOTIF
1066 	XmString s;
1067 
1068 	/* For the callback function we parse NL characters to create a
1069 	 * multi-line label.  This doesn't work for all languages, but
1070 	 * XmStringCreateLocalized() doesn't do multi-line labels... */
1071 	if (beval->msgCB != NULL)
1072 	    s = XmStringCreateLtoR((char *)beval->msg, XmFONTLIST_DEFAULT_TAG);
1073 	else
1074 	    s = XmStringCreateLocalized((char *)beval->msg);
1075 	{
1076 	    XmFontList fl;
1077 
1078 	    fl = gui_motif_fontset2fontlist(&gui.tooltip_fontset);
1079 	    if (fl == NULL)
1080 	    {
1081 		XmStringFree(s);
1082 		return;
1083 	    }
1084 	    XmStringExtent(fl, s, &w, &h);
1085 	    XmFontListFree(fl);
1086 	}
1087 	w += gui.border_offset << 1;
1088 	h += gui.border_offset << 1;
1089 	XtVaSetValues(beval->balloonLabel, XmNlabelString, s, NULL);
1090 	XmStringFree(s);
1091 #else /* Athena */
1092 	/* Assume XtNinternational == True */
1093 	XFontSet	fset;
1094 	XFontSetExtents *ext;
1095 
1096 	XtVaGetValues(beval->balloonLabel, XtNfontSet, &fset, NULL);
1097 	ext = XExtentsOfFontSet(fset);
1098 	h = ext->max_ink_extent.height;
1099 	w = XmbTextEscapement(fset,
1100 			      (char *)beval->msg,
1101 			      (int)STRLEN(beval->msg));
1102 	w += gui.border_offset << 1;
1103 	h += gui.border_offset << 1;
1104 	XtVaSetValues(beval->balloonLabel, XtNlabel, beval->msg, NULL);
1105 #endif
1106 
1107 	/* Compute position of the balloon area */
1108 	tx = beval->x_root + EVAL_OFFSET_X;
1109 	ty = beval->y_root + EVAL_OFFSET_Y;
1110 	if ((tx + w) > beval->screen_width)
1111 	    tx = beval->screen_width - w;
1112 	if ((ty + h) > beval->screen_height)
1113 	    ty = beval->screen_height - h;
1114 #ifdef FEAT_GUI_MOTIF
1115 	XtVaSetValues(beval->balloonShell,
1116 		XmNx, tx,
1117 		XmNy, ty,
1118 		NULL);
1119 #else
1120 	/* Athena */
1121 	XtVaSetValues(beval->balloonShell,
1122 		XtNx, tx,
1123 		XtNy, ty,
1124 		NULL);
1125 #endif
1126 	/* Set tooltip colors */
1127 	{
1128 	    Arg args[2];
1129 
1130 #ifdef FEAT_GUI_MOTIF
1131 	    args[0].name = XmNbackground;
1132 	    args[0].value = gui.tooltip_bg_pixel;
1133 	    args[1].name = XmNforeground;
1134 	    args[1].value = gui.tooltip_fg_pixel;
1135 #else /* Athena */
1136 	    args[0].name = XtNbackground;
1137 	    args[0].value = gui.tooltip_bg_pixel;
1138 	    args[1].name = XtNforeground;
1139 	    args[1].value = gui.tooltip_fg_pixel;
1140 #endif
1141 	    XtSetValues(beval->balloonLabel, &args[0], XtNumber(args));
1142 	}
1143 
1144 	XtPopup(beval->balloonShell, XtGrabNone);
1145 
1146 	beval->showState = ShS_SHOWING;
1147 
1148 	current_beval = beval;
1149     }
1150 }
1151 
1152 /*
1153  * Undraw a balloon.
1154  */
1155     static void
1156 undrawBalloon(BalloonEval *beval)
1157 {
1158     if (beval->balloonShell != (Widget)0)
1159 	XtPopdown(beval->balloonShell);
1160     beval->showState = ShS_NEUTRAL;
1161 
1162     current_beval = NULL;
1163 }
1164 
1165     static void
1166 cancelBalloon(BalloonEval *beval)
1167 {
1168     if (beval->showState == ShS_SHOWING
1169 	    || beval->showState == ShS_UPDATE_PENDING)
1170 	undrawBalloon(beval);
1171 
1172     if (beval->timerID != (XtIntervalId)NULL)
1173     {
1174 	XtRemoveTimeOut(beval->timerID);
1175 	beval->timerID = (XtIntervalId)NULL;
1176     }
1177     beval->showState = ShS_NEUTRAL;
1178 }
1179 
1180 
1181     static void
1182 createBalloonEvalWindow(BalloonEval *beval)
1183 {
1184     Arg		args[12];
1185     int		n;
1186 
1187     n = 0;
1188 #ifdef FEAT_GUI_MOTIF
1189     XtSetArg(args[n], XmNallowShellResize, True); n++;
1190     beval->balloonShell = XtAppCreateShell("balloonEval", "BalloonEval",
1191 		    overrideShellWidgetClass, gui.dpy, args, n);
1192 #else
1193     /* Athena */
1194     XtSetArg(args[n], XtNallowShellResize, True); n++;
1195     beval->balloonShell = XtAppCreateShell("balloonEval", "BalloonEval",
1196 		    overrideShellWidgetClass, gui.dpy, args, n);
1197 #endif
1198 
1199     n = 0;
1200 #ifdef FEAT_GUI_MOTIF
1201     {
1202 	XmFontList fl;
1203 
1204 	fl = gui_motif_fontset2fontlist(&gui.tooltip_fontset);
1205 	XtSetArg(args[n], XmNforeground, gui.tooltip_fg_pixel); n++;
1206 	XtSetArg(args[n], XmNbackground, gui.tooltip_bg_pixel); n++;
1207 	XtSetArg(args[n], XmNfontList, fl); n++;
1208 	XtSetArg(args[n], XmNalignment, XmALIGNMENT_BEGINNING); n++;
1209 	beval->balloonLabel = XtCreateManagedWidget("balloonLabel",
1210 			xmLabelWidgetClass, beval->balloonShell, args, n);
1211     }
1212 #else /* FEAT_GUI_ATHENA */
1213     XtSetArg(args[n], XtNforeground, gui.tooltip_fg_pixel); n++;
1214     XtSetArg(args[n], XtNbackground, gui.tooltip_bg_pixel); n++;
1215     XtSetArg(args[n], XtNinternational, True); n++;
1216     XtSetArg(args[n], XtNfontSet, gui.tooltip_fontset); n++;
1217     beval->balloonLabel = XtCreateManagedWidget("balloonLabel",
1218 		    labelWidgetClass, beval->balloonShell, args, n);
1219 #endif
1220 }
1221 
1222 #endif /* !FEAT_GUI_GTK */
1223 #endif /* !FEAT_GUI_MSWIN */
1224 
1225 #endif /* FEAT_BEVAL_GUI */
1226