com.mousefeed.eclipse.NagPopUp.java Source code

Java tutorial

Introduction

Here is the source code for com.mousefeed.eclipse.NagPopUp.java

Source

/*
 * 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