| /******************************************************************************* |
| * Copyright (c) 2011, 2013 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| ******************************************************************************/ |
| |
| package org.eclipse.e4.ui.bindings.internal; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.TreeSet; |
| import org.eclipse.core.commands.ParameterizedCommand; |
| import org.eclipse.core.commands.common.CommandException; |
| import org.eclipse.core.commands.common.NotDefinedException; |
| import org.eclipse.e4.core.contexts.IEclipseContext; |
| import org.eclipse.e4.ui.bindings.EBindingService; |
| import org.eclipse.e4.ui.bindings.keys.KeyBindingDispatcher; |
| import org.eclipse.jface.bindings.Binding; |
| import org.eclipse.jface.dialogs.Dialog; |
| import org.eclipse.jface.dialogs.PopupDialog; |
| import org.eclipse.jface.window.Window; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Label; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.swt.widgets.Table; |
| import org.eclipse.swt.widgets.TableColumn; |
| import org.eclipse.swt.widgets.TableItem; |
| |
| /** |
| * <p> |
| * A dialog displaying a list of key bindings. The dialog will execute a command if it is selected. |
| * </p> |
| * <p> |
| * The methods on this class are not thread-safe and must be run from the UI thread. |
| * </p> |
| * |
| * @since 3.1 |
| */ |
| public class KeyAssistDialog extends PopupDialog { |
| |
| /** |
| * The data key for the binding stored on an SWT widget. The key is a fully-qualified name, but |
| * in reverse order. This is so that the equals method will detect misses faster. |
| */ |
| private static final String BINDING_KEY = "Binding.bindings.jface.eclipse.org"; //$NON-NLS-1$ |
| |
| /** |
| * The value of <code>previousWidth</code> to set if there is no remembered width. |
| */ |
| private static final int NO_REMEMBERED_WIDTH = -1; |
| |
| /** |
| * The ordered list of command identifiers corresponding to the table. |
| */ |
| private List<Binding> bindings = new ArrayList<Binding>(); |
| |
| /** |
| * The table containing of the possible completions. This value is <code>null</code> until the |
| * dialog is created. |
| */ |
| private Table completionsTable = null; |
| |
| /** |
| * The width of the shell when it was previously open. This is only remembered until |
| * <code>clearRememberedState()</code> is called. |
| */ |
| private int previousWidth = NO_REMEMBERED_WIDTH; |
| |
| /** |
| * The key binding listener for the associated workbench. |
| */ |
| private KeyBindingDispatcher workbenchKeyboard; |
| |
| /** |
| * A sorted map of conflicts or partial matches to be used when the dialog pops up. |
| * |
| * @since 3.3 |
| */ |
| private Collection<Binding> matches; |
| |
| private IEclipseContext context; |
| |
| /** |
| * Constructs a new instance of <code>KeyAssistDialog</code>. When the dialog is first |
| * constructed, it contains no widgets. The dialog is first created with no parent. If a parent |
| * is required, call <code>setParentShell()</code>. Also, between uses, it might be necessary to |
| * call <code>setParentShell()</code> as well. |
| * |
| * @param context |
| * The context in which this dialog is created; must not be <code>null</code>. |
| * @param associatedKeyboard |
| * The key binding listener for the workbench; must not be <code>null</code>. |
| */ |
| public KeyAssistDialog(IEclipseContext context, KeyBindingDispatcher associatedKeyboard) { |
| super((Shell) null, PopupDialog.INFOPOPUP_SHELLSTYLE, true, false, false, false, null, null); |
| //super(null, PopupDialog.INFOPOPUP_SHELLSTYLE, true, false, false, false, false, DIALOG_TITLE, getKeySequenceString()); //$NON-NLS-1$ |
| |
| this.context = context; |
| this.workbenchKeyboard = associatedKeyboard; |
| } |
| |
| /** |
| * Clears out the remembered state of the key assist dialog. This includes its width, as well as |
| * the selected binding. |
| */ |
| public void clearRememberedState() { |
| previousWidth = NO_REMEMBERED_WIDTH; |
| } |
| |
| /** |
| * Closes this shell, but first remembers some state of the dialog. This way it will have a |
| * response if asked to open the dialog again or if asked to open the keys preference page. This |
| * does not remember the internal state. |
| * |
| * @return Whether the shell was already closed. |
| */ |
| public boolean close() { |
| return close(false); |
| } |
| |
| /** |
| * Closes this shell, but first remembers some state of the dialog. This way it will have a |
| * response if asked to open the dialog again or if asked to open the keys preference page. |
| * |
| * @param rememberState |
| * Whether the internal state should be remembered. |
| * @return Whether the shell was already closed. |
| */ |
| public boolean close(boolean rememberState) { |
| return close(rememberState, true); |
| } |
| |
| /** |
| * Closes this shell, but first remembers some state of the dialog. This way it will have a |
| * response if asked to open the dialog again or if asked to open the keys preference page. |
| * |
| * @param rememberState |
| * Whether the internal state should be remembered. |
| * @param resetState |
| * Whether the state should be reset. |
| * @return Whether the shell was already closed. |
| */ |
| private boolean close(boolean rememberState, boolean resetState) { |
| Shell shell = getShell(); |
| if (rememberState) { |
| |
| // Remember the previous width. |
| int widthToRemember; |
| if ((shell != null) && (!shell.isDisposed())) { |
| widthToRemember = getShell().getSize().x; |
| } else { |
| widthToRemember = NO_REMEMBERED_WIDTH; |
| } |
| |
| this.previousWidth = widthToRemember; |
| completionsTable = null; |
| } |
| matches = null; |
| boolean popupClosed = super.close(); |
| if (resetState) { |
| workbenchKeyboard.resetState(); |
| } |
| return popupClosed; |
| } |
| |
| /** |
| * Sets the position for the dialog based on the position of the workbench window. The dialog is |
| * flush with the bottom right corner of the workbench window. However, the dialog will not |
| * appear outside of the display's client area. |
| * |
| * @param size |
| * The final size of the dialog; must not be <code>null</code>. |
| */ |
| private void configureLocation(Point size) { |
| Shell shell = getShell(); |
| |
| Shell workbenchWindowShell = (Shell) shell.getParent(); |
| int xCoord; |
| int yCoord; |
| if (workbenchWindowShell != null) { |
| /* |
| * Position the shell at the bottom right corner of the workbench window |
| */ |
| Rectangle workbenchWindowBounds = workbenchWindowShell.getBounds(); |
| xCoord = workbenchWindowBounds.x + workbenchWindowBounds.width - size.x - 10; |
| yCoord = workbenchWindowBounds.y + workbenchWindowBounds.height - size.y - 10; |
| |
| } else { |
| xCoord = 0; |
| yCoord = 0; |
| |
| } |
| Rectangle bounds = new Rectangle(xCoord, yCoord, size.x, size.y); |
| shell.setBounds(getConstrainedShellBounds(bounds)); |
| } |
| |
| /** |
| * Sets the size for the dialog based on its previous size. The width of the dialog is its |
| * previous width, if it exists. Otherwise, it is simply the packed width of the dialog. The |
| * maximum width is 40% of the workbench window's width. The dialog's height is the packed |
| * height of the dialog to a maximum of half the height of the workbench window. |
| * |
| * @return The size of the dialog |
| */ |
| private Point configureSize() { |
| Shell shell = getShell(); |
| |
| // Get the packed size of the shell. |
| shell.pack(); |
| Point size = shell.getSize(); |
| |
| // Use the previous width if appropriate. |
| if ((previousWidth != NO_REMEMBERED_WIDTH) && (previousWidth > size.x)) { |
| size.x = previousWidth; |
| } |
| |
| // Enforce maximum sizing. |
| Shell workbenchWindowShell = (Shell) shell.getParent(); |
| if (workbenchWindowShell != null) { |
| Point workbenchWindowSize = workbenchWindowShell.getSize(); |
| int maxWidth = workbenchWindowSize.x * 2 / 5; |
| int maxHeight = workbenchWindowSize.y / 2; |
| if (size.x > maxWidth) { |
| size.x = maxWidth; |
| } |
| if (size.y > maxHeight) { |
| size.y = maxHeight; |
| } |
| } |
| |
| // Set the size for the shell. |
| shell.setSize(size); |
| return size; |
| } |
| |
| /** |
| * Creates the content area for the key assistant. This creates a table and places it inside the |
| * composite. The composite will contain a list of all the key bindings. |
| * |
| * @param parent |
| * The parent composite to contain the dialog area; must not be <code>null</code>. |
| */ |
| protected Control createDialogArea(Composite parent) { |
| |
| // Create a composite for the dialog area. |
| Composite composite = new Composite(parent, SWT.NONE); |
| GridLayout compositeLayout = new GridLayout(); |
| compositeLayout.marginHeight = 0; |
| compositeLayout.marginWidth = 0; |
| composite.setLayout(compositeLayout); |
| composite.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| composite.setBackground(parent.getBackground()); |
| |
| // Layout the partial matches. |
| Collection<Binding> bindings; |
| // if we're going to display a list of conflicts or partial matches... |
| if (matches != null) { |
| bindings = matches; |
| } |
| // else just grab the entire list of active bindings |
| else { |
| bindings = getActiveBindings(); |
| } |
| |
| if (bindings == null || bindings.isEmpty()) { |
| createEmptyDialogArea(composite); |
| } else { |
| createTableDialogArea(composite, bindings); |
| } |
| return composite; |
| } |
| |
| /** |
| * Creates an empty dialog area with a simple message saying there were no matches. This is used |
| * if no partial matches could be found. This should not really ever happen, but might be |
| * possible if the commands are changing while waiting for this dialog to open. |
| * |
| * @param parent |
| * The parent composite for the dialog area; must not be <code>null</code>. |
| */ |
| private void createEmptyDialogArea(Composite parent) { |
| Label noMatchesLabel = new Label(parent, SWT.NULL); |
| noMatchesLabel.setText("No matches"); //$NON-NLS-1$ |
| noMatchesLabel.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| noMatchesLabel.setBackground(parent.getBackground()); |
| } |
| |
| /** |
| * Creates a dialog area with a table of the partial matches for the current key binding state. |
| * The table will be either the minimum width, or <code>previousWidth</code> if it is not |
| * <code>NO_REMEMBERED_WIDTH</code>. |
| * |
| * @param parent |
| * The parent composite for the dialog area; must not be <code>null</code>. |
| * @param partialMatches |
| * The lexicographically sorted map of partial matches for the current state; must |
| * not be <code>null</code> or empty. |
| */ |
| private void createTableDialogArea(Composite parent, Collection<Binding> partialMatches) { |
| // Layout the table. |
| completionsTable = new Table(parent, SWT.FULL_SELECTION | SWT.SINGLE); |
| GridData gridData = new GridData(GridData.FILL_BOTH); |
| completionsTable.setLayoutData(gridData); |
| completionsTable.setBackground(parent.getBackground()); |
| completionsTable.setLinesVisible(true); |
| |
| // Initialize the columns and rows. |
| bindings.clear(); |
| TableColumn columnCommandName = new TableColumn(completionsTable, SWT.LEFT, 0); |
| TableColumn columnKeySequence = new TableColumn(completionsTable, SWT.LEFT, 1); |
| Iterator<Binding> itemsItr = partialMatches.iterator(); |
| while (itemsItr.hasNext()) { |
| Binding binding = itemsItr.next(); |
| String sequence = binding.getTriggerSequence().format(); |
| ParameterizedCommand command = binding.getParameterizedCommand(); |
| try { |
| String[] text = { command.getName(), sequence }; |
| TableItem item = new TableItem(completionsTable, SWT.NULL); |
| item.setText(text); |
| item.setData(BINDING_KEY, binding); |
| bindings.add(binding); |
| } catch (NotDefinedException e) { |
| // Not much to do, but this shouldn't really happen. |
| } |
| } |
| |
| Dialog.applyDialogFont(parent); |
| columnKeySequence.pack(); |
| if (previousWidth != NO_REMEMBERED_WIDTH) { |
| columnKeySequence.setWidth(previousWidth); |
| } |
| columnCommandName.pack(); |
| if (completionsTable.getItems().length > 0) { |
| completionsTable.setSelection(0); |
| } |
| |
| /* |
| * If you double-click on the table, it should execute the selected command. |
| */ |
| completionsTable.addListener(SWT.DefaultSelection, new Listener() { |
| public void handleEvent(Event event) { |
| executeKeyBinding(event); |
| } |
| }); |
| } |
| |
| /** |
| * Handles the default selection event on the table of possible completions. This attempts to |
| * execute the given command. |
| */ |
| private void executeKeyBinding(Event trigger) { |
| int selectionIndex = completionsTable.getSelectionIndex(); |
| // Try to execute the corresponding command. |
| if (selectionIndex >= 0) { |
| close(); |
| Binding binding = bindings.get(selectionIndex); |
| try { |
| // workbenchKeyboard.updateShellKludge(null); |
| workbenchKeyboard.executeCommand(binding.getParameterizedCommand(), trigger); |
| } catch (CommandException e) { |
| // WorkbenchPlugin.log(binding.getParameterizedCommand().toString(), e); |
| // TODO we probably need to log something here. |
| System.err.println(binding.getParameterizedCommand().toString() + " : " + e); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| private Collection<Binding> getActiveBindings() { |
| |
| EBindingService bindingService = context.getActiveLeaf().get(EBindingService.class); |
| |
| Iterator<Binding> iter, matchesIter; |
| Binding binding, bindingToAdd; |
| Collection<Binding> matchesForCommand; |
| Collection<Binding> activeBindings = bindingService.getActiveBindings(); |
| Collection<Binding> conflictBindings = bindingService.getAllConflicts(); |
| Collection<Binding> sortedMatches = new TreeSet<Binding>(new Comparator<Binding>() { |
| public int compare(Binding binding1, Binding binding2) { |
| ParameterizedCommand cmdA = binding1.getParameterizedCommand(); |
| ParameterizedCommand cmdB = binding2.getParameterizedCommand(); |
| int result = 0; |
| try { |
| result = cmdA.getName().compareTo(cmdB.getName()); |
| } catch (NotDefinedException e) { |
| // whaaa? |
| } |
| return result; |
| } |
| }); |
| |
| // if the active scheme is not the default scheme then we should clean up the active |
| // bindings list... if we find multiple bindings for the same command and they are for |
| // different schemes, then we need to handle which one should be displayed in the dialog |
| if (activeBindings != null) { |
| iter = activeBindings.iterator(); |
| while (iter.hasNext()) { |
| binding = iter.next(); |
| matchesForCommand = bindingService |
| .getBindingsFor(binding.getParameterizedCommand()); |
| // if there is more than one match, then look for a binding that does not belong to |
| // the default scheme. If they all belong to the default scheme or they all do NOT |
| // belong to the default scheme, then arbitrarily choose one |
| if (matchesForCommand != null && matchesForCommand.size() > 1) { |
| bindingToAdd = null; |
| |
| matchesIter = matchesForCommand.iterator(); |
| while (matchesIter.hasNext()) { |
| bindingToAdd = matchesIter.next(); |
| if (!bindingToAdd.getSchemeId().equals(EBindingService.DEFAULT_SCHEME_ID)) { |
| sortedMatches.add(bindingToAdd); |
| break; |
| } |
| } |
| // if they're all the same, arbitrarily choose one |
| if (bindingToAdd != null) { |
| sortedMatches.add(bindingToAdd); |
| } |
| } |
| // if there is only one match, then just add it |
| else if (matchesForCommand != null && matchesForCommand.size() == 1) { |
| sortedMatches.addAll(matchesForCommand); |
| } |
| } |
| } |
| if (conflictBindings != null) { |
| iter = conflictBindings.iterator(); |
| while (iter.hasNext()) { |
| binding = iter.next(); |
| sortedMatches.add(binding); |
| } |
| } |
| return sortedMatches; |
| } |
| |
| /** |
| * Opens this dialog. This method can be called multiple times on the same dialog. This only |
| * opens the dialog if there is no remembered state; if there is remembered state, then it tries |
| * to open the preference page instead. |
| * |
| * @return The return code from this dialog. |
| */ |
| public int open() { |
| // If the dialog is already open, dispose the shell and recreate it. |
| Shell shell = getShell(); |
| if (shell != null) { |
| close(false, false); |
| return Window.OK; |
| } |
| create(); |
| |
| // Configure the size and location. |
| Point size = configureSize(); |
| configureLocation(size); |
| |
| // Call the super method. |
| return super.open(); |
| } |
| |
| /** |
| * Opens this dialog with the list of bindings for the user to select from. |
| * |
| * @return The return code from this dialog. |
| * @since 3.3 |
| */ |
| public int open(Collection<Binding> bindings) { |
| matches = new TreeSet<Binding>(new Comparator<Binding>() { |
| public int compare(Binding a, Binding b) { |
| Binding bindingA = a; |
| Binding bindingB = b; |
| ParameterizedCommand commandA = bindingA.getParameterizedCommand(); |
| ParameterizedCommand commandB = bindingB.getParameterizedCommand(); |
| try { |
| return commandA.getName().compareTo(commandB.getName()); |
| } catch (NotDefinedException e) { |
| // should not happen |
| return 0; |
| } |
| } |
| }); |
| matches.addAll(bindings); |
| |
| // If the dialog is already open, dispose the shell and recreate it. |
| Shell shell = getShell(); |
| if (shell != null) { |
| close(false, false); |
| return Window.OK; |
| } |
| create(); |
| // Bug 369860. Stop ShellActivationListener from creating a context for this. |
| getShell().setData("org.eclipse.e4.ui.ignoreDialog", Boolean.TRUE); //$NON-NLS-1$ |
| // Configure the size and location. |
| Point size = configureSize(); |
| configureLocation(size); |
| |
| // Call the super method. |
| return super.open(); |
| } |
| |
| /** |
| * Exposing this within the keys package. |
| * |
| * @param newParentShell |
| * The new parent shell; this value may be <code>null</code> if there is to be no |
| * parent. |
| */ |
| public void setParentShell(Shell newParentShell) { |
| super.setParentShell(newParentShell); |
| } |
| |
| /** |
| * Returns the currently selected binding from the table if the table is not disposed |
| * |
| * @return the currently selected binding or <code>null</code> |
| */ |
| protected Binding getSelectedBinding() { |
| if ((completionsTable != null) && (!completionsTable.isDisposed())) { |
| int selectedIndex = completionsTable.getSelectionIndex(); |
| if (selectedIndex != -1) { |
| TableItem selectedItem = completionsTable.getItem(selectedIndex); |
| return (Binding) selectedItem.getData(BINDING_KEY); |
| } |
| } |
| return null; |
| } |
| } |