001: /*******************************************************************************
002: * Copyright (c) 2006, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * Tom Schindl <tom.schindl@bestsolution.at> - initial API and implementation
010: *******************************************************************************/package org.eclipse.jface.window;
011:
012: import java.util.HashMap;
013:
014: import org.eclipse.jface.viewers.ColumnViewer;
015: import org.eclipse.jface.viewers.ViewerCell;
016: import org.eclipse.swt.SWT;
017: import org.eclipse.swt.events.DisposeEvent;
018: import org.eclipse.swt.events.DisposeListener;
019: import org.eclipse.swt.graphics.Point;
020: import org.eclipse.swt.graphics.Rectangle;
021: import org.eclipse.swt.layout.FillLayout;
022: import org.eclipse.swt.widgets.Composite;
023: import org.eclipse.swt.widgets.Control;
024: import org.eclipse.swt.widgets.Event;
025: import org.eclipse.swt.widgets.Listener;
026: import org.eclipse.swt.widgets.Monitor;
027: import org.eclipse.swt.widgets.Shell;
028:
029: /**
030: * This class gives implementors to provide customized tooltips for any control.
031: *
032: * @since 3.3
033: */
034: public abstract class ToolTip {
035: private Control control;
036:
037: private int xShift = 3;
038:
039: private int yShift = 0;
040:
041: private int popupDelay = 0;
042:
043: private int hideDelay = 0;
044:
045: private ToolTipOwnerControlListener listener;
046:
047: private HashMap data;
048:
049: // Ensure that only one tooltip is active in time
050: private static Shell CURRENT_TOOLTIP;
051:
052: /**
053: * Recreate the tooltip on every mouse move
054: */
055: public static final int RECREATE = 1;
056:
057: /**
058: * Don't recreate the tooltip as long the mouse doesn't leave the area
059: * triggering the Tooltip creation
060: */
061: public static final int NO_RECREATE = 1 << 1;
062:
063: private TooltipHideListener hideListener = new TooltipHideListener();
064:
065: private boolean hideOnMouseDown = true;
066:
067: private boolean respectDisplayBounds = true;
068:
069: private boolean respectMonitorBounds = true;
070:
071: private int style;
072:
073: private Object currentArea;
074:
075: /**
076: * Create new instance which add TooltipSupport to the widget
077: *
078: * @param control
079: * the control on whose action the tooltip is shown
080: */
081: public ToolTip(Control control) {
082: this (control, RECREATE, false);
083: }
084:
085: /**
086: * @param control
087: * the control to which the tooltip is bound
088: * @param style
089: * style passed to control tooltip behaviour
090: *
091: * @param manualActivation
092: * <code>true</code> if the activation is done manually using
093: * {@link #show(Point)}
094: * @see #RECREATE
095: * @see #NO_RECREATE
096: */
097: public ToolTip(Control control, int style, boolean manualActivation) {
098: this .control = control;
099: this .style = style;
100: this .control.addDisposeListener(new DisposeListener() {
101:
102: public void widgetDisposed(DisposeEvent e) {
103: deactivate();
104: }
105:
106: });
107:
108: this .listener = new ToolTipOwnerControlListener();
109:
110: if (!manualActivation) {
111: activate();
112: }
113: }
114:
115: /**
116: * Restore arbitary data under the given key
117: *
118: * @param key
119: * the key
120: * @param value
121: * the value
122: */
123: public void setData(String key, Object value) {
124: if (data == null) {
125: data = new HashMap();
126: }
127: data.put(key, value);
128: }
129:
130: /**
131: * Get the data restored under the key
132: *
133: * @param key
134: * the key
135: * @return data or <code>null</code> if no entry is restored under the key
136: */
137: public Object getData(String key) {
138: if (data != null) {
139: return data.get(key);
140: }
141: return null;
142: }
143:
144: /**
145: * Set the shift (from the mouse position triggered the event) used to
146: * display the tooltip. By default the tooltip is shifted 3 pixels to the
147: * left
148: *
149: * @param p
150: * the new shift
151: */
152: public void setShift(Point p) {
153: xShift = p.x;
154: yShift = p.y;
155: }
156:
157: /**
158: * Activate tooltip support for this control
159: */
160: public void activate() {
161: deactivate();
162: control.addListener(SWT.Dispose, listener);
163: control.addListener(SWT.MouseHover, listener);
164: control.addListener(SWT.MouseMove, listener);
165: control.addListener(SWT.MouseExit, listener);
166: control.addListener(SWT.MouseDown, listener);
167: }
168:
169: /**
170: * Deactivate tooltip support for the underlying control
171: */
172: public void deactivate() {
173: control.removeListener(SWT.Dispose, listener);
174: control.removeListener(SWT.MouseHover, listener);
175: control.removeListener(SWT.MouseMove, listener);
176: control.removeListener(SWT.MouseExit, listener);
177: control.removeListener(SWT.MouseDown, listener);
178: }
179:
180: /**
181: * Return whther the tooltip respects bounds of the display.
182: *
183: * @return <code>true</code> if the tooltip respects bounds of the display
184: */
185: public boolean isRespectDisplayBounds() {
186: return respectDisplayBounds;
187: }
188:
189: /**
190: * Set to <code>false</code> if display bounds should not be respected or
191: * to <code>true</code> if the tooltip is should repositioned to not
192: * overlap the display bounds.
193: * <p>
194: * Default is <code>true</code>
195: * </p>
196: *
197: * @param respectDisplayBounds
198: */
199: public void setRespectDisplayBounds(boolean respectDisplayBounds) {
200: this .respectDisplayBounds = respectDisplayBounds;
201: }
202:
203: /**
204: * Return whther the tooltip respects bounds of the monitor.
205: *
206: * @return <code>true</code> if tooltip respects the bounds of the monitor
207: */
208: public boolean isRespectMonitorBounds() {
209: return respectMonitorBounds;
210: }
211:
212: /**
213: * Set to <code>false</code> if monitor bounds should not be respected or
214: * to <code>true</code> if the tooltip is should repositioned to not
215: * overlap the monitors bounds. The monitor the tooltip belongs to is the
216: * same is control's monitor the tooltip is shown for.
217: * <p>
218: * Default is <code>true</code>
219: * </p>
220: *
221: * @param respectMonitorBounds
222: */
223: public void setRespectMonitorBounds(boolean respectMonitorBounds) {
224: this .respectMonitorBounds = respectMonitorBounds;
225: }
226:
227: /**
228: * Should the tooltip displayed because of the given event.
229: * <p>
230: * <b>Subclasses may overwrite this to get custom behaviour</b>
231: * </p>
232: *
233: * @param event
234: * the event
235: * @return <code>true</code> if tooltip should be displayed
236: */
237: protected boolean shouldCreateToolTip(Event event) {
238: if ((style & NO_RECREATE) != 0) {
239: Object tmp = getToolTipArea(event);
240:
241: // No new area close the current tooltip
242: if (tmp == null) {
243: hide();
244: return false;
245: }
246:
247: boolean rv = !tmp.equals(currentArea);
248: return rv;
249: }
250:
251: return true;
252: }
253:
254: /**
255: * This method is called before the tooltip is hidden
256: *
257: * @param event
258: * the event trying to hide the tooltip
259: * @return <code>true</code> if the tooltip should be hidden
260: */
261: private boolean shouldHideToolTip(Event event) {
262: if (event != null && event.type == SWT.MouseMove
263: && (style & NO_RECREATE) != 0) {
264: Object tmp = getToolTipArea(event);
265:
266: // No new area close the current tooltip
267: if (tmp == null) {
268: hide();
269: return false;
270: }
271:
272: boolean rv = !tmp.equals(currentArea);
273: return rv;
274: }
275:
276: return true;
277: }
278:
279: /**
280: * This method is called to check for which area the tooltip is
281: * created/hidden for. In case of {@link #NO_RECREATE} this is used to
282: * decide if the tooltip is hidden recreated.
283: *
284: * <code>By the default it is the widget the tooltip is created for but could be any object. To decide if
285: * the area changed the {@link Object#equals(Object)} method is used.</code>
286: *
287: * @param event
288: * the event
289: * @return the area responsible for the tooltip creation or
290: * <code>null</code> this could be any object describing the area
291: * (e.g. the {@link Control} onto which the tooltip is bound to, a part of
292: * this area e.g. for {@link ColumnViewer} this could be a
293: * {@link ViewerCell})
294: */
295: protected Object getToolTipArea(Event event) {
296: return control;
297: }
298:
299: /**
300: * Start up the tooltip programmatically
301: *
302: * @param location
303: * the location relative to the control the tooltip is shown
304: */
305: public void show(Point location) {
306: Event event = new Event();
307: event.x = location.x;
308: event.y = location.y;
309: event.widget = control;
310: toolTipCreate(event);
311: }
312:
313: private Shell toolTipCreate(final Event event) {
314: if (shouldCreateToolTip(event)) {
315: Shell shell = new Shell(control.getShell(), SWT.ON_TOP
316: | SWT.TOOL | SWT.NO_FOCUS);
317: shell.setLayout(new FillLayout());
318:
319: toolTipOpen(shell, event);
320:
321: return shell;
322: }
323:
324: return null;
325: }
326:
327: private void toolTipShow(Shell tip, Event event) {
328: if (!tip.isDisposed()) {
329: currentArea = getToolTipArea(event);
330: createToolTipContentArea(event, tip);
331: if (isHideOnMouseDown()) {
332: toolTipHookBothRecursively(tip);
333: } else {
334: toolTipHookByTypeRecursively(tip, true, SWT.MouseExit);
335: }
336:
337: tip.pack();
338: tip.setLocation(fixupDisplayBounds(tip.getSize(),
339: getLocation(tip.getSize(), event)));
340: tip.setVisible(true);
341: }
342: }
343:
344: private Point fixupDisplayBounds(Point tipSize, Point location) {
345: if (respectDisplayBounds || respectMonitorBounds) {
346: Rectangle bounds;
347: Point rightBounds = new Point(tipSize.x + location.x,
348: tipSize.y + location.y);
349:
350: Monitor[] ms = control.getDisplay().getMonitors();
351:
352: if (respectMonitorBounds && ms.length > 1) {
353: // By default present in the monitor of the control
354: bounds = control.getMonitor().getBounds();
355: Point p = new Point(location.x, location.y);
356:
357: // Search on which monitor the event occurred
358: Rectangle tmp;
359: for (int i = 0; i < ms.length; i++) {
360: tmp = ms[i].getBounds();
361: if (tmp.contains(p)) {
362: bounds = tmp;
363: break;
364: }
365: }
366:
367: } else {
368: bounds = control.getDisplay().getBounds();
369: }
370:
371: if (!(bounds.contains(location) && bounds
372: .contains(rightBounds))) {
373: if (rightBounds.x > bounds.width) {
374: location.x -= rightBounds.x - bounds.width;
375: }
376:
377: if (rightBounds.y > bounds.height) {
378: location.y -= rightBounds.y - bounds.height;
379: }
380:
381: if (location.x < bounds.x) {
382: location.x = bounds.x;
383: }
384:
385: if (location.y < bounds.y) {
386: location.y = bounds.y;
387: }
388: }
389: }
390:
391: return location;
392: }
393:
394: /**
395: * Get the display relative location where the tooltip is displayed.
396: * Subclasses may overwrite to implement custom positioning.
397: *
398: * @param tipSize
399: * the size of the tooltip to be shown
400: * @param event
401: * the event triggered showing the tooltip
402: * @return the absolute position on the display
403: */
404: public Point getLocation(Point tipSize, Event event) {
405: return control.toDisplay(event.x + xShift, event.y + yShift);
406: }
407:
408: private void toolTipHide(Shell tip, Event event) {
409: if (tip != null && !tip.isDisposed()
410: && shouldHideToolTip(event)) {
411: currentArea = null;
412: tip.dispose();
413: CURRENT_TOOLTIP = null;
414: afterHideToolTip(event);
415: }
416: }
417:
418: private void toolTipOpen(final Shell shell, final Event event) {
419: // Ensure that only one Tooltip is shown in time
420: if (CURRENT_TOOLTIP != null) {
421: toolTipHide(CURRENT_TOOLTIP, null);
422: }
423:
424: CURRENT_TOOLTIP = shell;
425:
426: if (popupDelay > 0) {
427: control.getDisplay().timerExec(popupDelay, new Runnable() {
428: public void run() {
429: toolTipShow(shell, event);
430: }
431: });
432: } else {
433: toolTipShow(CURRENT_TOOLTIP, event);
434: }
435:
436: if (hideDelay > 0) {
437: control.getDisplay().timerExec(popupDelay + hideDelay,
438: new Runnable() {
439:
440: public void run() {
441: toolTipHide(shell, null);
442: }
443: });
444: }
445: }
446:
447: private void toolTipHookByTypeRecursively(Control c, boolean add,
448: int type) {
449: if (add) {
450: c.addListener(type, hideListener);
451: } else {
452: c.removeListener(type, hideListener);
453: }
454:
455: if (c instanceof Composite) {
456: Control[] children = ((Composite) c).getChildren();
457: for (int i = 0; i < children.length; i++) {
458: toolTipHookByTypeRecursively(children[i], add, type);
459: }
460: }
461: }
462:
463: private void toolTipHookBothRecursively(Control c) {
464: c.addListener(SWT.MouseDown, hideListener);
465: c.addListener(SWT.MouseExit, hideListener);
466:
467: if (c instanceof Composite) {
468: Control[] children = ((Composite) c).getChildren();
469: for (int i = 0; i < children.length; i++) {
470: toolTipHookBothRecursively(children[i]);
471: }
472: }
473: }
474:
475: /**
476: * Creates the content area of the the tooltip.
477: *
478: * @param event
479: * the event that triggered the activation of the tooltip
480: * @param parent
481: * the parent of the content area
482: * @return the content area created
483: */
484: protected abstract Composite createToolTipContentArea(Event event,
485: Composite parent);
486:
487: /**
488: * This method is called after a Tooltip is hidden.
489: * <p>
490: * <b>Subclasses may override to clean up requested system resources</b>
491: * </p>
492: *
493: * @param event
494: * event triggered the hiding action (may be <code>null</code>
495: * if event wasn't triggered by user actions directly)
496: */
497: protected void afterHideToolTip(Event event) {
498:
499: }
500:
501: /**
502: * Set the hide delay.
503: *
504: * @param hideDelay
505: * the delay before the tooltip is hidden. If <code>0</code>
506: * the tooltip is shown until user moves to other item
507: */
508: public void setHideDelay(int hideDelay) {
509: this .hideDelay = hideDelay;
510: }
511:
512: /**
513: * Set the popup delay.
514: *
515: * @param popupDelay
516: * the delay before the tooltip is shown to the user. If
517: * <code>0</code> the tooltip is shown immediately
518: */
519: public void setPopupDelay(int popupDelay) {
520: this .popupDelay = popupDelay;
521: }
522:
523: /**
524: * Return if hiding on mouse down is set.
525: *
526: * @return <code>true</code> if hiding on mouse down in the tool tip is on
527: */
528: public boolean isHideOnMouseDown() {
529: return hideOnMouseDown;
530: }
531:
532: /**
533: * If you don't want the tool tip to be hidden when the user clicks inside
534: * the tool tip set this to <code>false</code>. You maybe also need to
535: * hide the tool tip yourself depending on what you do after clicking in the
536: * tooltip (e.g. if you open a new {@link Shell})
537: *
538: * @param hideOnMouseDown
539: * flag to indicate of tooltip is hidden automatically on mouse
540: * down inside the tool tip
541: */
542: public void setHideOnMouseDown(final boolean hideOnMouseDown) {
543: // Only needed if there's currently a tooltip active
544: if (CURRENT_TOOLTIP != null && !CURRENT_TOOLTIP.isDisposed()) {
545: // Only change if value really changed
546: if (hideOnMouseDown != this .hideOnMouseDown) {
547: control.getDisplay().syncExec(new Runnable() {
548:
549: public void run() {
550: if (CURRENT_TOOLTIP != null
551: && CURRENT_TOOLTIP.isDisposed()) {
552: toolTipHookByTypeRecursively(
553: CURRENT_TOOLTIP, hideOnMouseDown,
554: SWT.MouseDown);
555: }
556: }
557:
558: });
559: }
560: }
561:
562: this .hideOnMouseDown = hideOnMouseDown;
563: }
564:
565: /**
566: * Hide the currently active tool tip
567: */
568: public void hide() {
569: toolTipHide(CURRENT_TOOLTIP, null);
570: }
571:
572: private class ToolTipOwnerControlListener implements Listener {
573: public void handleEvent(Event event) {
574: switch (event.type) {
575: case SWT.Dispose:
576: case SWT.KeyDown:
577: case SWT.MouseDown:
578: case SWT.MouseMove:
579: toolTipHide(CURRENT_TOOLTIP, event);
580: break;
581: case SWT.MouseHover:
582: toolTipCreate(event);
583: break;
584: case SWT.MouseExit:
585: /*
586: * Check if the mouse exit happend because we move over the
587: * tooltip
588: */
589: if (CURRENT_TOOLTIP != null
590: && !CURRENT_TOOLTIP.isDisposed()) {
591: if (CURRENT_TOOLTIP.getBounds().contains(
592: control.toDisplay(event.x, event.y))) {
593: break;
594: }
595: }
596:
597: toolTipHide(CURRENT_TOOLTIP, event);
598: break;
599: }
600: }
601: }
602:
603: private class TooltipHideListener implements Listener {
604: public void handleEvent(Event event) {
605: if (event.widget instanceof Control) {
606:
607: Control c = (Control) event.widget;
608: Shell shell = c.getShell();
609:
610: switch (event.type) {
611: case SWT.MouseDown:
612: if (isHideOnMouseDown()) {
613: toolTipHide(shell, event);
614: }
615: break;
616: case SWT.MouseExit:
617: /*
618: * Give some insets to ensure we get exit informations from
619: * a wider area ;-)
620: */
621: Rectangle rect = shell.getBounds();
622: rect.x += 5;
623: rect.y += 5;
624: rect.width -= 10;
625: rect.height -= 10;
626:
627: if (!rect.contains(c.getDisplay()
628: .getCursorLocation())) {
629: toolTipHide(shell, event);
630: }
631:
632: break;
633: }
634: }
635: }
636: }
637: }
|