Java tutorial
/* * Copyright (C) Heavy Lifting Software 2007, Robert Wloch 2012. * * This file is part of MouseFeed. * * MouseFeed is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * MouseFeed is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with MouseFeed. If not, see <http://www.gnu.org/licenses/>. */ package com.mousefeed.eclipse; import static com.mousefeed.eclipse.Layout.WHOLE_SIZE; import static com.mousefeed.eclipse.Layout.WINDOW_MARGIN; import static org.apache.commons.lang.Validate.isTrue; import static org.apache.commons.lang.Validate.notNull; import static org.apache.commons.lang.time.DateUtils.MILLIS_PER_SECOND; import com.mousefeed.client.Messages; import java.util.HashSet; import org.apache.commons.lang.StringUtils; import org.eclipse.core.commands.Command; import org.eclipse.core.commands.ParameterizedCommand; import org.eclipse.core.commands.common.NotDefinedException; import org.eclipse.jface.dialogs.PopupDialog; import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.FocusAdapter; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.commands.ICommandService; import org.eclipse.ui.dialogs.PreferencesUtil; //COUPLING:OFF - just uses a lot of other classes. It's Ok. /** * Pop-up dialog, which notifies a user about wrong mouse/accelerator usage. * * @author Andriy Palamarchuk * @author Robert Wloch */ public class NagPopUp extends PopupDialog { /** * Launcher runnable to open preference dialog. * * @author Robert Wloch */ protected static class PreferenceDialogLauncher implements Runnable { /** * Data object used as data parameter to the keys preference page. */ private final Object data; /** * Constructs a launcher to open the keys preference page with the optional data object as parameter. * @param data optional data object used as data parameter to the keys preference page */ protected PreferenceDialogLauncher(final Object data) { this.data = data; } /** * Creates and opens the Keys preference page. */ public void run() { final Display workbenchDisplay = PlatformUI.getWorkbench().getDisplay(); final Shell activeShell = workbenchDisplay.getActiveShell(); final String id = ORG_ECLIPSE_UI_PREFERENCE_PAGES_KEYS_ID; final String[] displayedIds = new String[] { id }; final PreferenceDialog preferenceDialog = PreferencesUtil.createPreferenceDialogOn(activeShell, id, displayedIds, data); preferenceDialog.open(); } } /** * ID of keys preference page. */ private static final String ORG_ECLIPSE_UI_PREFERENCE_PAGES_KEYS_ID = "org.eclipse.ui.preferencePages.Keys"; /** * How close to cursor along X axis the popup will be shown. */ private static final int DISTANCE_TO_CURSOR = 50; /** * Number of times to increase font size in. */ private static final int FONT_INCREASE_MULT = 2; /** * Time after which the pop up will automatically close itself. */ private static final int CLOSE_TIMEOUT = 4 * (int) MILLIS_PER_SECOND; /** * Timeout, after which listener closes the dialog on any user action. * Is necessary to skip events caused by the current user action. */ private static final int CLOSE_LISTENER_TIMEOUT = 250; /** * Provides messages text. */ private static final Messages MESSAGES = new Messages(NagPopUp.class); /** * The last action invocation reminder text factory. */ private static final LastActionInvocationRemiderFactory REMINDER_FACTORY = new LastActionInvocationRemiderFactory(); /** * @see NagPopUp#NagPopUp(String, String, boolean) */ private final String actionName; /** * @see NagPopUp#NagPopUp(String, String) */ private final String actionId; /** * @see NagPopUp#NagPopUp(String, String, boolean) */ private final String accelerator; /** * Indicates whether MouseFeed canceled the action the popup notifies about. */ private final boolean actionCancelled; /** * Is <code>true</code> when the dialog is already open, but not closed yet. */ private boolean open; /** * The notification text. */ private StyledText actionDescriptionText; /** * The notification link. */ @SuppressWarnings("unused") private Link actionLink; /** * Closes the dialog on any outside action, such as click, key press, etc. */ private Listener closeOnActionListener = new Listener() { public void handleEvent(final Event event) { NagPopUp.this.close(); } }; /** * Creates a pop-up with notification for the specified accelerator * and action. * * @param actionName the action label. Not blank. * @param accelerator the string describing the accelerator. Not blank. * @param actionCancelled indicates whether MouseFeed canceled the action * the popup notifies about. */ public NagPopUp(final String actionName, final String accelerator, final boolean actionCancelled) { super((Shell) null, PopupDialog.HOVER_SHELLSTYLE, false, false, false, false, false, getTitleText(actionCancelled), getActionConfigurationReminder()); isTrue(StringUtils.isNotBlank(actionName)); isTrue(StringUtils.isNotBlank(accelerator)); this.actionName = actionName; this.accelerator = accelerator; this.actionCancelled = actionCancelled; this.actionId = null; } /** * Creates a pop-up with suggestion to open the Keys preference page to * configure a keyboard shortcut for an action. * * @param actionName the action label. Not blank. * @param actionId the contribution id. Not blank. */ public NagPopUp(final String actionName, final String actionId) { super((Shell) null, PopupDialog.HOVER_SHELLSTYLE, false, false, false, false, false, getTitleText(false), getActionConfigurationReminder()); isTrue(StringUtils.isNotBlank(actionName)); isTrue(StringUtils.isNotBlank(actionId)); this.actionName = actionName; this.actionId = actionId; this.actionCancelled = false; this.accelerator = null; } /** * Creates a control for showing the info. * {@inheritDoc} */ @Override protected Control createDialogArea(final Composite parent) { final Composite composite = new Composite(parent, SWT.NO_FOCUS); composite.setLayout(new FormLayout()); if (isLinkPopup()) { final String linkText = MESSAGES.get("message.configureShortcut", actionName); actionLink = createLink(composite, linkText); } else { actionDescriptionText = createActionDescriptionText(composite); } composite.pack(true); return composite; } /** * Reusable check for actionId not null and accelerator being null. * @return true if actionId != null && accelerator == null */ protected boolean isLinkPopup() { return actionId != null && accelerator == null; } /** * Creates the text control to show action description. * @param parent the parent control. Not <code>null</code>. * @return the text control. Not <code>null</code>. */ private StyledText createActionDescriptionText(final Composite parent) { notNull(parent); final StyledText text = new StyledText(parent, SWT.READ_ONLY); configureFormData(text); text.setText(accelerator + " (" + actionName + ")"); if (actionCancelled) { final StyleRange style = new StyleRange(); style.start = 0; style.length = text.getText().length(); style.strikeout = true; text.setStyleRange(style); } configureBigFont(text); // since SWT.NO_FOCUS is only a hint... text.addFocusListener(new FocusAdapter() { @Override public void focusGained(final FocusEvent event) { NagPopUp.this.close(); } }); return text; } /** * Creates the link control to show a hyperlink to the Keys * preference page. * * @param parent the parent control. Not <code>null</code>. * @param text the text of the link * @return the link control. Not <code>null</code>. */ protected Link createLink(final Composite parent, final String text) { final Link link = new Link(parent, SWT.NONE); link.setFont(parent.getFont()); link.setText("<A>" + text + "</A>"); //$NON-NLS-1$//$NON-NLS-2$ configureFormData(link); configureBigFont(link); link.addFocusListener(new FocusAdapter() { @Override public void focusGained(final FocusEvent e) { doLinkActivated(); } }); return link; } /** * Handle link activation. */ @SuppressWarnings({ "rawtypes", "unchecked" }) final void doLinkActivated() { Object data = null; final IWorkbench workbench = Activator.getDefault().getWorkbench(); final ICommandService commandService = (ICommandService) workbench.getService(ICommandService.class); final Command command = commandService.getCommand(actionId); if (command != null) { final HashSet allParameterizedCommands = new HashSet(); try { allParameterizedCommands.addAll(ParameterizedCommand.generateCombinations(command)); } catch (final NotDefinedException e) { // It is safe to just ignore undefined commands. } if (!allParameterizedCommands.isEmpty()) { data = allParameterizedCommands.iterator().next(); // only commands can be bound to keyboard shortcuts openWorkspacePreferences(data); } } } /** * Opens the preference dialog with optional data. * * @param data an optional data object that can be handed as a parameter to the preference dialog. May be null. */ protected final void openWorkspacePreferences(final Object data) { final Display display = Display.getCurrent(); final PreferenceDialogLauncher runnable = new PreferenceDialogLauncher(data); display.asyncExec(runnable); } /** * Set the text color here, not in {@link #createDialogArea(Composite)}, * because it is redefined after that method is called. * @param parent the control parent. Not <code>null</code>. * @return the super value. */ @Override protected Control createContents(final Composite parent) { final Control control = super.createContents(parent); if (actionCancelled) { actionDescriptionText.setForeground(getDisplay().getSystemColor(SWT.COLOR_RED)); } return control; } /** * Configures sizes and margins for this. * @param c the control to set the form data for. Not <code>null</code>. */ private void configureFormData(final Control c) { final FormData formData = new FormData(); formData.left = new FormAttachment(WINDOW_MARGIN); formData.right = new FormAttachment(WHOLE_SIZE, -WINDOW_MARGIN); formData.top = new FormAttachment(WINDOW_MARGIN); formData.bottom = new FormAttachment(WHOLE_SIZE, -WINDOW_MARGIN); c.setLayoutData(formData); } /** * Configures big font for this. * @param c the control to increase font for. Not <code>null</code>. */ private void configureBigFont(final Control c) { final FontData[] fontData = c.getFont().getFontData(); for (int i = 0; i < fontData.length; i++) { fontData[i].setHeight(fontData[i].getHeight() * FONT_INCREASE_MULT); } final Font newFont = new Font(getDisplay(), fontData); c.setFont(newFont); c.addDisposeListener(new DestroyFontDisposeListener(newFont)); } /** * {@inheritDoc} * Places the dialog close to a mouse pointer. */ @Override public int open() { open = true; setParentShell(getDisplay().getActiveShell()); if (actionCancelled) { getDisplay().beep(); } getDisplay().timerExec(CLOSE_TIMEOUT, new Runnable() { public void run() { NagPopUp.this.close(); } }); addCloseOnActionListeners(); return super.open(); } /** {@inheritDoc} */ @Override public boolean close() { open = false; removeCloseOnActionListeners(); return super.close(); } /** * Adds listeners to close the dialog on any user action. * @see #closeOnActionListener * @see #CLOSE_LISTENER_TIMEOUT */ private void addCloseOnActionListeners() { getDisplay().timerExec(CLOSE_LISTENER_TIMEOUT, new Runnable() { public void run() { if (!open) { return; } final Listener l = closeOnActionListener; getDisplay().addFilter(SWT.MouseDown, l); getDisplay().addFilter(SWT.Selection, l); getDisplay().addFilter(SWT.KeyDown, l); } }); } /** * Removes listeners to close the dialog on any user action. * @see #closeOnActionListener */ private void removeCloseOnActionListeners() { // use workbench display, because can be called more than once, // including when this shell and the parent shell are already discarded final Display d = PlatformUI.getWorkbench().getDisplay(); d.removeFilter(SWT.MouseDown, closeOnActionListener); d.removeFilter(SWT.Selection, closeOnActionListener); d.removeFilter(SWT.KeyDown, closeOnActionListener); } /** * Current dialog display. Never <code>null</code>. */ private Display getDisplay() { return PlatformUI.getWorkbench().getDisplay(); } /** {@inheritDoc} */ @Override protected Point getInitialLocation(final Point initialSize) { final Point p = getDisplay().getCursorLocation(); p.x += DISTANCE_TO_CURSOR; p.y = Math.max(p.y - initialSize.y / 2, 0); return p; } /** * Text to show in the title. * Is static to make sure it does not rely on the instance members because * is called before calling "super" constructor. * @param canceled whether the action was canceled. * @return the title text. Can be <code>null</code>. */ private static String getTitleText(final boolean canceled) { return canceled ? MESSAGES.get("title.canceled") : MESSAGES.get("title.reminder"); } /** * Generates the text shown at the bottom of the popup. * Is static to make sure it does not rely on the instance members because * is called before calling "super" constructor. * @return the text. Never <code>null</code>. */ private static String getActionConfigurationReminder() { return REMINDER_FACTORY.getText(); } } //COUPLING:ON