1 /******************************************************************************* 2 * Copyright (c) 2006, 2017 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * Tom Schindl <tom.schindl@bestsolution.at> - initial API and implementation 13 * bugfix in: 195137, 198089, 225190 14 *******************************************************************************/ 15 16 package org.eclipse.jface.window; 17 18 import java.util.HashMap; 19 20 import org.eclipse.jface.viewers.ColumnViewer; 21 import org.eclipse.jface.viewers.ViewerCell; 22 import org.eclipse.swt.SWT; 23 import org.eclipse.swt.graphics.Point; 24 import org.eclipse.swt.graphics.Rectangle; 25 import org.eclipse.swt.layout.FillLayout; 26 import org.eclipse.swt.widgets.Composite; 27 import org.eclipse.swt.widgets.Control; 28 import org.eclipse.swt.widgets.Event; 29 import org.eclipse.swt.widgets.Listener; 30 import org.eclipse.swt.widgets.Monitor; 31 import org.eclipse.swt.widgets.Shell; 32 33 /** 34 * This class gives implementors to provide customized tooltips for any control. 35 * 36 * @since 3.3 37 */ 38 public abstract class ToolTip { 39 private Control control; 40 41 private int xShift = 3; 42 43 private int yShift = 0; 44 45 private int popupDelay = 0; 46 47 private int hideDelay = 0; 48 49 private ToolTipOwnerControlListener listener; 50 51 private HashMap<String, Object> data; 52 53 // Ensure that only one tooltip is active in time 54 private static Shell CURRENT_TOOLTIP; 55 56 /** 57 * Recreate the tooltip on every mouse move 58 */ 59 public static final int RECREATE = 1; 60 61 /** 62 * Don't recreate the tooltip as long the mouse doesn't leave the area 63 * triggering the tooltip creation 64 */ 65 public static final int NO_RECREATE = 1 << 1; 66 67 private TooltipHideListener hideListener = new TooltipHideListener(); 68 69 private Listener shellListener; 70 71 private boolean hideOnMouseDown = true; 72 73 private boolean respectDisplayBounds = true; 74 75 private boolean respectMonitorBounds = true; 76 77 private int style; 78 79 private Object currentArea; 80 81 /** 82 * Create new instance which add TooltipSupport to the widget 83 * 84 * @param control 85 * the control on whose action the tooltip is shown 86 */ ToolTip(Control control)87 public ToolTip(Control control) { 88 this(control, RECREATE, false); 89 } 90 91 /** 92 * @param control 93 * the control to which the tooltip is bound 94 * @param style 95 * style passed to control tooltip behavior 96 * 97 * @param manualActivation 98 * <code>true</code> if the activation is done manually using 99 * {@link #show(Point)} 100 * @see #RECREATE 101 * @see #NO_RECREATE 102 */ ToolTip(Control control, int style, boolean manualActivation)103 public ToolTip(Control control, int style, boolean manualActivation) { 104 this.control = control; 105 this.style = style; 106 this.listener = new ToolTipOwnerControlListener(); 107 this.shellListener = event -> { 108 if (ToolTip.this.control != null 109 && !ToolTip.this.control.isDisposed()) { 110 ToolTip.this.control.getDisplay().asyncExec(() -> { 111 // Check if the new active shell is the tooltip 112 // itself 113 if (ToolTip.this.control != null && !ToolTip.this.control.isDisposed() 114 && ToolTip.this.control.getDisplay().getActiveShell() != CURRENT_TOOLTIP) { 115 toolTipHide(CURRENT_TOOLTIP, event); 116 } 117 }); 118 } 119 }; 120 121 if (!manualActivation) { 122 activate(); 123 } 124 } 125 126 /** 127 * Restore arbitrary data under the given key 128 * 129 * @param key 130 * the key 131 * @param value 132 * the value 133 */ setData(String key, Object value)134 public void setData(String key, Object value) { 135 if (data == null) { 136 data = new HashMap<>(); 137 } 138 data.put(key, value); 139 } 140 141 /** 142 * Get the data restored under the key 143 * 144 * @param key 145 * the key 146 * @return data or <code>null</code> if no entry is restored under the key 147 */ getData(String key)148 public Object getData(String key) { 149 if (data != null) { 150 return data.get(key); 151 } 152 return null; 153 } 154 155 /** 156 * Set the shift (from the mouse position triggered the event) used to 157 * display the tooltip. 158 * <p> 159 * By default the tooltip is shifted 3 pixels to the right. 160 * </p> 161 * 162 * @param p 163 * the new shift 164 */ setShift(Point p)165 public void setShift(Point p) { 166 xShift = p.x; 167 yShift = p.y; 168 } 169 170 /** 171 * Activate tooltip support for this control 172 */ activate()173 public void activate() { 174 deactivate(); 175 control.addListener(SWT.Dispose, listener); 176 control.addListener(SWT.MouseHover, listener); 177 control.addListener(SWT.MouseMove, listener); 178 control.addListener(SWT.MouseExit, listener); 179 control.addListener(SWT.MouseDown, listener); 180 control.addListener(SWT.MouseWheel, listener); 181 } 182 183 /** 184 * Deactivate tooltip support for the underlying control 185 */ deactivate()186 public void deactivate() { 187 control.removeListener(SWT.Dispose, listener); 188 control.removeListener(SWT.MouseHover, listener); 189 control.removeListener(SWT.MouseMove, listener); 190 control.removeListener(SWT.MouseExit, listener); 191 control.removeListener(SWT.MouseDown, listener); 192 control.removeListener(SWT.MouseWheel, listener); 193 } 194 195 /** 196 * Return whether the tooltip respects bounds of the display. 197 * 198 * @return <code>true</code> if the tooltip respects bounds of the display 199 */ isRespectDisplayBounds()200 public boolean isRespectDisplayBounds() { 201 return respectDisplayBounds; 202 } 203 204 /** 205 * Set to <code>false</code> if display bounds should not be respected or to 206 * <code>true</code> if the tooltip is should repositioned to not overlap the 207 * display bounds. 208 * <p> 209 * Default is <code>true</code> 210 * </p> 211 * 212 * @param respectDisplayBounds <code>false</code> if tooltip is allowed to 213 * overlap display bounds 214 */ setRespectDisplayBounds(boolean respectDisplayBounds)215 public void setRespectDisplayBounds(boolean respectDisplayBounds) { 216 this.respectDisplayBounds = respectDisplayBounds; 217 } 218 219 /** 220 * Return whether the tooltip respects bounds of the monitor. 221 * 222 * @return <code>true</code> if tooltip respects the bounds of the monitor 223 */ isRespectMonitorBounds()224 public boolean isRespectMonitorBounds() { 225 return respectMonitorBounds; 226 } 227 228 /** 229 * Set to <code>false</code> if monitor bounds should not be respected or to 230 * <code>true</code> if the tooltip is should repositioned to not overlap the 231 * monitors bounds. The monitor the tooltip belongs to is the same is control's 232 * monitor the tooltip is shown for. 233 * <p> 234 * Default is <code>true</code> 235 * </p> 236 * 237 * @param respectMonitorBounds <code>false</code> if tooltip is allowed to 238 * overlap monitor bounds 239 */ setRespectMonitorBounds(boolean respectMonitorBounds)240 public void setRespectMonitorBounds(boolean respectMonitorBounds) { 241 this.respectMonitorBounds = respectMonitorBounds; 242 } 243 244 /** 245 * Should the tooltip displayed because of the given event. 246 * <p> 247 * <b>Subclasses may overwrite this to get custom behavior</b> 248 * </p> 249 * 250 * @param event 251 * the event 252 * @return <code>true</code> if tooltip should be displayed 253 */ shouldCreateToolTip(Event event)254 protected boolean shouldCreateToolTip(Event event) { 255 if ((style & NO_RECREATE) != 0) { 256 Object tmp = getToolTipArea(event); 257 258 // No new area close the current tooltip 259 if (tmp == null) { 260 hide(); 261 return false; 262 } 263 264 return !tmp.equals(currentArea); 265 } 266 267 return true; 268 } 269 270 /** 271 * This method is called before the tooltip is hidden 272 * 273 * @param event 274 * the event trying to hide the tooltip 275 * @return <code>true</code> if the tooltip should be hidden 276 */ shouldHideToolTip(Event event)277 private boolean shouldHideToolTip(Event event) { 278 if (event != null && event.type == SWT.MouseMove 279 && (style & NO_RECREATE) != 0) { 280 Object tmp = getToolTipArea(event); 281 282 // No new area close the current tooltip 283 if (tmp == null) { 284 hide(); 285 return false; 286 } 287 288 return !tmp.equals(currentArea); 289 } 290 291 return true; 292 } 293 294 /** 295 * This method is called to check for which area the tooltip is 296 * created/hidden for. In case of {@link #NO_RECREATE} this is used to 297 * decide if the tooltip is hidden recreated. 298 * 299 * <code>By the default it is the widget the tooltip is created for but could be any object. To decide if 300 * the area changed the {@link Object#equals(Object)} method is used.</code> 301 * 302 * @param event 303 * the event 304 * @return the area responsible for the tooltip creation or 305 * <code>null</code> this could be any object describing the area 306 * (e.g. the {@link Control} onto which the tooltip is bound to, a 307 * part of this area e.g. for {@link ColumnViewer} this could be a 308 * {@link ViewerCell}) 309 */ getToolTipArea(Event event)310 protected Object getToolTipArea(Event event) { 311 return control; 312 } 313 314 /** 315 * Start up the tooltip programmatically 316 * 317 * @param location 318 * the location relative to the control the tooltip is shown 319 */ show(Point location)320 public void show(Point location) { 321 Event event = new Event(); 322 event.x = location.x; 323 event.y = location.y; 324 event.widget = control; 325 toolTipCreate(event); 326 } 327 toolTipCreate(final Event event)328 private Shell toolTipCreate(final Event event) { 329 if (shouldCreateToolTip(event)) { 330 Shell shell = new Shell(control.getShell(), SWT.ON_TOP | SWT.TOOL 331 | SWT.NO_FOCUS); 332 shell.setLayout(new FillLayout()); 333 334 toolTipOpen(shell, event); 335 336 return shell; 337 } 338 339 return null; 340 } 341 toolTipShow(Shell tip, Event event)342 private void toolTipShow(Shell tip, Event event) { 343 if (!tip.isDisposed()) { 344 currentArea = getToolTipArea(event); 345 createToolTipContentArea(event, tip); 346 if (isHideOnMouseDown()) { 347 toolTipHookBothRecursively(tip); 348 } else { 349 toolTipHookByTypeRecursively(tip, true, SWT.MouseExit); 350 } 351 352 tip.pack(); 353 Point size = tip.getSize(); 354 Point location = fixupDisplayBounds(size, getLocation(size, event)); 355 356 // Need to adjust a bit more if the mouse cursor.y == tip.y and 357 // the cursor.x is inside the tip 358 Point cursorLocation = tip.getDisplay().getCursorLocation(); 359 360 if (cursorLocation.y == location.y && location.x < cursorLocation.x 361 && location.x + size.x > cursorLocation.x) { 362 location.y -= 2; 363 } 364 365 tip.setLocation(location); 366 tip.setVisible(true); 367 } 368 } 369 fixupDisplayBounds(Point tipSize, Point location)370 private Point fixupDisplayBounds(Point tipSize, Point location) { 371 if (respectDisplayBounds || respectMonitorBounds) { 372 Rectangle bounds; 373 Point rightBounds = new Point(tipSize.x + location.x, tipSize.y 374 + location.y); 375 376 Monitor[] ms = control.getDisplay().getMonitors(); 377 378 if (respectMonitorBounds && ms.length > 1) { 379 // By default present in the monitor of the control 380 bounds = control.getMonitor().getBounds(); 381 Point p = new Point(location.x, location.y); 382 383 // Search on which monitor the event occurred 384 Rectangle tmp; 385 for (Monitor element : ms) { 386 tmp = element.getBounds(); 387 if (tmp.contains(p)) { 388 bounds = tmp; 389 break; 390 } 391 } 392 393 } else { 394 bounds = control.getDisplay().getBounds(); 395 } 396 397 if (!(bounds.contains(location) && bounds.contains(rightBounds))) { 398 if (rightBounds.x > bounds.x + bounds.width) { 399 location.x -= rightBounds.x - (bounds.x + bounds.width); 400 } 401 402 if (rightBounds.y > bounds.y + bounds.height) { 403 location.y -= rightBounds.y - (bounds.y + bounds.height); 404 } 405 406 if (location.x < bounds.x) { 407 location.x = bounds.x; 408 } 409 410 if (location.y < bounds.y) { 411 location.y = bounds.y; 412 } 413 } 414 } 415 416 return location; 417 } 418 419 /** 420 * Get the display relative location where the tooltip is displayed. 421 * Subclasses may overwrite to implement custom positioning. 422 * 423 * @param tipSize 424 * the size of the tooltip to be shown 425 * @param event 426 * the event triggered showing the tooltip 427 * @return the absolute position on the display 428 */ getLocation(Point tipSize, Event event)429 public Point getLocation(Point tipSize, Event event) { 430 return control.toDisplay(event.x + xShift, event.y + yShift); 431 } 432 toolTipHide(Shell tip, Event event)433 private void toolTipHide(Shell tip, Event event) { 434 if (tip != null && !tip.isDisposed() && shouldHideToolTip(event)) { 435 control.getShell().removeListener(SWT.Deactivate, shellListener); 436 currentArea = null; 437 passOnEvent(tip, event); 438 tip.dispose(); 439 CURRENT_TOOLTIP = null; 440 afterHideToolTip(event); 441 } 442 if (event != null && event.type == SWT.Dispose) { 443 deactivate(); 444 data = null; 445 } 446 } 447 passOnEvent(Shell tip, Event event)448 private void passOnEvent(Shell tip, Event event) { 449 if (control != null && !control.isDisposed() && event != null 450 && event.widget != control && event.type == SWT.MouseDown) { 451 // the following was left in order to fix bug 298770 with minimal change. In 3.7, the complete method should be removed. 452 tip.close(); 453 } 454 } 455 toolTipOpen(final Shell shell, final Event event)456 private void toolTipOpen(final Shell shell, final Event event) { 457 // Ensure that only one Tooltip is shown in time 458 if (CURRENT_TOOLTIP != null) { 459 toolTipHide(CURRENT_TOOLTIP, null); 460 } 461 462 CURRENT_TOOLTIP = shell; 463 464 control.getShell().addListener(SWT.Deactivate, shellListener); 465 466 if (popupDelay > 0) { 467 control.getDisplay().timerExec(popupDelay, () -> toolTipShow(shell, event)); 468 } else { 469 toolTipShow(CURRENT_TOOLTIP, event); 470 } 471 472 if (hideDelay > 0) { 473 control.getDisplay().timerExec(popupDelay + hideDelay, 474 () -> toolTipHide(shell, null)); 475 } 476 } 477 toolTipHookByTypeRecursively(Control c, boolean add, int type)478 private void toolTipHookByTypeRecursively(Control c, boolean add, int type) { 479 if (add) { 480 c.addListener(type, hideListener); 481 } else { 482 c.removeListener(type, hideListener); 483 } 484 485 if (c instanceof Composite) { 486 Control[] children = ((Composite) c).getChildren(); 487 for (Control element : children) { 488 toolTipHookByTypeRecursively(element, add, type); 489 } 490 } 491 } 492 toolTipHookBothRecursively(Control c)493 private void toolTipHookBothRecursively(Control c) { 494 c.addListener(SWT.MouseDown, hideListener); 495 c.addListener(SWT.MouseExit, hideListener); 496 497 if (c instanceof Composite) { 498 Control[] children = ((Composite) c).getChildren(); 499 for (Control element : children) { 500 toolTipHookBothRecursively(element); 501 } 502 } 503 } 504 505 /** 506 * Creates the content area of the the tooltip. 507 * 508 * @param event 509 * the event that triggered the activation of the tooltip 510 * @param parent 511 * the parent of the content area 512 * @return the content area created 513 */ createToolTipContentArea(Event event, Composite parent)514 protected abstract Composite createToolTipContentArea(Event event, 515 Composite parent); 516 517 /** 518 * This method is called after a tooltip is hidden. 519 * <p> 520 * <b>Subclasses may override to clean up requested system resources</b> 521 * </p> 522 * 523 * @param event 524 * event triggered the hiding action (may be <code>null</code> 525 * if event wasn't triggered by user actions directly) 526 */ afterHideToolTip(Event event)527 protected void afterHideToolTip(Event event) { 528 529 } 530 531 /** 532 * Set the hide delay. 533 * 534 * @param hideDelay 535 * the delay before the tooltip is hidden. If <code>0</code> 536 * the tooltip is shown until user moves to other item 537 */ setHideDelay(int hideDelay)538 public void setHideDelay(int hideDelay) { 539 this.hideDelay = hideDelay; 540 } 541 542 /** 543 * Set the popup delay. 544 * 545 * @param popupDelay 546 * the delay before the tooltip is shown to the user. If 547 * <code>0</code> the tooltip is shown immediately 548 */ setPopupDelay(int popupDelay)549 public void setPopupDelay(int popupDelay) { 550 this.popupDelay = popupDelay; 551 } 552 553 /** 554 * Return if hiding on mouse down is set. 555 * 556 * @return <code>true</code> if hiding on mouse down in the tool tip is on 557 */ isHideOnMouseDown()558 public boolean isHideOnMouseDown() { 559 return hideOnMouseDown; 560 } 561 562 /** 563 * If you don't want the tool tip to be hidden when the user clicks inside 564 * the tool tip set this to <code>false</code>. You maybe also need to 565 * hide the tool tip yourself depending on what you do after clicking in the 566 * tooltip (e.g. if you open a new {@link Shell}) 567 * 568 * @param hideOnMouseDown 569 * flag to indicate of tooltip is hidden automatically on mouse 570 * down inside the tool tip 571 */ setHideOnMouseDown(final boolean hideOnMouseDown)572 public void setHideOnMouseDown(final boolean hideOnMouseDown) { 573 // Only needed if there's currently a tooltip active 574 if (CURRENT_TOOLTIP != null && !CURRENT_TOOLTIP.isDisposed()) { 575 // Only change if value really changed 576 if (hideOnMouseDown != this.hideOnMouseDown) { 577 control.getDisplay().syncExec(() -> { 578 if (CURRENT_TOOLTIP != null 579 && CURRENT_TOOLTIP.isDisposed()) { 580 toolTipHookByTypeRecursively(CURRENT_TOOLTIP, 581 hideOnMouseDown, SWT.MouseDown); 582 } 583 }); 584 } 585 } 586 587 this.hideOnMouseDown = hideOnMouseDown; 588 } 589 590 /** 591 * Hide the currently active tool tip 592 */ hide()593 public void hide() { 594 toolTipHide(CURRENT_TOOLTIP, null); 595 } 596 597 private class ToolTipOwnerControlListener implements Listener { 598 @Override handleEvent(Event event)599 public void handleEvent(Event event) { 600 switch (event.type) { 601 case SWT.Dispose: 602 case SWT.KeyDown: 603 case SWT.MouseDown: 604 case SWT.MouseMove: 605 case SWT.MouseWheel: 606 toolTipHide(CURRENT_TOOLTIP, event); 607 break; 608 case SWT.MouseHover: 609 toolTipCreate(event); 610 break; 611 case SWT.MouseExit: 612 /* 613 * Check if the mouse exit happened because we move over the 614 * tooltip 615 */ 616 if (CURRENT_TOOLTIP != null && !CURRENT_TOOLTIP.isDisposed()) { 617 if (CURRENT_TOOLTIP.getBounds().contains( 618 control.toDisplay(event.x, event.y))) { 619 break; 620 } 621 } 622 623 toolTipHide(CURRENT_TOOLTIP, event); 624 break; 625 } 626 } 627 } 628 629 private class TooltipHideListener implements Listener { 630 @Override handleEvent(Event event)631 public void handleEvent(Event event) { 632 if (event.widget instanceof Control) { 633 634 Control c = (Control) event.widget; 635 Shell shell = c.getShell(); 636 637 switch (event.type) { 638 case SWT.MouseDown: 639 if (isHideOnMouseDown()) { 640 toolTipHide(shell, event); 641 } 642 break; 643 case SWT.MouseExit: 644 Rectangle rect = shell.getBounds(); 645 if (!rect.contains(c.getDisplay().getCursorLocation())) { 646 toolTipHide(shell, event); 647 } 648 649 break; 650 } 651 } 652 } 653 } 654 } 655