| /******************************************************************************* |
| * 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; |
| } |
| |
| } |