com.microsoft.tfs.client.common.ui.framework.table.tooltip.TableTooltipManager.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.ui.framework.table.tooltip.TableTooltipManager.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.client.common.ui.framework.table.tooltip;

import java.lang.reflect.Field;
import java.util.Timer;
import java.util.TimerTask;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;

import com.microsoft.tfs.client.common.ui.framework.WindowSystem;
import com.microsoft.tfs.util.Check;

/**
 * This provides a generic implementation of per-cell tooltips for tables. You
 * are expected to subclass this and fill a tooltip shell. This allows for
 * generic, complex tooltip support. For a concrete implementation that simply
 * provides textual tooltips, see {@link TableTooltipLabelManager}.
 *
 * Note: you may think this looks unnecessarily complex and have a temptation to
 * use Table.setTooltipText() in a mouse hover listener. Please don't. The
 * problem is that mouse hover events are fired independently of tooltip raises.
 * Further, this is wholly platform dependent. On Mac OS, you'll be setting
 * tooltip text after it would have been raised. On Windows, you may get
 * reasonable results, but intermittent because you're resetting the text at
 * inappropriate times.
 */
public abstract class TableTooltipManager {
    private final Table table;

    private boolean optionMoveWithMouse = true;

    /* The shell containing the tooltip */
    private Shell tooltipShell = null;

    /* The item that is (or was) hovered over. */
    private TableItem hoverItem = null;

    /* The index of the column that is (or was) hovered over. */
    private int hoverColumnIndex = -1;

    /* A timer to close the tooltip after a few seconds */
    private Timer hoverClearTimer = null;

    /* Max size of a tooltip */
    private Point maxTooltipSize = null;

    /*
     * SWT MouseWheel event: must get this reflectively since SWT 3.0 lacks it
     * :(
     */
    private int SWT_MouseWheel = -1;

    /**
     * @return the default tooltip timeout in milliseconds for the current
     *         platform
     */
    public static int getPlatformDefaultTooltipTimeout() {
        if (WindowSystem.isCurrentWindowSystem(WindowSystem.AQUA)) {
            return 10000;
        } else if (WindowSystem.isCurrentWindowSystem(WindowSystem.WINDOWS)) {
            return 5000;
        } else {
            return 7500;
        }
    }

    public TableTooltipManager(final Table table) {
        Check.notNull(table, "table"); //$NON-NLS-1$

        this.table = table;

        /* Get the SWT.MouseWheel field reflectively for RAD 6 compat */
        try {
            final Field mouseWheelField = SWT.class.getField("MouseWheel"); //$NON-NLS-1$
            final Integer mouseWheelValue = (Integer) mouseWheelField.get(null);
            SWT_MouseWheel = mouseWheelValue.intValue();
        } catch (final Exception e) {
        }
    }

    public void addTooltipManager() {
        table.setToolTipText(""); //$NON-NLS-1$

        table.addListener(SWT.MouseHover, tooltipEventListener);
        table.addListener(SWT.MouseMove, tooltipEventListener);

        table.addListener(SWT.Dispose, tooltipEventListener);
        table.addListener(SWT.MouseDown, tooltipEventListener);
        table.addListener(SWT.MouseUp, tooltipEventListener);
        table.addListener(SWT.MouseDoubleClick, tooltipEventListener);
        table.addListener(SWT.MouseEnter, tooltipEventListener);
        table.addListener(SWT.MouseExit, tooltipEventListener);
        table.addListener(SWT.FocusOut, tooltipEventListener);
        table.addListener(SWT.FocusIn, tooltipEventListener);
        table.addListener(SWT.Selection, tooltipEventListener);

        if (SWT_MouseWheel >= 0) {
            table.addListener(SWT_MouseWheel, tooltipEventListener);
        }
    }

    public void removeTooltipManager() {
        table.removeListener(SWT.MouseHover, tooltipEventListener);
        table.removeListener(SWT.MouseMove, tooltipEventListener);

        table.removeListener(SWT.Dispose, tooltipEventListener);
        table.removeListener(SWT.MouseDown, tooltipEventListener);
        table.removeListener(SWT.MouseUp, tooltipEventListener);
        table.removeListener(SWT.MouseDoubleClick, tooltipEventListener);
        table.removeListener(SWT.MouseEnter, tooltipEventListener);
        table.removeListener(SWT.MouseExit, tooltipEventListener);
        table.removeListener(SWT.FocusOut, tooltipEventListener);
        table.removeListener(SWT.FocusIn, tooltipEventListener);
        table.removeListener(SWT.Selection, tooltipEventListener);

        if (SWT_MouseWheel >= 0) {
            table.addListener(SWT_MouseWheel, tooltipEventListener);
        }

        table.setToolTipText(null);
    }

