/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; /** * A dedicated tooltip used during gestures, for example to show the resize dimensions. * <p> * This is necessary because {@link org.eclipse.jface.window.ToolTip} causes flicker when * used to dynamically update the position and text of the tip, and it does not seem to * have setter methods to update the text or position without recreating the tip. */ public class GestureToolTip { /** Minimum number of milliseconds to wait between alignment changes */ private static final int TIMEOUT_MS = 750; /** * The alpha to use for the tooltip window (which sadly will apply to the tooltip text * as well.) */ private static final int SHELL_TRANSPARENCY = 220; /** The size of the font displayed in the tooltip */ private static final int FONT_SIZE = 9; /** Horizontal delta from the mouse cursor to shift the tooltip by */ private static final int OFFSET_X = 20; /** Vertical delta from the mouse cursor to shift the tooltip by */ private static final int OFFSET_Y = 20; /** The label which displays the tooltip */ private CLabel mLabel; /** The shell holding the tooltip */ private Shell mShell; /** The font shown in the label; held here such that it can be disposed of after use */ private Font mFont; /** Is the tooltip positioned below the given anchor? */ private boolean mBelow; /** Is the tooltip positioned to the right of the given anchor? */ private boolean mToRightOf; /** Is an alignment change pending? */ private boolean mTimerPending; /** The new value for {@link #mBelow} when the timer expires */ private boolean mPendingBelow; /** The new value for {@link #mToRightOf} when the timer expires */ private boolean mPendingRight; /** The time stamp (from {@link System#currentTimeMillis()} of the last alignment change */ private long mLastAlignmentTime; /** * Creates a new tooltip over the given parent with the given relative position. * * @param parent the parent control * @param below if true, display the tooltip below the mouse cursor otherwise above * @param toRightOf if true, display the tooltip to the right of the mouse cursor, * otherwise to the left */ public GestureToolTip(Composite parent, boolean below, boolean toRightOf) { mBelow = below; mToRightOf = toRightOf; mLastAlignmentTime = System.currentTimeMillis(); mShell = new Shell(parent.getShell(), SWT.ON_TOP | SWT.TOOL | SWT.NO_FOCUS); mShell.setLayout(new FillLayout()); mShell.setAlpha(SHELL_TRANSPARENCY); Display display = parent.getDisplay(); mLabel = new CLabel(mShell, SWT.SHADOW_NONE); mLabel.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); mLabel.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); Font systemFont = display.getSystemFont(); FontData[] fd = systemFont.getFontData(); for (int i = 0; i < fd.length; i++) { fd[i].setHeight(FONT_SIZE); } mFont = new Font(display, fd); mLabel.setFont(mFont); mShell.setVisible(false); } /** * Show the tooltip at the given position and with the given text. Note that the * position may not be applied immediately; to prevent flicker alignment changes * are queued up with a timer (unless it's been a while since the last change, in * which case the update is applied immediately.) * * @param text the new text to be displayed * @param below if true, display the tooltip below the mouse cursor otherwise above * @param toRightOf if true, display the tooltip to the right of the mouse cursor, * otherwise to the left */ public void update(final String text, boolean below, boolean toRightOf) { // If the alignment has not changed recently, just apply the change immediately // instead of within a delay if (!mTimerPending && (below != mBelow || toRightOf != mToRightOf) && (System.currentTimeMillis() - mLastAlignmentTime >= TIMEOUT_MS)) { mBelow = below; mToRightOf = toRightOf; mLastAlignmentTime = System.currentTimeMillis(); } Point location = mShell.getDisplay().getCursorLocation(); mLabel.setText(text); // Pack the label to its minimum size -- unless we are positioning the tooltip // on the left. Because of the way SWT works (at least on the OSX) this sometimes // creates flicker, because when we switch to a longer string (such as when // switching from "52dp" to "wrap_content" during a resize) the window size will // change first, and then the location will update later - so there will be a // brief flash of the longer label before it is moved to the right position on the // left. To work around this, we simply pass false to pack such that it will reuse // its cached size, which in practice means that for labels on the right, the // label will grow but not shrink. // This workaround is disabled because it doesn't work well in Eclipse 3.5; the // labels don't grow when they should. Re-enable when we drop 3.5 support. //boolean changed = mToRightOf; boolean changed = true; mShell.pack(changed); Point size = mShell.getSize(); // Position the tooltip to the left or right, and above or below, according // to the saved state of these flags, not the current parameters. We don't want // to flicker, instead we react on a timer to changes in alignment below. if (mBelow) { location.y += OFFSET_Y; } else { location.y -= OFFSET_Y; location.y -= size.y; } if (mToRightOf) { location.x += OFFSET_X; } else { location.x -= OFFSET_X; location.x -= size.x; } mShell.setLocation(location); if (!mShell.isVisible()) { mShell.setVisible(true); } // Has the orientation changed? mPendingBelow = below; mPendingRight = toRightOf; if (below != mBelow || toRightOf != mToRightOf) { // Yes, so schedule a timer (unless one is already scheduled) if (!mTimerPending) { mTimerPending = true; final Runnable timer = new Runnable() { @Override public void run() { mTimerPending = false; // Check whether the alignment is still different than the target // (since we may change back and forth repeatedly during the timeout) if (mBelow != mPendingBelow || mToRightOf != mPendingRight) { mBelow = mPendingBelow; mToRightOf = mPendingRight; mLastAlignmentTime = System.currentTimeMillis(); if (mShell != null && mShell.isVisible()) { update(text, mBelow, mToRightOf); } } } }; mShell.getDisplay().timerExec(TIMEOUT_MS, timer); } } } /** Hide the tooltip and dispose of any associated resources */ public void dispose() { mShell.dispose(); mFont.dispose(); mShell = null; mFont = null; mLabel = null; } }