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