/*
* 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 org.apache.commons.lang.Validate.isTrue;
import static org.apache.commons.lang.Validate.notNull;
import com.mousefeed.client.OnWrongInvocationMode;
import com.mousefeed.client.collector.AbstractActionDesc;
import com.mousefeed.client.collector.Collector;
import com.mousefeed.eclipse.preferences.PreferenceAccessor;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
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.e4.ui.workbench.renderers.swt.HandledContributionItem;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.SubContributionItem;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.menus.CommandContributionItem;
/**
* Globally listens for the selection events.
*
* @author Andriy Palamarchuk
* @author Robert Wloch
*/
@SuppressWarnings("restriction")
public class GlobalSelectionListener implements Listener {
/**
* The id of the command/action to configure invocation mode for other
* actions.
*/
private static final String CONFIGURE_ACTION_INVOCATION_DEF = "com.mousefeed.commands.configureActionInvocation";
/**
* Provides access to the plugin preferences.
*/
private final PreferenceAccessor preferences = PreferenceAccessor
.getInstance();
/**
* Finds keyboard shortcut for an action.
*/
private final ActionActionDescGenerator actionActionDescGenerator = new ActionActionDescGenerator();
/**
* Finds keyboard shortcut for a command.
*/
private final CommandActionDescGenerator commandActionDescGenerator = new CommandActionDescGenerator();
/**
* Finds keyboard shortcut for a command.
*/
private final HandledActionDescGenerator handledActionDescGenerator = new HandledActionDescGenerator();
/**
* Collects user activity data.
*/
private final Collector collector = Activator.getDefault().getCollector();
/**
* The workbench command service.
*/
private final ICommandService commandService = (ICommandService) getWorkbench()
.getService(ICommandService.class);
/**
* Counts the number of times an action or command is invoked.
*/
private final Map<String, Integer> actionUsageMonitor = new HashMap<String, Integer>();
/**
* Default constructor does nothing.
*/
public GlobalSelectionListener() {
}
/**
* Processes an event.
*
* @param event
* the event. Not <code>null</code>.
*/
@Override
public void handleEvent(final Event event) {
final Widget widget = event.widget;
if (widget instanceof ToolItem || widget instanceof MenuItem) {
final Object data = widget.getData();
if (data instanceof IContributionItem) {
processContributionItem((IContributionItem) data, event);
}
} else {
// do not handle these types of actions
}
}
private void processContributionItem(
final IContributionItem contributionItem, final Event event) {
if (contributionItem instanceof SubContributionItem) {
final SubContributionItem subCI = (SubContributionItem) contributionItem;
processContributionItem(subCI.getInnerItem(), event);
} else if (contributionItem instanceof ActionContributionItem) {
final ActionContributionItem item = (ActionContributionItem) contributionItem;
final AbstractActionDesc actionDesc = actionActionDescGenerator
.generate(item.getAction());
processActionDesc(actionDesc, event);
} else if (contributionItem instanceof CommandContributionItem) {
final AbstractActionDesc actionDesc = commandActionDescGenerator
.generate((CommandContributionItem) contributionItem);
processActionDesc(actionDesc, event);
} else if (contributionItem instanceof HandledContributionItem) {
final AbstractActionDesc actionDesc = handledActionDescGenerator
.generate((HandledContributionItem) contributionItem);
processActionDesc(actionDesc, event);
} else {
// no action contribution item on the widget data
}
}
/**
* Processes the prepared action description.
*
* @param actionDesc
* the action description to process. Assumed not
* <code>null</code>.
* @param event
* the original event. Assumed not <code>null</code>.
*/
private void processActionDesc(final AbstractActionDesc actionDesc,
final Event event) {
// skips the configure action invocation action
if (CONFIGURE_ACTION_INVOCATION_DEF.equals(actionDesc.getId())) {
return;
}
giveActionFeedback(actionDesc, event);
logUserAction(actionDesc);
commandService.refreshElements(CONFIGURE_ACTION_INVOCATION_DEF, null);
}
/**
* Current workbench. Not <code>null</code>.
*/
private IWorkbench getWorkbench() {
return PlatformUI.getWorkbench();
}
/**
* Sends action information to {@link #collector}.
*
* @param actionDesc
* the action data to send. Assumed not <code>null</code>.
*/
private void logUserAction(final AbstractActionDesc actionDesc) {
collector.onAction(actionDesc);
}
/**
* Depending on the settings reports to the user that action can be called
* by the action accelerator, cancels the action.
*
* @param actionDesc
* the populated action description. Must have a keyboard
* shortcut defined. Not <code>null</code>.
*/
private void giveActionFeedback(final AbstractActionDesc actionDesc,
final Event event) {
notNull(actionDesc);
isTrue(StringUtils.isNotBlank(actionDesc.getLabel()));
if (!preferences.isInvocationControlEnabled()) {
return;
}
final String id = actionDesc.getId();
if (!actionDesc.hasAccelerator()) {
Integer currentCount = actionUsageMonitor.get(id);
if (currentCount == null) {
currentCount = Integer.valueOf(0);
}
currentCount = currentCount + 1;
actionUsageMonitor.put(id, currentCount);
if (isConfigureKeyboardShortcutEnabled(currentCount.intValue())
&& isConfigurableAction(actionDesc)) {
new NagPopUp(actionDesc.getLabel(), actionDesc.getId()).open();
}
return;
}
switch (getOnWrongInvocationMode(id)) {
case DO_NOTHING:
// go on
break;
case REMIND:
new NagPopUp(actionDesc.getLabel(), actionDesc.getAccelerator(),
false).open();
break;
case ENFORCE:
cancelEvent(event);
new NagPopUp(actionDesc.getLabel(), actionDesc.getAccelerator()
+ "", true).open();
break;
default:
throw new AssertionError();
}
}
/**
* Checks if for the current action a keyboard shortcut can be configured.
*
* @param actionDesc
* ActionDesc for the current action. Not null.
* @return true, if the current action has at least one ParameterizedCommand
* (only those are listed in the keys preference page), false else.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
protected boolean isConfigurableAction(final AbstractActionDesc actionDesc) {
final String actionId = actionDesc.getId();
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.
}
return !allParameterizedCommands.isEmpty();
}
return false;
}
/**
* Checks, if keyboard shortcut configuration should be activated.
*
* @param currentCount
* current counter for an action invocation
* @return true, if the configure keyboard shortcut preference is enabled
* and currentCount exceeds the value of the action invocation
* threshold property.
*/
public boolean isConfigureKeyboardShortcutEnabled(final int currentCount) {
final boolean isConfigureKeyboardShortcutEnabled = preferences
.isConfigureKeyboardShortcutEnabled();
final boolean isCounterAboveThreshold = currentCount > preferences
.getConfigureKeyboardShortcutThreshold();
return isConfigureKeyboardShortcutEnabled && isCounterAboveThreshold;
}
/**
* Returns the wrong invocation mode handling for the action with the
* specified id.
*
* @param id
* the action id. Assumed not <code>null</code>.
* @return the mode. Not <code>null</code>.
*/
private OnWrongInvocationMode getOnWrongInvocationMode(final String id) {
final OnWrongInvocationMode mode = preferences
.getOnWrongInvocationMode(id);
return mode == null ? preferences.getOnWrongInvocationMode() : mode;
}
/**
* Stops further processing of the specified event.
*
* @param event
* the event to disable. Assumed not <code>null</code>.
*/
private void cancelEvent(final Event event) {
event.type = SWT.None;
event.doit = false;
}
}