Package org.eclipse.ui.internal.keys

Source Code of org.eclipse.ui.internal.keys.KeyAssistDialog

/*******************************************************************************
* Copyright (c) 2004, 2007 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.ui.internal.keys;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.SortedMap;
import java.util.TreeMap;

import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.CommandException;
import org.eclipse.core.commands.common.NotDefinedException;
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.KeyStroke;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.preference.PreferenceDialog;
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;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.activities.IActivityManager;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.dialogs.PreferencesUtil;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.keys.IBindingService;

import com.ibm.icu.text.MessageFormat;

/**
* <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
*/
final 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 translation bundle in which to look up internationalized text.
   */
  private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle
      .getBundle(KeyAssistDialog.class.getName());

  /**
   * The activity manager for the associated workbench.
   */
  private final IActivityManager activityManager;

  /**
   * The binding service for the associated workbench.
   */
  private final IBindingService bindingService;

  /**
   * The binding that was selected when the key assist dialog last closed.
   * This is only remembered until <code>clearRememberedState()</code> is
   * called.
   */
  private Binding binding = null;

  /**
   * The ordered list of command identifiers corresponding to the table.
   */
  private final List bindings = new ArrayList();

  /**
   * The command service for the associated workbench.
   */
  private final ICommandService commandService;

  /**
   * The table containing of the possible completions. This value is
   * <code>null</code> until the dialog is created.
   */
  private Table completionsTable = null;

  /**
   * Whether this dialog is currently holding some remembered state.
   */
  private boolean hasRememberedState = false;

  /**
   * The key binding state for the associated workbench.
   */
  private final KeyBindingState keyBindingState;

  /**
   * 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 final WorkbenchKeyboard workbenchKeyboard;

  /**
   * A sorted map of conflicts to be used when the dialog pops up.
   *
   * @since 3.3
   */
  private SortedMap conflictMatches;

  /**
   * 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 workbench
   *            The workbench 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>.
   * @param associatedState
   *            The key binding state associated with the workbench; must not
   *            be <code>null</code>.
   */
  KeyAssistDialog(final IWorkbench workbench,
      final WorkbenchKeyboard associatedKeyboard,
      final KeyBindingState associatedState) {
    super((Shell) null, PopupDialog.INFOPOPUP_SHELLSTYLE, true, false,
        false, false, null, null);

    this.activityManager = workbench.getActivitySupport()
        .getActivityManager();
    this.bindingService = (IBindingService) workbench
        .getService(IBindingService.class);
    this.commandService = (ICommandService) workbench
        .getService(ICommandService.class);
    this.keyBindingState = associatedState;
    this.workbenchKeyboard = associatedKeyboard;

    this.setInfoText(getKeySequenceString());
  }

  /**
   * Clears out the remembered state of the key assist dialog. This includes
   * its width, as well as the selected binding.
   */
  final void clearRememberedState() {
    previousWidth = NO_REMEMBERED_WIDTH;
    binding = null;
    hasRememberedState = 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. This does not remember the internal state.
   *
   * @return Whether the shell was already closed.
   */
  public final 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 final boolean close(final 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 final boolean close(final boolean rememberState,
      final boolean resetState) {
    final Shell shell = getShell();
    if (rememberState) {
      // Remember the previous width.
      final int widthToRemember;
      if ((shell != null) && (!shell.isDisposed())) {
        widthToRemember = getShell().getSize().x;
      } else {
        widthToRemember = NO_REMEMBERED_WIDTH;
      }

      // Remember the selected command name and key sequence.
      final Binding bindingToRemember;
      if ((completionsTable != null) && (!completionsTable.isDisposed())) {
        final int selectedIndex = completionsTable.getSelectionIndex();
        if (selectedIndex != -1) {
          final TableItem selectedItem = completionsTable
              .getItem(selectedIndex);
          bindingToRemember = (Binding) selectedItem
              .getData(BINDING_KEY);
        } else {
          bindingToRemember = null;
        }
      } else {
        bindingToRemember = null;
      }

      rememberState(widthToRemember, bindingToRemember);
      completionsTable = null;
    }

    if (resetState) {
      keyBindingState.reset();
    }
    return super.close();
  }

  /**
   * 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 final void configureLocation(final Point size) {
    final Shell shell = getShell();

    final Shell workbenchWindowShell = keyBindingState
        .getAssociatedWindow().getShell();
    final int xCoord;
    final int yCoord;
    if (workbenchWindowShell != null) {
      /*
       * Position the shell at the bottom right corner of the workbench
       * window
       */
      final 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;

    }
    final 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 final Point configureSize() {
    final Shell shell = getShell();

    // Get the packed size of the shell.
    shell.pack();
    final Point size = shell.getSize();

    // Use the previous width if appropriate.
    if ((previousWidth != NO_REMEMBERED_WIDTH) && (previousWidth > size.x)) {
      size.x = previousWidth;
    }

    // Enforce maximum sizing.
    final Shell workbenchWindowShell = keyBindingState
        .getAssociatedWindow().getShell();
    if (workbenchWindowShell != null) {
      final Point workbenchWindowSize = workbenchWindowShell.getSize();
      final int maxWidth = workbenchWindowSize.x * 2 / 5;
      final 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;
  }

  /**
   * Returns a string representing the key sequence used to open this dialog.
   *
   * @return the string describing the key sequence, or <code>null</code> if
   *         it cannot be determined.
   */
  private String getKeySequenceString() {
    final Command command = commandService
        .getCommand("org.eclipse.ui.window.showKeyAssist"); //$NON-NLS-1$
    final TriggerSequence[] keyBindings = bindingService
        .getActiveBindingsFor(new ParameterizedCommand(command, null));
    final int keyBindingsCount = keyBindings.length;
    final KeySequence currentState = keyBindingState.getCurrentSequence();
    final int prefixSize = currentState.getKeyStrokes().length;

    // Try to find the first possible matching key binding.
    KeySequence keySequence = null;
    for (int i = 0; i < keyBindingsCount; i++) {
      keySequence = (KeySequence) keyBindings[i];

      // Now just double-check to make sure the key is still possible.
      if (prefixSize > 0) {
        if (keySequence.startsWith(currentState, false)) {
          /*
           * Okay, so we have a partial match. Replace the key binding
           * with the required suffix completion.
           */
          final KeyStroke[] oldKeyStrokes = keySequence
              .getKeyStrokes();
          final int newSize = oldKeyStrokes.length - prefixSize;
          final KeyStroke[] newKeyStrokes = new KeyStroke[newSize];
          System.arraycopy(oldKeyStrokes, prefixSize, newKeyStrokes,
              0, newSize);
          keySequence = KeySequence.getInstance(newKeyStrokes);
          break;
        }

        /*
         * The prefix doesn't match, so null out the key binding and try
         * again.
         */
        keySequence = null;
        continue;

      }

      // There is no prefix, so just grab the first.
      break;
    }
    if (keySequence == null) {
      return null; // couldn't find a suitable key binding
    }

    return MessageFormat.format(Util.translateString(RESOURCE_BUNDLE,
        "openPreferencePage"), //$NON-NLS-1$
        new Object[] { keySequence.format() });
  }

  /**
   * 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 final Control createDialogArea(final Composite parent) {
    // First, register the shell type with the context support
    registerShellType();

    // Create a composite for the dialog area.
    final Composite composite = new Composite(parent, SWT.NONE);
    final 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.
    final SortedMap partialMatches;
    if (conflictMatches != null) {
      partialMatches = conflictMatches;
      conflictMatches = null;
    } else {
      partialMatches = getPartialMatches();
    }

    if (partialMatches.isEmpty()) {
      createEmptyDialogArea(composite);
    } else {
      createTableDialogArea(composite, partialMatches);
    }
    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 final void createEmptyDialogArea(final Composite parent) {
    final Label noMatchesLabel = new Label(parent, SWT.NULL);
    noMatchesLabel.setText(Util.translateString(RESOURCE_BUNDLE,
        "NoMatches.Message")); //$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 final void createTableDialogArea(final Composite parent,
      final SortedMap partialMatches) {
    // Layout the table.
    completionsTable = new Table(parent, SWT.FULL_SELECTION | SWT.SINGLE);
    final GridData gridData = new GridData(GridData.FILL_BOTH);
    completionsTable.setLayoutData(gridData);
    completionsTable.setBackground(parent.getBackground());
    completionsTable.setLinesVisible(true);

    // Initialize the columns and rows.
    bindings.clear();
    final TableColumn columnCommandName = new TableColumn(completionsTable,
        SWT.LEFT, 0);
    final TableColumn columnKeySequence = new TableColumn(completionsTable,
        SWT.LEFT, 1);
    final Iterator itemsItr = partialMatches.entrySet().iterator();
    while (itemsItr.hasNext()) {
      final Map.Entry entry = (Map.Entry) itemsItr.next();
      final TriggerSequence sequence = (TriggerSequence) entry.getValue();
      final Binding binding = (Binding) entry.getKey();
      final ParameterizedCommand command = binding
          .getParameterizedCommand();
      try {
        final String[] text = { command.getName(), sequence.format() };
        final 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 you double-click on the table, it should execute the selected
     * command.
     */
    completionsTable.addListener(SWT.DefaultSelection, new Listener() {
      public final void handleEvent(final Event event) {
        executeKeyBinding(event);
      }
    });
  }

  /**
   * Edits the remembered selection in the preference dialog.
   */
  private final void editKeyBinding() {
    // Create a preference dialog on the keys preference page.
    final String keysPageId = "org.eclipse.ui.preferencePages.Keys"; //$NON-NLS-1$
    final PreferenceDialog dialog = PreferencesUtil
        .createPreferenceDialogOn(getShell(), keysPageId, null, binding);

    /*
     * Forget the remembered state (so we don't get stuck editing
     * preferences).
     */
    clearRememberedState();

    // Open the dialog (blocking).
    dialog.open();
  }

  /**
   * Handles the default selection event on the table of possible completions.
   * This attempts to execute the given command.
   */
  private final void executeKeyBinding(final Event trigger) {
    // Try to execute the corresponding command.
    final int selectionIndex = completionsTable.getSelectionIndex();
    if (selectionIndex >= 0) {
      final Binding binding = (Binding) bindings.get(selectionIndex);
      try {
        workbenchKeyboard.executeCommand(binding, trigger);
      } catch (final CommandException e) {
        workbenchKeyboard.logException(e, binding
            .getParameterizedCommand());
      }
    }
  }

  /**
   * Gets the list of key bindings that are partial matches to the current key
   * binding state.
   *
   * @return A sorted map of key sequences (KeySequence) to command identifier
   *         (String) representing the list of enabled commands that could
   *         possibly complete the current key sequence.
   */
  private final SortedMap getPartialMatches() {
    // Put all partial matches into the matches into the map.
    final Map partialMatches = bindingService
        .getPartialMatches(keyBindingState.getCurrentSequence());

    // Create a sorted map that sorts based on lexicographical order.
    final SortedMap sortedMatches = new TreeMap(new Comparator() {
      public final int compare(final Object a, final Object b) {
        final Binding bindingA = (Binding) a;
        final Binding bindingB = (Binding) b;
        final ParameterizedCommand commandA = bindingA
            .getParameterizedCommand();
        final ParameterizedCommand commandB = bindingB
            .getParameterizedCommand();
        try {
          return commandA.getName().compareTo(commandB.getName());
        } catch (final NotDefinedException e) {
          // should not happen
          return 0;
        }
      }
    });

    /*
     * Remove those partial matches for which either the command is not
     * identified or the activity manager believes the command is not
     * enabled.
     */
    final Iterator partialMatchItr = partialMatches.entrySet().iterator();
    while (partialMatchItr.hasNext()) {
      final Map.Entry entry = (Map.Entry) partialMatchItr.next();
      final Binding binding = (Binding) entry.getValue();
      final Command command = binding.getParameterizedCommand()
          .getCommand();
      if (command.isDefined()
          && activityManager.getIdentifier(command.getId())
              .isEnabled()) {
        sortedMatches.put(binding, entry.getKey());
      }
    }

    return sortedMatches;

  }

  /**
   * Returns whether the dialog is currently holding some remembered state.
   *
   * @return <code>true</code> if the dialog has remembered state;
   *         <code>false</code> otherwise.
   */
  private final boolean hasRememberedState() {
    return hasRememberedState;
  }

  /**
   * 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 final int open() {
    // If there is remember state, open the preference page.
    if (hasRememberedState()) {
      editKeyBinding();
      clearRememberedState();
      return Window.OK;
    }

    // If the dialog is already open, dispose the shell and recreate it.
    final Shell shell = getShell();
    if (shell != null) {
      close(false, false);
    }
    create();

    // Configure the size and location.
    final 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 final int open(Collection bindings) {
    conflictMatches = new TreeMap(new Comparator() {
      public final int compare(final Object a, final Object b) {
        final Binding bindingA = (Binding) a;
        final Binding bindingB = (Binding) b;
        final ParameterizedCommand commandA = bindingA
            .getParameterizedCommand();
        final ParameterizedCommand commandB = bindingB
            .getParameterizedCommand();
        try {
          return commandA.getName().compareTo(commandB.getName());
        } catch (final NotDefinedException e) {
          // should not happen
          return 0;
        }
      }
    });
    Iterator i = bindings.iterator();
    while (i.hasNext()) {
      Binding b = (Binding) i.next();
      conflictMatches.put(b, b.getTriggerSequence());
    }

    // If the dialog is already open, dispose the shell and recreate it.
    final Shell shell = getShell();
    if (shell != null) {
      close(false, false);
    }
    create();

    // Configure the size and location.
    final Point size = configureSize();
    configureLocation(size);

    // Call the super method.
    return super.open();
  }

  /**
   * Registers the shell as the same type as its parent with the context
   * support. This ensures that it does not modify the current state of the
   * application.
   */
  private final void registerShellType() {
    final Shell shell = getShell();
    final IContextService contextService = (IContextService) keyBindingState
        .getAssociatedWindow().getWorkbench().getService(
            IContextService.class);
    contextService.registerShell(shell, contextService
        .getShellType((Shell) shell.getParent()));
  }

  /**
   * Remembers the current state of this dialog.
   *
   * @param previousWidth
   *            The previous width of the dialog.
   * @param binding
   *            The binding to remember, may be <code>null</code> if none.
   */
  private final void rememberState(final int previousWidth,
      final Binding binding) {
    this.previousWidth = previousWidth;
    this.binding = binding;
    hasRememberedState = true;
  }

  /**
   * 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.
   */
  protected final void setParentShell(final Shell newParentShell) {
    super.setParentShell(newParentShell);
  }
}
TOP

Related Classes of org.eclipse.ui.internal.keys.KeyAssistDialog

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.