    /**
     * If this option is true, the tooltip will follow mouse movement (provided
     * the mouse stays over the same table item they hovered on. If this is
     * false the tooltip will close on a mouse move.
     *
     * @param optionMoveWithMouse
     *        true to follow the mouse, false to dispose tooltip
     */
    protected void setOptionMoveWithMouse(final boolean optionMoveWithMouse) {
        this.optionMoveWithMouse = optionMoveWithMouse;
    }

    protected boolean getOptionMoveWithMouse() {
        return optionMoveWithMouse;
    }

    private final Listener tooltipEventListener = new Listener() {
        @Override
        public void handleEvent(final Event event) {
            if (table.isDisposed()) {
                return;
            }

            switch (event.type) {
            case SWT.MouseHover:
                final TableItem newHoverItem = table.getItem(new Point(event.x, event.y));

                if (tooltipShell != null && !tooltipShell.isDisposed()) {
                    return;
                }

                if (newHoverItem == null) {
                    return;
                }

                /*
                 * We still need to calculate the hover column index, even
                 * when we're not in cell-based tooltip mode. This will
                 * allow us to paint clipped text for a column even for
                 * row-based tooltip contributors.
                 */
                final int newHoverColumnIndex = getColumnIndex(newHoverItem, event.x, event.y);

                /*
                 * Allow subclasses to veto the tooltip change. (They may
                 * provide per-row tooltips instead of per-column.)
                 */
                if (!shouldReplaceTooltip(newHoverItem, newHoverColumnIndex, hoverItem, hoverColumnIndex)) {
                    return;
                }

                /* Update the hover fields */
                hoverItem = newHoverItem;
                hoverColumnIndex = newHoverColumnIndex;

                final Display display = table.getDisplay();

                tooltipShell = new Shell(table.getShell(), SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
                tooltipShell.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));

                if (!createTooltip(tooltipShell, hoverItem, hoverColumnIndex)) {
                    tooltipShell = null;
                    return;
                }

                final Point tooltipSize = getTooltipSize(tooltipShell);
                final Point tooltipLocation = getTooltipLocation(event.x, event.y);

                tooltipShell.setBounds(tooltipLocation.x, tooltipLocation.y, tooltipSize.x, tooltipSize.y);
                tooltipShell.setVisible(true);

                /* Set up a Timer to clear this tooltip */
                hoverClearTimer = new Timer();
                hoverClearTimer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        display.asyncExec(new Runnable() {
                            @Override
                            public void run() {
                                if (table != null && !table.isDisposed()) {
                                    closeTooltip();
                                }
                            }
                        });
                    }
                }, getTooltipTimeout());

                break;

            case SWT.MouseMove:
                final TableItem moveItem = table.getItem(new Point(event.x, event.y));
                final int moveColumnIndex = getColumnIndex(moveItem, event.x, event.y);

                /* No tooltip open */
                if (hoverItem == null) {
                    return;
                }

                /* Moved off the table item - clear tool tip */
                if (shouldReplaceTooltip(moveItem, moveColumnIndex, hoverItem, hoverColumnIndex)) {
                    closeTooltip();
                    hoverItem = null;
                    return;
                }

                /* Moved the mouse, move the tooltip */
                else if (optionMoveWithMouse && tooltipShell != null && !tooltipShell.isDisposed()) {
                    tooltipShell.setLocation(getTooltipLocation(event.x, event.y));
                }

                break;

            case SWT.MouseDown:
            case SWT.MouseUp:
                closeTooltip();
                break;

            case SWT.Selection:
                final TableItem[] selection = table.getSelection();

                if (selection.length != 1) {
                    closeTooltip();
                } else if (selection[0] != hoverItem) {
                    closeTooltip();
                }

                break;

            case SWT.Dispose:
                removeTooltipManager();
                /* Fall-through to close tooltip */

            default:
                closeTooltip();
            }
        }
    };

    protected Point getTooltipSize(final Shell tooltipShell) {
        if (maxTooltipSize == null) {
            final GC gc = new GC(tooltipShell);

            final int max = Dialog.convertVerticalDLUsToPixels(gc.getFontMetrics(),
                    IDialogConstants.MINIMUM_MESSAGE_AREA_WIDTH);
            maxTooltipSize = new Point(max, max);

            gc.dispose();
        }

        Point tooltipSize = tooltipShell.computeSize(SWT.DEFAULT, SWT.DEFAULT);

        if (tooltipSize.x > maxTooltipSize.x) {
            tooltipSize = tooltipShell.computeSize(maxTooltipSize.x, SWT.DEFAULT);
        }
        if (tooltipSize.y > maxTooltipSize.y) {
            tooltipSize = tooltipShell.computeSize(maxTooltipSize.x, maxTooltipSize.y);
        }

        return tooltipSize;
    }

    /**
     * Determines the location of the tooltip, given the current mouse
     * coordinates. Subclasses may override.
     *
     * @param cursorX
     *        X position of the mouse cursor
     * @param cursorY
     *        Y position of the mouse cursor
     * @return A Point to draw the tooltip at
     */
    protected Point getTooltipLocation(final int cursorX, final int cursorY) {
        final Point tooltipLocation = table.toDisplay(cursorX, cursorY);

        final Point[] cursorSizes = table.getDisplay().getCursorSizes();
        if (cursorSizes != null && cursorSizes.length > 0
                && WindowSystem.isCurrentWindowSystem(WindowSystem.AQUA)) {
            tooltipLocation.x += cursorSizes[0].x;
            tooltipLocation.y += cursorSizes[0].y;
        } else if (cursorSizes != null && cursorSizes.length > 0) {
            tooltipLocation.x += (cursorSizes[0].x / 2);
            tooltipLocation.y += (cursorSizes[0].y / 2);
        } else {
            tooltipLocation.x += 7;
            tooltipLocation.y += 15;
        }

        return tooltipLocation;
    }

    /**
     * This is the time when tooltips will clear themselves (in ms.) After a
     * tooltip is raised, it will close itself after several seconds, depending
     * on the platform. Subclasses may override.
     *
     * @return ms to clear the timeout in
     */
    protected int getTooltipTimeout() {
        return getPlatformDefaultTooltipTimeout();
    }

    /**
     * Subclasses may override to veto tooltip replacement on a per-item or
     * per-cell basis. The default implementation replaces tooltips on a
     * per-cell basis. This allows subclasses to avoid flicker when moving
     * between cells when the tooltip should stay up.
     *
     * @param newHoverItem
     *        The item currently being hovered over
     * @param newHoverColumnIndex
     *        The column index currently being hovered over
     * @param oldHoverItem
     *        The item that was previously being hovered over (may be equal to
     *        newHoverItem)
     * @param oldHoverColumnIndex
     *        The column index currently being hovered over (may be equal to
     *        newHoverColumnIndex)
     * @return true to replace the tooltip, false if it would be identical
     */
    protected boolean shouldReplaceTooltip(final TableItem newHoverItem, final int newHoverColumnIndex,
            final TableItem oldHoverItem, final int oldHoverColumnIndex) {
        /* Replace the tooltip if the item or column index is different. */
        return (newHoverItem != hoverItem || newHoverColumnIndex != hoverColumnIndex);
    }

    /**
     * Subclasses must subclass this to populate the tooltip. If the subclass
     * returns false, no tooltip will be displayed for this table item.
     *
     * @param shell
     *        The shell to populate with tooltip data
     * @param tableItem
     *        The TableItem that this tooltip is for
     * @param columnIndex
     *        The column index of the cell or -1 if cell tips are not supported
     * @return true if the shell was populated and should be raised as a
     *         tooltip, false to suppress tooltip
     */
    protected abstract boolean createTooltip(Shell shell, TableItem tableItem, int columnIndex);

    public void closeTooltip() {
        if (tooltipShell == null) {
            return;
        }

        if (!tooltipShell.isDisposed()) {
            tooltipShell.close();
            tooltipShell.dispose();
        }

        tooltipShell = null;

        if (hoverClearTimer != null) {
            hoverClearTimer.cancel();
            hoverClearTimer = null;
        }
    }

    /**
     * Find the column index which contains the specified point in the specified
     * table item.
     *
     * @return The index of the column containing the specified point or -1 if
     *         the point is not in any column
     */
    private int getColumnIndex(final TableItem item, final int x, final int y) {
        if (item != null) {
            for (int i = 0; i < table.getColumnCount(); i++) {
                final Rectangle rect = item.getBounds(i);

                if (rect.contains(x, y)) {
                    return i;
                }
            }
        }
        return -1;
    }
}