blob: ddba40e518939b1d566e1dc61d2c8f85e55b287b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2016 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Tom Hochstein (Freescale) - Bug 393703 - NotHandledException selecting inactive command under 'Previous Choices' in Quick access
* René Brandstetter - Bug 433778
* Patrik Suzzi <psuzzi@gmail.com> - Bug 491410
*******************************************************************************/
package org.eclipse.ui.internal.quickaccess;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.core.commands.Command;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.Assert;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.util.Util;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
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.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.WorkbenchWindow;
import org.eclipse.ui.internal.progress.ProgressManagerUtil;
import org.eclipse.ui.internal.quickaccess.providers.ActionProvider;
import org.eclipse.ui.internal.quickaccess.providers.CommandProvider;
import org.eclipse.ui.internal.quickaccess.providers.EditorProvider;
import org.eclipse.ui.internal.quickaccess.providers.HelpSearchProvider;
import org.eclipse.ui.internal.quickaccess.providers.PerspectiveProvider;
import org.eclipse.ui.internal.quickaccess.providers.PreferenceProvider;
import org.eclipse.ui.internal.quickaccess.providers.PropertiesProvider;
import org.eclipse.ui.internal.quickaccess.providers.ViewProvider;
import org.eclipse.ui.internal.quickaccess.providers.WizardProvider;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.quickaccess.QuickAccessElement;
/**
* This is the quick access popup dialog used in 3.x. The new quick access is
* done through a shell in {@link SearchField}.
*
* @since 3.3
*
*/
public class QuickAccessDialog extends PopupDialog {
private TriggerSequence[] invokingCommandKeySequences;
private Command invokingCommand;
private QuickAccessContents contents;
private KeyAdapter keyAdapter;
private Set<ModifyListener> toRemoveTextListeners;
private Text filterText;
private IWorkbenchWindow window;
private static final String USER_INPUT_TEXTS = "textArray"; //$NON-NLS-1$
private static final String TEXT_ENTRIES = "textEntries"; //$NON-NLS-1$
private static final String ORDERED_PROVIDERS = "orderedProviders"; //$NON-NLS-1$
private static final String ORDERED_ELEMENTS = "orderedElements"; //$NON-NLS-1$
static final int MAXIMUM_NUMBER_OF_ELEMENTS = 60;
static final int MAXIMUM_NUMBER_OF_TEXT_ENTRIES_PER_ELEMENT = 3;
protected Map<QuickAccessElement, ArrayList<String>> textMap = new HashMap<>();
protected Map<String, QuickAccessElement> elementMap = new HashMap<>();
private final Display display;
private String lastSelectionFilter = ""; //$NON-NLS-1$
private PreviousPicksProvider previousPicksProvider;
public QuickAccessDialog(IWorkbenchWindow window, Command invokingCommand) {
super(ProgressManagerUtil.getDefaultParent(), SWT.RESIZE, true, true, // persist
// size
false, // but not location
true, true, QuickAccessMessages.QuickAccessContents_QuickAccess,
QuickAccessMessages.QuickAccess_StartTypingToFindMatches);
this.window = window;
this.display = window.getShell() != null ? window.getShell().getDisplay() : null;
WorkbenchWindow workbenchWindow = (WorkbenchWindow) window;
final MWindow model = workbenchWindow.getModel();
BusyIndicator.showWhile(window.getShell() == null ? null : window.getShell().getDisplay(), () -> {
final CommandProvider commandProvider = new CommandProvider();
commandProvider.setContext(model.getContext().getActiveLeaf());
List<QuickAccessProvider> providers = new ArrayList<>();
previousPicksProvider = new PreviousPicksProvider(MAXIMUM_NUMBER_OF_ELEMENTS);
previousPicksProvider.setElementsInitializer(() -> restorePreviousEntries(providers));
providers.add(previousPicksProvider);
providers.add(new EditorProvider());
providers.add(new ViewProvider(model.getContext().get(MApplication.class), model));
providers.add(new PerspectiveProvider());
providers.add(commandProvider);
providers.add(new ActionProvider());
providers.add(new WizardProvider());
providers.add(new PreferenceProvider());
providers.add(new PropertiesProvider());
providers.addAll(QuickAccessExtensionManager.getProviders(() -> {
if (display != null) {
display.asyncExec(() -> {
QuickAccessDialog dialog = new QuickAccessDialog(window, invokingCommand);
dialog.filterText.setText(lastSelectionFilter);
dialog.open();
});
}
}));
providers.add(new HelpSearchProvider());
Collection<String> previousPickProviderIds = getPreviousPickProviderIds(getDialogSettings());
previousPicksProvider.setInvolvedProviders(
providers.stream().filter(provider -> previousPickProviderIds.contains(provider.getId()))
.collect(Collectors.toSet()));
QuickAccessDialog.this.contents = new QuickAccessContents(
providers.toArray(new QuickAccessProvider[providers.size()])) {
@Override
protected void updateFeedback(boolean filterTextEmpty, boolean showAllMatches) {
TriggerSequence[] sequences = getInvokingCommandKeySequences();
if (showAllMatches || sequences == null || sequences.length == 0) {
setInfoText(""); //$NON-NLS-1$
} else {
setInfoText(NLS.bind(QuickAccessMessages.QuickAccess_PressKeyToShowAllMatches,
sequences[0].format()));
}
}
@Override
protected void doClose() {
QuickAccessDialog.this.close();
}
/**
* @param element
*/
void addPreviousPick(String text, QuickAccessElement element) {
previousPicksProvider.addPreviousPick(element, removedElement -> {
ArrayList<String> removedList = textMap.remove(removedElement);
removedList.forEach(elementMap::remove);
});
ArrayList<String> textList = textMap.computeIfAbsent(element, key -> new ArrayList<>());
textList.remove(text);
if (textList.size() == MAXIMUM_NUMBER_OF_TEXT_ENTRIES_PER_ELEMENT) {
String removedText = textList.remove(0);
elementMap.remove(removedText);
}
if (!text.isEmpty()) {
textList.add(text);
QuickAccessElement replacedElement = elementMap.put(text, element);
if (replacedElement != null && !replacedElement.equals(element)) {
textList = textMap.get(replacedElement);
if (textList != null) {
textList.remove(text);
if (textList.isEmpty()) {
textMap.remove(replacedElement);
previousPicksProvider.removeElement(replacedElement);
}
}
}
}
}
@Override
protected QuickAccessElement getPerfectMatch(String filter) {
return elementMap.get(filter);
}
@Override
protected void handleElementSelected(String text, Object selectedElement) {
lastSelectionFilter = text;
if (selectedElement instanceof QuickAccessElement) {
addPreviousPick(text, (QuickAccessElement) selectedElement);
storeDialog(getDialogSettings());
/*
* Execute after the dialog has been fully closed/disposed and the correct
* EclipseContext is in place.
*/
final QuickAccessElement element = (QuickAccessElement) selectedElement;
window.getShell().getDisplay().asyncExec(element::execute);
}
}
};
QuickAccessDialog.this.invokingCommand = invokingCommand;
if (QuickAccessDialog.this.invokingCommand != null && !QuickAccessDialog.this.invokingCommand.isDefined()) {
QuickAccessDialog.this.invokingCommand = null;
} else {
// Pre-fetch key sequence - do not change because
// scope will
// change later.
getInvokingCommandKeySequences();
}
// create early
create();
});
QuickAccessDialog.this.contents.updateProposals(""); //$NON-NLS-1$
}
@Override
protected Control createTitleControl(Composite parent) {
parent.getShell().setText(QuickAccessMessages.QuickAccessContents_QuickAccess);
filterText = new Text(parent, SWT.NONE);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(filterText);
contents.hookFilterText(filterText);
filterText.addKeyListener(getKeyAdapter());
return filterText;
}
@Override
protected Control createDialogArea(Composite parent) {
Composite composite = (Composite) super.createDialogArea(parent);
boolean isWin32 = Util.isWindows();
GridLayoutFactory.fillDefaults().extendedMargins(isWin32 ? 0 : 3, 3, 2, 2).applyTo(composite);
Label hintText = contents.createHintText(composite, SWT.DEFAULT);
GridData gridData = new GridData(SWT.FILL, SWT.DEFAULT, true, false,
((GridLayout) composite.getLayout()).numColumns, 1);
gridData.horizontalIndent = IDialogConstants.HORIZONTAL_MARGIN;
hintText.setLayoutData(gridData);
Table table = contents.createTable(composite, getDefaultOrientation());
table.addKeyListener(getKeyAdapter());
return composite;
}
private TriggerSequence[] getInvokingCommandKeySequences() {
if (invokingCommandKeySequences == null) {
if (invokingCommand != null) {
IBindingService bindingService = Adapters.adapt(window.getWorkbench(), IBindingService.class);
invokingCommandKeySequences = bindingService.getActiveBindingsFor(invokingCommand.getId());
}
}
return invokingCommandKeySequences;
}
private KeyAdapter getKeyAdapter() {
if (keyAdapter == null) {
keyAdapter = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int accelerator = SWTKeySupport.convertEventToUnmodifiedAccelerator(e);
KeySequence keySequence = KeySequence
.getInstance(SWTKeySupport.convertAcceleratorToKeyStroke(accelerator));
TriggerSequence[] sequences = getInvokingCommandKeySequences();
if (sequences == null)
return;
for (TriggerSequence sequence : sequences) {
if (sequence.equals(keySequence)) {
e.doit = false;
contents.setShowAllMatches(!contents.getShowAllMatches());
return;
}
}
}
};
}
return keyAdapter;
}
@Override
protected Control getFocusControl() {
return filterText;
}
@Override
public boolean close() {
if (!filterText.isDisposed()) {
filterText.removeKeyListener(getKeyAdapter());
if (toRemoveTextListeners != null) {
for (ModifyListener listener : toRemoveTextListeners) {
filterText.removeModifyListener(listener);
}
}
}
storeDialog(getDialogSettings());
return super.close();
}
@Override
protected Point getDefaultSize() {
GC gc = new GC(getContents());
FontMetrics fontMetrics = gc.getFontMetrics();
gc.dispose();
int x = Dialog.convertHorizontalDLUsToPixels(fontMetrics, 300);
if (x < 350) {
x = 350;
}
int y = Dialog.convertVerticalDLUsToPixels(fontMetrics, 270);
if (y < 420) {
y = 420;
}
return new Point(x, y);
}
@Override
protected Point getDefaultLocation(Point initialSize) {
Point size = new Point(400, 400);
Rectangle parentBounds = getParentShell().getBounds();
int x = parentBounds.x + parentBounds.width / 2 - size.x / 2;
int y = parentBounds.y + parentBounds.height / 2 - size.y / 2;
return new Point(x, y);
}
@Override
protected IDialogSettings getDialogSettings() {
final IDialogSettings workbenchDialogSettings = WorkbenchPlugin.getDefault().getDialogSettings();
IDialogSettings result = workbenchDialogSettings.getSection(getId());
if (result == null) {
result = workbenchDialogSettings.addNewSection(getId());
}
return result;
}
protected static String getId() {
return "org.eclipse.ui.internal.QuickAccess"; //$NON-NLS-1$
}
private void storeDialog(IDialogSettings dialogSettings) {
String[] orderedElements = new String[previousPicksProvider.elements.size()];
String[] orderedProviders = new String[previousPicksProvider.elements.size()];
String[] textEntries = new String[previousPicksProvider.elements.size()];
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < orderedElements.length; i++) {
QuickAccessElement quickAccessElement = previousPicksProvider.elements.get(i);
ArrayList<String> elementText = textMap.get(quickAccessElement);
Assert.isNotNull(elementText);
orderedElements[i] = quickAccessElement.getId();
orderedProviders[i] = contents.getProviderFor(quickAccessElement).getId();
arrayList.addAll(elementText);
textEntries[i] = elementText.size() + ""; //$NON-NLS-1$
}
String[] textArray = arrayList.toArray(new String[arrayList.size()]);
dialogSettings.put(ORDERED_ELEMENTS, orderedElements);
dialogSettings.put(ORDERED_PROVIDERS, orderedProviders);
dialogSettings.put(TEXT_ENTRIES, textEntries);
dialogSettings.put(USER_INPUT_TEXTS, textArray);
}
private List<QuickAccessElement> restorePreviousEntries(Collection<QuickAccessProvider> providers) {
IDialogSettings dialogSettings = getDialogSettings();
if (dialogSettings == null) {
return Collections.emptyList();
}
List<QuickAccessElement> res = new ArrayList<>();
String[] orderedElements = dialogSettings.getArray(ORDERED_ELEMENTS);
String[] orderedProviders = dialogSettings.getArray(ORDERED_PROVIDERS);
String[] rawTextEntriesCountByElement = dialogSettings.getArray(TEXT_ENTRIES);
String[] userInputTexts = dialogSettings.getArray(USER_INPUT_TEXTS);
elementMap = new HashMap<>();
textMap = new HashMap<>();
if (orderedElements != null && orderedProviders != null && rawTextEntriesCountByElement != null
&& userInputTexts != null) {
Map<String, QuickAccessProvider> providerMap = providers.stream()
.collect(Collectors.toMap(QuickAccessProvider::getId, Function.identity()));
Integer[] textEntriesCountByElement = Arrays.stream(rawTextEntriesCountByElement).map(Integer::parseInt)
.toArray(Integer[]::new);
int inputTextIndex = 0;
for (int i = 0; i < orderedElements.length; i++) {
int numberOfMatchingTextsForCurrentElement = textEntriesCountByElement[i].intValue();
QuickAccessProvider quickAccessProvider = providerMap.get(orderedProviders[i]);
if (quickAccessProvider != null) {
String firstText = null;
if (inputTextIndex < userInputTexts.length && numberOfMatchingTextsForCurrentElement >= 1) {
firstText = userInputTexts[inputTextIndex];
}
QuickAccessElement quickAccessElement = quickAccessProvider.findElement(orderedElements[i],
firstText);
if (quickAccessElement != null) {
contents.registerProviderFor(quickAccessElement, quickAccessProvider);
ArrayList<String> matchingTextsForElement = new ArrayList<>();
for (int j = inputTextIndex; j < inputTextIndex
+ numberOfMatchingTextsForCurrentElement; j++) {
String text = userInputTexts[j];
// text length can be zero for old workspaces,
// see bug 190006
if (!text.isEmpty()) {
matchingTextsForElement.add(text);
elementMap.put(text, quickAccessElement);
}
}
textMap.put(quickAccessElement, matchingTextsForElement);
res.add(quickAccessElement);
}
}
inputTextIndex += numberOfMatchingTextsForCurrentElement;
}
}
return res;
}
/**
* @param dialogSettings
* @return
*/
private Collection<String> getPreviousPickProviderIds(IDialogSettings dialogSettings) {
if (dialogSettings == null) {
return Collections.emptySet();
}
String[] orderedProviders = dialogSettings.getArray(ORDERED_PROVIDERS);
if (orderedProviders == null) {
return Collections.emptySet();
}
return new HashSet<>(Arrays.asList(orderedProviders));
}
public QuickAccessContents getQuickAccessContents() {
return this.contents;
}
}