/*
* 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.notNull;
import com.mousefeed.client.collector.AbstractActionDesc;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.IHandler;
import org.eclipse.jface.action.ExternalActionManager;
import org.eclipse.jface.action.ExternalActionManager.ICallback;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.jface.commands.ActionHandler;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.RetargetAction;
import org.eclipse.ui.activities.IActivityManager;
import org.eclipse.ui.internal.keys.BindingService;
import org.eclipse.ui.keys.IBindingService;
//COUPLING:OFF
/**
* Generates {@link ActionDescImpl} from {@link IAction}. Pure strategy. The
* logic was developed by trying different actions, making sure all the
* ActionDesc components are correctly generated.
*
* @author Andriy Palamarchuk
* @author Robert Wloch
*/
@SuppressWarnings("restriction")
public class ActionActionDescGenerator {
/**
* Stores the generated action description.
*/
private ActionDescImpl actionDesc;
/**
* Searches for an action inside of
* <code>org.eclipse.ui.actions.TextActionHandler</code> utility actions.
*/
private final TextActionHandlerActionLocator actionSearcher = new TextActionHandlerActionLocator();
/**
* The binding service for the associated workbench.
*/
private final IBindingService bindingService;
/**
* The activity manager for the associated workbench.
*/
private final IActivityManager activityManager;
/**
* Creates new finder.
*/
public ActionActionDescGenerator() {
bindingService = (IBindingService) getWorkbench().getAdapter(
IBindingService.class);
activityManager = getWorkbench().getActivitySupport()
.getActivityManager();
}
/**
* Generates action description from the action.
*
* @param action
* the action to generate description for. Not <code>null</code>.
* @return the action description for the provided action. Never
* <code>null</code>.
*/
public AbstractActionDesc generate(final IAction action) {
notNull(action);
actionDesc = new ActionDescImpl();
// actions may have no text nor tool tip text
final String text = action.getText();
final String toolTipText = action.getToolTipText();
if (text != null) {
actionDesc.setLabel(text);
} else if (toolTipText != null) {
actionDesc.setLabel(toolTipText);
} else {
actionDesc.setLabel(action.getClass().getSimpleName());
}
actionDesc.setClassName(action.getClass().getName());
extractActionData(action);
return actionDesc;
}
// clear, simple structure, leave extra returns
/**
* Scans action for data to populate action description.
*
* @param action
* the action to search accelerator for. Not <code>null</code>.
*/
public void extractActionData(final IAction action) {
notNull(action);
fromActionAccelerator(action.getAccelerator());
fromActionDefinition(action.getActionDefinitionId());
// retarget action?
if (action instanceof RetargetAction) {
final RetargetAction a = (RetargetAction) action;
if (a.getActionHandler() != null) {
extractActionData(a.getActionHandler());
}
}
fromActionBinding(action);
}
/**
* Populates {@link #actionDesc} from the action definition id.
*
* @param actionDefinitionId
* . <code>null</code> if not defined.
*/
private void fromActionDefinition(final String definitionId) {
if (definitionId == null) {
return;
}
actionDesc.setDef(definitionId);
final String s = findAcceleratorForActionDefinition(definitionId);
if (s != null) {
actionDesc.setAccelerator(s);
}
}
/**
* Populates {@link #actionDesc} from the action accelerator.
*
* @param accelerator
* . 0 if not defined.
*/
private void fromActionAccelerator(final int accelerator) {
if (accelerator != 0) {
actionDesc.setAccelerator(keyToStr(accelerator));
}
}
/**
* Finds keyboard shortcut for the action definition id.
*
* @param actionDefinitionId
* the action definition id to search accelerator for. If blank,
* the method returns <code>null</code>
* @return A human-readable string representation of the accelerator. Is
* <code>null</code> if can't be found.
*/
private String findAcceleratorForActionDefinition(final String definitionId) {
if (StringUtils.isBlank(definitionId)) {
return null;
}
final ICallback callback = ExternalActionManager.getInstance()
.getCallback();
return callback.getAcceleratorText(definitionId);
}
/**
* Scans current bindings, returns a trigger sequence, associated with this
* action. Uses heuristic to find an association. Assumes that if a binding
* action and the provided action are the same, if they have same class.
* This logic can potentially fail if an action class is used for different
* actions.
*
* @param action
* the action to search trigger sequence for. Returns
* <code>null</code> if the action is <code>null</code>.
* @return the accelerator from the trigger sequence associated with the
* action or <code>null</code> if such sequence was not found.
*/
private String fromActionBinding(final IAction action) {
if (action == null) {
return null;
}
if (action instanceof RetargetAction) {
return fromActionBinding(((RetargetAction) action)
.getActionHandler());
}
return scanBindings(action);
}
/**
* Scans bindings for the action.
*
* @param action
* the action to scan bindings for. Assumed not <code>null</code>
* .
* @return the accelerator from the action trigger sequence.
* <code>null</code> if the binding was not found.
*/
@SuppressWarnings("rawtypes")
private String scanBindings(final IAction action) {
final KeySequence keySequence = KeySequence.getInstance();
// In eclipse 4 BindingService#getPartialMatches() doesn't call the
// BindingManager any more leading to an ArrayIndexOutOfBoundsException
// due to bad code quality. Only for this reason, mousefeed needs to
// perform a direct call on the BindingManager now.
if (bindingService instanceof BindingService) {
final BindingService theBindingService = (BindingService) bindingService;
final Map matches = theBindingService.getBindingManager()
.getPartialMatches(keySequence);
final Class<? extends IAction> actionClass = action.getClass();
for (Object o : matches.keySet()) {
final TriggerSequence triggerSequence = (TriggerSequence) o;
final Binding binding = (Binding) matches.get(triggerSequence);
final Command command = binding.getParameterizedCommand()
.getCommand();
final IHandler handler = getCommandHandler(command);
if (!(handler instanceof ActionHandler)) {
continue;
}
if (isCommandEnabled(command)) {
final String accelerator = getFromBindingData(action,
actionClass, triggerSequence, handler);
if (accelerator != null) {
return accelerator;
}
}
}
}
return null;
}
// RETURNCOUNT:OFF
// clear, simple structure, leave extra returns
/**
* Gets accelerator for the bindings data.
*
* @param action
* the action. Assumed not null.
* @param actionClass
* effective action class. Assumed not null.
* @param triggerSequence
* the binding trigger sequence. Assumed not null.
* @param handler
* the action handler. Assumed not null.
* @return the accelerator if found, <code>null</code> otherwise.
*/
private String getFromBindingData(final IAction action,
final Class<? extends IAction> actionClass,
final TriggerSequence triggerSequence, final IHandler handler) {
final ActionHandler actionHandler = (ActionHandler) handler;
final IAction boundAction = actionHandler.getAction();
if (boundAction == null) {
return null;
}
if (boundAction.getClass().equals(actionClass)) {
return triggerSequence.toString();
}
if (!(boundAction instanceof RetargetAction)) {
return null;
}
final IAction searchTarget = ((RetargetAction) boundAction)
.getActionHandler();
if (searchTarget == null) {
return null;
}
if (searchTarget.getClass().equals(actionClass)) {
return triggerSequence.toString();
}
if (actionSearcher.isSearchable(searchTarget)) {
final String id = actionSearcher.findActionDefinitionId(action,
searchTarget);
return findAcceleratorForActionDefinition(id);
}
return null;
}
// RETURNCOUNT:ON
/**
* Converts the provided key combination code to accelerator.
*
* @param accelerator
* The accelerator to convert; should be a valid SWT accelerator
* value.
* @return The equivalent key stroke; never <code>null</code>.
*/
private String keyToStr(final int accelerator) {
return SWTKeySupport.convertAcceleratorToKeyStroke(accelerator)
.format();
}
/**
* Current workbench. Not <code>null</code>.
*/
private IWorkbench getWorkbench() {
return PlatformUI.getWorkbench();
}
/**
* Retrieves command handler from a command.
*
* @param command
* the command to retrieve the handler from. Not
* <code>null</code>.
* @return the handler. Returns <code>null</code>, if can't retrieve a
* handler.
*/
private IHandler getCommandHandler(final Command command) {
try {
final Method method = Command.class.getDeclaredMethod("getHandler");
method.setAccessible(true);
return (IHandler) method.invoke(command);
} catch (final SecurityException e) {
// want to know when this happens
throw new RuntimeException(e);
} catch (final NoSuchMethodException e) {
// should never happen
throw new AssertionError(e);
} catch (final IllegalAccessException e) {
// should never happen
throw new AssertionError(e);
} catch (final InvocationTargetException e) {
// should never happen
throw new AssertionError(e);
}
}
/**
* Returns <code>true</code> if the command is defined and is enabled.
*
* @param command
* the command to check. Not <code>null</code>.
*/
private boolean isCommandEnabled(final Command command) {
return command.isDefined()
&& activityManager.getIdentifier(command.getId()).isEnabled();
}
}
// COUPLING:ON