| /******************************************************************************* |
| * Copyright (c) 2005, 2020 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 |
| * Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654, 491272, 491398 |
| * Leung Wang Hei <gemaspecial@yahoo.com.hk> - Bug 483343 |
| * Patrik Suzzi <psuzzi@gmail.com> - Bug 491291, 491529, 491293, 492434, 492452, 459989, 507322 |
| *******************************************************************************/ |
| package org.eclipse.ui.internal.quickaccess; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.Function; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import org.eclipse.core.runtime.Adapters; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.IJobChangeEvent; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.core.runtime.jobs.JobChangeAdapter; |
| import org.eclipse.jface.bindings.TriggerSequence; |
| import org.eclipse.jface.layout.GridDataFactory; |
| import org.eclipse.jface.layout.TableColumnLayout; |
| import org.eclipse.jface.resource.FontDescriptor; |
| import org.eclipse.jface.resource.JFaceResources; |
| import org.eclipse.jface.resource.LocalResourceManager; |
| import org.eclipse.jface.util.Util; |
| import org.eclipse.jface.viewers.ColumnWeightData; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.ControlAdapter; |
| import org.eclipse.swt.events.ControlEvent; |
| import org.eclipse.swt.events.KeyEvent; |
| import org.eclipse.swt.events.KeyListener; |
| import org.eclipse.swt.events.MouseAdapter; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseMoveListener; |
| import org.eclipse.swt.events.SelectionAdapter; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.Font; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.graphics.TextLayout; |
| import org.eclipse.swt.graphics.TextStyle; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Label; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.swt.widgets.Table; |
| import org.eclipse.swt.widgets.TableColumn; |
| import org.eclipse.swt.widgets.TableItem; |
| import org.eclipse.swt.widgets.Text; |
| import org.eclipse.ui.IWorkbenchPreferenceConstants; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.internal.WorkbenchPlugin; |
| import org.eclipse.ui.keys.IBindingService; |
| import org.eclipse.ui.progress.UIJob; |
| import org.eclipse.ui.quickaccess.QuickAccessElement; |
| import org.eclipse.ui.themes.ColorUtil; |
| |
| /** |
| * Provides the contents for the quick access shell created by |
| * {@link SearchField}. This was also used by {@link QuickAccessDialog} prior to |
| * e4. The SearchField is responsible for handling opening and closing the shell |
| * as well as setting {@link #setShowAllMatches(boolean)}. |
| */ |
| public abstract class QuickAccessContents { |
| /** |
| * When opened in a popup we were given the command used to open it. Now that we |
| * have a shell, we are just using a hard coded command id. |
| */ |
| private static final String QUICK_ACCESS_COMMAND_ID = "org.eclipse.ui.window.quickAccess"; //$NON-NLS-1$ |
| |
| protected Text filterText; |
| |
| private QuickAccessProvider[] providers; |
| private Map<String, QuickAccessProvider> providerMap = new HashMap<>(); |
| private Map<QuickAccessElement, QuickAccessProvider> elementsToProviders = new HashMap<>(); |
| |
| protected Table table; |
| protected Label infoLabel; |
| |
| private LocalResourceManager resourceManager = new LocalResourceManager(JFaceResources.getResources()); |
| |
| protected String rememberedText; |
| |
| /** |
| * A color for dulled out items created by mixing the table foreground. Will be |
| * disposed when the {@link #resourceManager} is disposed. |
| */ |
| private Color grayColor; |
| private TextLayout textLayout; |
| private boolean showAllMatches = false; |
| protected boolean resized = false; |
| private TriggerSequence keySequence; |
| private Job computeProposalsJob; |
| |
| public QuickAccessContents(QuickAccessProvider[] providers) { |
| this.providers = providers; |
| } |
| |
| /** |
| * Returns the number of items the table can fit in its current layout |
| */ |
| private int computeNumberOfItems() { |
| Rectangle rect = table.getClientArea(); |
| int itemHeight = table.getItemHeight(); |
| int headerHeight = table.getHeaderHeight(); |
| return (rect.height - headerHeight + itemHeight - 1) / (itemHeight + table.getGridLineWidth()); |
| } |
| |
| /** |
| * Refreshes the contents of the quick access shell |
| * |
| * @param filter The filter text to apply to results |
| * |
| */ |
| public void updateProposals(String filter) { |
| if (computeProposalsJob != null) { |
| computeProposalsJob.cancel(); |
| computeProposalsJob = null; |
| } |
| if (table == null || table.isDisposed()) { |
| return; |
| } |
| final Display display = table.getDisplay(); |
| |
| // perfect match, to be selected in the table if not null |
| QuickAccessElement perfectMatch = getPerfectMatch(filter); |
| |
| String computingMessage = NLS.bind(QuickAccessMessages.QuickaAcessContents_computeMatchingEntries, filter); |
| int maxNumberOfItemsInTable = computeNumberOfItems(); |
| AtomicReference<List<QuickAccessEntry>[]> entries = new AtomicReference<>(); |
| final Job currentComputeEntriesJob = Job.create(computingMessage, theMonitor -> { |
| entries.set( |
| computeMatchingEntries(filter, perfectMatch, maxNumberOfItemsInTable, theMonitor)); |
| return theMonitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS; |
| }); |
| currentComputeEntriesJob.setPriority(Job.INTERACTIVE); |
| // feedback is delayed in a job as we don't want to show it on every keystroke |
| // but only when user seems to be waiting |
| UIJob computingFeedbackJob = new UIJob(table.getDisplay(), QuickAccessMessages.QuickAccessContents_computeMatchingEntries_displayFeedback_jobName) { |
| @Override |
| public IStatus runInUIThread(IProgressMonitor monitor) { |
| if (currentComputeEntriesJob.getResult() == null && !monitor.isCanceled() && !table.isDisposed()) { |
| showHintText(computingMessage, grayColor); |
| return Status.OK_STATUS; |
| } |
| return Status.CANCEL_STATUS; |
| } |
| }; |
| currentComputeEntriesJob.addJobChangeListener(new JobChangeAdapter() { |
| @Override |
| public void done(IJobChangeEvent event) { |
| computingFeedbackJob.cancel(); |
| if (computeProposalsJob == currentComputeEntriesJob && event.getResult().isOK() |
| && !table.isDisposed()) { |
| display.asyncExec(() -> { |
| computingFeedbackJob.cancel(); |
| refreshTable(perfectMatch, entries.get(), filter); |
| }); |
| } |
| } |
| }); |
| this.computeProposalsJob = currentComputeEntriesJob; |
| currentComputeEntriesJob.schedule(); |
| computingFeedbackJob.schedule(200); // delay a bit so if proposals compute fast enough, we don't show feedback |
| } |
| |
| /** |
| * Allows the quick access content owner to mark a quick access element as being |
| * a perfect match, putting it at the start of the table. |
| * |
| * @param filter the filter text used to find a match |
| * @return an element to be put at the top of the table or <code>null</code> |
| */ |
| protected abstract QuickAccessElement getPerfectMatch(String filter); |
| |
| /** |
| * Notifies the quick access content owner that the contents of the table have |
| * been changed. |
| * |
| * @param filterTextEmpty whether the filter text used to calculate matches was |
| * empty |
| * @param showAllMatches whether the results were constrained by the size of |
| * the dialog |
| * |
| */ |
| protected abstract void updateFeedback(boolean filterTextEmpty, boolean showAllMatches); |
| |
| /** |
| * Sets whether to display all matches to the current filter or limit the |
| * results. Will refresh the table contents and update the info label. |
| * |
| * @param showAll whether to display all matches |
| */ |
| public void setShowAllMatches(boolean showAll) { |
| if (showAllMatches != showAll) { |
| showAllMatches = showAll; |
| updateInfoLabel(); |
| updateProposals(filterText.getText().toLowerCase()); |
| } |
| } |
| |
| private void updateInfoLabel() { |
| if (infoLabel != null) { |
| TriggerSequence sequence = getTriggerSequence(); |
| boolean forceHide = (getNumberOfFilteredResults() == 0) |
| || (showAllMatches && (table.getItemCount() <= computeNumberOfItems())); |
| if (sequence == null || forceHide) { |
| infoLabel.setText(""); //$NON-NLS-1$ |
| } else if (showAllMatches) { |
| infoLabel.setText( |
| NLS.bind(QuickAccessMessages.QuickAccessContents_PressKeyToLimitResults, sequence.format())); |
| } else { |
| infoLabel |
| .setText(NLS.bind(QuickAccessMessages.QuickAccess_PressKeyToShowAllMatches, sequence.format())); |
| } |
| infoLabel.getParent().layout(true); |
| } |
| } |
| |
| /** |
| * Returns the trigger sequence that can be used to open the quick access dialog |
| * as well as toggle the show all results feature. Can return <code>null</code> |
| * if no trigger sequence is known. |
| * |
| * @return the trigger sequence used to open the quick access or |
| * <code>null</code> |
| */ |
| public TriggerSequence getTriggerSequence() { |
| if (keySequence == null) { |
| IBindingService bindingService = Adapters.adapt(PlatformUI.getWorkbench(), IBindingService.class); |
| keySequence = bindingService.getBestActiveBindingFor(QUICK_ACCESS_COMMAND_ID); |
| } |
| return keySequence; |
| } |
| |
| /** |
| * Return whether the shell is currently set to display all matches or limit the |
| * results. |
| * |
| * @return whether all matches will be displayed |
| */ |
| public boolean getShowAllMatches() { |
| return showAllMatches; |
| } |
| |
| private void refreshTable(QuickAccessElement perfectMatch, List<QuickAccessEntry>[] entries, String filter) { |
| if (table.isDisposed()) { |
| return; |
| } |
| if (table.getItemCount() > entries.length && table.getItemCount() - entries.length > 20) { |
| table.removeAll(); |
| } |
| TableItem[] items = table.getItems(); |
| int selectionIndex = -1; |
| int index = 0; |
| for (List<QuickAccessEntry> entriesForCurrentCategory : entries) { |
| if (entriesForCurrentCategory != null) { |
| boolean firstEntry = true; |
| for (Iterator<QuickAccessEntry> it = entriesForCurrentCategory.iterator(); it.hasNext();) { |
| QuickAccessEntry entry = it.next(); |
| entry.firstInCategory = firstEntry; |
| firstEntry = false; |
| if (!it.hasNext()) { |
| entry.lastInCategory = true; |
| } |
| TableItem item; |
| if (index < items.length) { |
| item = items[index]; |
| table.clear(index); |
| } else { |
| item = new TableItem(table, SWT.NONE); |
| } |
| if (perfectMatch == entry.element && selectionIndex == -1) { |
| selectionIndex = index; |
| } |
| item.setData(entry); |
| item.setText(0, entry.provider.getName()); |
| item.setText(1, entry.element.getLabel()); |
| if (Util.isWpf()) { |
| item.setImage(1, entry.getImage(entry.element, resourceManager)); |
| } |
| index++; |
| } |
| } |
| } |
| if (index < items.length) { |
| table.remove(index, items.length - 1); |
| } |
| if (selectionIndex == -1) { |
| selectionIndex = 0; |
| } |
| |
| if (table.getItemCount() > 0) { |
| table.setSelection(selectionIndex); |
| hideHintText(); |
| } else if (filter.isEmpty()) { |
| showHintText(QuickAccessMessages.QuickAccess_StartTypingToFindMatches, grayColor); |
| } else { |
| showHintText(QuickAccessMessages.QuickAccessContents_NoMatchingResults, grayColor); |
| } |
| updateInfoLabel(); |
| updateFeedback(filter.isEmpty(), showAllMatches); |
| } |
| |
| int numberOfFilteredResults; |
| |
| /** |
| * Compute how many items are effectively filtered at a specific point in time. |
| * So doing, the quick access content can perform operations that depends on |
| * this number, i.e. hide the info label. |
| * |
| * @return number number of elements filtered |
| */ |
| protected int getNumberOfFilteredResults() { |
| return numberOfFilteredResults; |
| } |
| |
| /** |
| * Returns a list per provider containing matching {@link QuickAccessEntry} that |
| * should be displayed in the table given a text filter and a perfect match |
| * entry that should be given priority. The number of items returned is affected |
| * by {@link #getShowAllMatches()} and the size of the table's composite. |
| * |
| * @param filter the string text filter to apply, possibly empty |
| * @param perfectMatch a quick access element that should be given priority or |
| * <code>null</code> |
| * |
| * @param aMonitor |
| * @return the array of lists (one per provider) contains the quick access |
| * entries that should be added to the table, possibly empty |
| */ |
| private List<QuickAccessEntry>[] computeMatchingEntries(String filter, QuickAccessElement perfectMatch, |
| int maxNumberOfItemsInTable, IProgressMonitor aMonitor) { |
| if (aMonitor == null) { |
| aMonitor = new NullProgressMonitor(); |
| } |
| // check for a category filter, like "Views: " |
| Matcher categoryMatcher = getCategoryPattern().matcher(filter); |
| String category = null; |
| if (categoryMatcher.matches()) { |
| category = categoryMatcher.group(1); |
| filter = category + " " + categoryMatcher.group(2); //$NON-NLS-1$ |
| } |
| final String finalFilter = filter; |
| |
| // collect matching elements |
| @SuppressWarnings("unchecked") |
| LinkedHashMap<QuickAccessProvider, List<QuickAccessElement>> elementsForProviders = new LinkedHashMap<>( |
| providers.length); |
| for (QuickAccessProvider provider : providers) { |
| if (aMonitor.isCanceled()) { |
| break; |
| } |
| boolean isPreviousPickProvider = provider instanceof PreviousPicksProvider; |
| // skip if filter contains a category, and current provider isn't this category |
| if (category != null && !category.equalsIgnoreCase(provider.getName()) && !isPreviousPickProvider) { |
| continue; |
| } |
| if (!filter.isEmpty() || isPreviousPickProvider || showAllMatches) { |
| AtomicReference<List<QuickAccessElement>> sortedElementRef = new AtomicReference<>(); |
| if (provider.requiresUiAccess()) { |
| UIJob job = new UIJob( |
| NLS.bind(QuickAccessMessages.QuickAccessContents_processingProviderInUI, |
| provider.getName())) { |
| @Override |
| public IStatus runInUIThread(IProgressMonitor monitor) { |
| sortedElementRef.set(Arrays.asList(provider.getElementsSorted(finalFilter, monitor))); |
| return Status.OK_STATUS; |
| } |
| }; |
| job.setPriority(Job.INTERACTIVE); |
| job.schedule(); |
| try { |
| job.join(0, new NullProgressMonitor()); |
| } catch (Exception e) { |
| WorkbenchPlugin.log(e); |
| } |
| } else { |
| sortedElementRef.set(Arrays.asList(provider.getElementsSorted(filter, aMonitor))); |
| } |
| List<QuickAccessElement> sortedElements = sortedElementRef.get(); |
| if (sortedElements == null) { |
| sortedElements = Collections.emptyList(); |
| } |
| if (!(provider instanceof PreviousPicksProvider)) { |
| for (QuickAccessElement element : sortedElements) { |
| elementsToProviders.put(element, provider); |
| } |
| } |
| if (!filter.isEmpty() && !sortedElements.isEmpty()) { |
| sortedElements = putPrefixMatchFirst(sortedElements, filter); |
| } |
| elementsForProviders.put(provider, new ArrayList<>(sortedElements)); |
| } |
| } |
| |
| // Sort out the Previous Pick |
| List<String> prevPickIds = new ArrayList<>(); |
| for (Entry<QuickAccessProvider, List<QuickAccessElement>> entry : elementsForProviders.entrySet()) { |
| if (entry.getKey() instanceof PreviousPicksProvider) { |
| prevPickIds |
| .addAll(entry.getValue().stream().map(QuickAccessElement::getId).collect(Collectors.toList())); |
| } |
| } |
| for (Entry<QuickAccessProvider, List<QuickAccessElement>> entry : elementsForProviders.entrySet()) { |
| if (!(entry.getKey() instanceof PreviousPicksProvider)) { |
| List<QuickAccessElement> filteredElements = new ArrayList<>(entry.getValue()); |
| filteredElements.removeIf(element -> prevPickIds.contains(element.getId())); |
| entry.setValue(filteredElements); |
| } |
| } |
| // remove perfect match (will be added on top later) |
| QuickAccessProvider perfectMatchProvider = null; |
| if (perfectMatch != null) { |
| for (Entry<QuickAccessProvider, List<QuickAccessElement>> entry : elementsForProviders.entrySet()) { |
| if (perfectMatchProvider != null) { |
| List<QuickAccessElement> filteredElements = new ArrayList<>(entry.getValue()); |
| if (filteredElements.removeIf(element -> prevPickIds.contains(element.getId()))) { |
| entry.setValue(filteredElements); |
| perfectMatchProvider = entry.getKey(); |
| } |
| } |
| } |
| } |
| LinkedHashMap<QuickAccessProvider, List<QuickAccessEntry>> entriesPerProvider = new LinkedHashMap<>( |
| elementsForProviders.size()); |
| if (showAllMatches) { |
| // Map elements to entries |
| for (Entry<QuickAccessProvider, List<QuickAccessElement>> elementsPerProvider : elementsForProviders |
| .entrySet()) { |
| QuickAccessProvider provider = elementsPerProvider.getKey(); |
| List<QuickAccessEntry> entries = elementsPerProvider.getValue().stream() // |
| .map(QuickAccessMatcher::new) // |
| .map(matcher -> matcher.match(finalFilter, provider)) // |
| .filter(Objects::nonNull) // |
| .collect(Collectors.toList()); |
| if (!entries.isEmpty()) { |
| entriesPerProvider.put(provider, entries); |
| } |
| } |
| } else { |
| int numberOfSlotsLeft = perfectMatch != null ? maxNumberOfItemsInTable -1 : maxNumberOfItemsInTable; |
| while (!elementsForProviders.isEmpty() && numberOfSlotsLeft > 0) { |
| int nbEntriesPerProvider = numberOfSlotsLeft / elementsForProviders.size(); |
| if (nbEntriesPerProvider > 0) { |
| for (Entry<QuickAccessProvider, List<QuickAccessElement>> elementsPerProvider : elementsForProviders |
| .entrySet()) { |
| QuickAccessProvider provider = elementsPerProvider.getKey(); |
| List<QuickAccessElement> elements = elementsPerProvider.getValue(); |
| int toPickEntries = nbEntriesPerProvider; |
| while (toPickEntries > 0 && !elements.isEmpty()) { |
| QuickAccessElement element = elements.remove(0); |
| QuickAccessEntry entry = new QuickAccessMatcher(element).match(filter, provider); |
| if (entry != null) { |
| numberOfSlotsLeft--; |
| toPickEntries--; |
| if (!entriesPerProvider.containsKey(provider)) { |
| entriesPerProvider.put(provider, new LinkedList<>()); |
| } |
| entriesPerProvider.get(provider).add(entry); |
| } |
| } |
| } |
| } else { |
| for (Entry<QuickAccessProvider, List<QuickAccessElement>> elementsForProvider : elementsForProviders |
| .entrySet()) { |
| if (numberOfSlotsLeft > 0) { |
| QuickAccessProvider provider = elementsForProvider.getKey(); |
| List<QuickAccessElement> elements = elementsForProvider.getValue(); |
| boolean entryPicked = false; |
| while (!entryPicked && !elements.isEmpty()) { |
| QuickAccessElement element = elements.remove(0); |
| QuickAccessEntry entry = new QuickAccessMatcher(element).match(filter, provider); |
| if (entry != null) { |
| numberOfSlotsLeft--; |
| entryPicked = true; |
| if (!entriesPerProvider.containsKey(provider)) { |
| entriesPerProvider.put(provider, new LinkedList<>()); |
| } |
| entriesPerProvider.get(provider).add(entry); |
| } |
| } |
| } |
| } |
| } |
| Set<QuickAccessProvider> exhaustedProviders = new HashSet<>(); |
| elementsForProviders.forEach((provider, elements) -> { |
| if (elements.isEmpty()) { |
| exhaustedProviders.add(provider); |
| } |
| }); |
| exhaustedProviders.forEach(elementsForProviders::remove); |
| } |
| } |
| // |
| List<List<QuickAccessEntry>> res = new ArrayList<>(); |
| if (perfectMatch != null) { |
| res.add(Collections.singletonList(new QuickAccessEntry(perfectMatch, |
| perfectMatchProvider != null ? perfectMatchProvider : providers[0], new int[0][0], new int[0][0], |
| QuickAccessEntry.MATCH_PERFECT))); |
| } |
| res.addAll(entriesPerProvider.values()); |
| return (List<QuickAccessEntry>[]) res.toArray(new List<?>[res.size()]); |
| } |
| |
| /* |
| * Consider whether we could directly check the "matchQuality" here, but it |
| * seems to be a more expensive operation |
| */ |
| private static List<QuickAccessElement> putPrefixMatchFirst(List<QuickAccessElement> elements, String prefix) { |
| List<QuickAccessElement> res = new ArrayList<>(elements); |
| List<Integer> matchingIndexes = new ArrayList<>(); |
| for (int i = 0; i < elements.size(); i++) { |
| if (elements.get(i).getLabel().toLowerCase().startsWith(prefix.toLowerCase())) { |
| matchingIndexes.add(Integer.valueOf(i)); |
| } |
| } |
| int currentMatchIndex = 0; |
| int currentNonMatchIndex = matchingIndexes.size(); |
| for (int i = 0; i < res.size(); i++) { |
| boolean isMatch = !matchingIndexes.isEmpty() && matchingIndexes.iterator().next().intValue() == i; |
| if (isMatch) { |
| matchingIndexes.remove(0); |
| res.set(currentMatchIndex, elements.get(i)); |
| currentMatchIndex++; |
| } else { |
| res.set(currentNonMatchIndex, elements.get(i)); |
| currentNonMatchIndex++; |
| } |
| } |
| return res; |
| } |
| |
| Pattern categoryPattern; |
| |
| /** |
| * Return a pattern like {@code "^(:?Views|Perspective):\\s?(.*)"}, with all the |
| * provider names separated by semicolon. |
| * |
| * @return Returns the patternProvider. |
| */ |
| protected Pattern getCategoryPattern() { |
| if (categoryPattern == null) { |
| // build regex like "^(:?Views|Perspective):\\s?(.*)" |
| StringBuilder sb = new StringBuilder(); |
| sb.append("^(:?"); //$NON-NLS-1$ |
| for (int i = 0; i < providers.length; i++) { |
| if (i != 0) |
| sb.append("|"); //$NON-NLS-1$ |
| sb.append(providers[i].getName()); |
| } |
| sb.append("):\\s?(.*)"); //$NON-NLS-1$ |
| String regex = sb.toString(); |
| categoryPattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); |
| } |
| return categoryPattern; |
| } |
| |
| private void doDispose() { |
| if (textLayout != null && !textLayout.isDisposed()) { |
| textLayout.dispose(); |
| } |
| if (resourceManager != null) { |
| // Disposing the resource manager will dispose the color |
| resourceManager.dispose(); |
| resourceManager = null; |
| } |
| } |
| |
| protected String getId() { |
| return "org.eclipse.ui.internal.QuickAccess"; //$NON-NLS-1$ |
| } |
| |
| protected abstract void handleElementSelected(String text, Object selectedElement); |
| |
| private void handleSelection() { |
| QuickAccessElement selectedElement = null; |
| String text = filterText.getText().toLowerCase(); |
| if (table.getSelectionCount() == 1) { |
| QuickAccessEntry entry = (QuickAccessEntry) table.getSelection()[0].getData(); |
| selectedElement = entry == null ? null : entry.element; |
| } |
| if (selectedElement != null) { |
| doClose(); |
| handleElementSelected(text, selectedElement); |
| } |
| } |
| |
| /** |
| * Should be called by the owner of the parent composite when the shell is being |
| * activated (made visible). This allows the show all keybinding to be updated. |
| */ |
| public void preOpen() { |
| // Make sure we always start filtering |
| setShowAllMatches(false); |
| // In case the key binding has changed, update the label |
| keySequence = null; |
| updateInfoLabel(); |
| } |
| |
| /** |
| * Informs the owner of the parent composite that the quick access dialog should |
| * be closed |
| */ |
| protected abstract void doClose(); |
| |
| /** |
| * Allows the dialog contents to interact correctly with the text box used to |
| * open it |
| * |
| * @param filterText text box to hook up |
| */ |
| public void hookFilterText(Text filterText) { |
| this.filterText = filterText; |
| filterText.addKeyListener(new KeyListener() { |
| @Override |
| public void keyPressed(KeyEvent e) { |
| switch (e.keyCode) { |
| case SWT.CR: |
| case SWT.KEYPAD_CR: |
| handleSelection(); |
| break; |
| case SWT.ARROW_DOWN: |
| int index = table.getSelectionIndex(); |
| if (index != -1 && table.getItemCount() > index + 1) { |
| table.setSelection(index + 1); |
| } |
| break; |
| case SWT.ARROW_UP: |
| index = table.getSelectionIndex(); |
| if (index != -1 && index >= 1) { |
| table.setSelection(index - 1); |
| } |
| break; |
| case SWT.ESC: |
| doClose(); |
| break; |
| } |
| } |
| |
| @Override |
| public void keyReleased(KeyEvent e) { |
| // do nothing |
| } |
| }); |
| filterText.addModifyListener(e -> { |
| String text = ((Text) e.widget).getText(); |
| updateProposals(text); |
| }); |
| } |
| |
| Label hintText; |
| private boolean displayHintText; |
| |
| /** Create HintText as child of the given parent composite */ |
| Label createHintText(Composite composite, int defaultOrientation) { |
| hintText = new Label(composite, SWT.FILL); |
| hintText.setOrientation(defaultOrientation); |
| displayHintText = true; |
| return hintText; |
| } |
| |
| /** Hide the hint text */ |
| private void hideHintText() { |
| if (displayHintText) { |
| setHintTextToDisplay(false); |
| } |
| } |
| |
| /** Show the hint text with the given color */ |
| private void showHintText(String text, Color color) { |
| if (hintText == null || hintText.isDisposed()) { |
| // toolbar hidden |
| return; |
| } |
| hintText.setText(text); |
| if (color != null) { |
| hintText.setForeground(color); |
| } |
| if (!displayHintText) { |
| setHintTextToDisplay(true); |
| } |
| } |
| |
| /** |
| * Sets hint text to be displayed and requests the layout |
| * |
| * @param toDisplay |
| */ |
| private void setHintTextToDisplay(boolean toDisplay) { |
| GridData data = (GridData) hintText.getLayoutData(); |
| data.exclude = !toDisplay; |
| hintText.setVisible(toDisplay); |
| hintText.requestLayout(); |
| this.displayHintText = toDisplay; |
| } |
| |
| /** |
| * Creates the table providing the contents for the quick access dialog |
| * |
| * @param composite parent composite with {@link GridLayout} |
| * @param defaultOrientation the window orientation to use for the table |
| * {@link SWT#RIGHT_TO_LEFT} or |
| * {@link SWT#LEFT_TO_RIGHT} |
| * @return the created table |
| */ |
| public Table createTable(Composite composite, int defaultOrientation) { |
| composite.addDisposeListener(e -> doDispose()); |
| Composite tableComposite = new Composite(composite, SWT.NONE); |
| GridDataFactory.fillDefaults().grab(true, true).applyTo(tableComposite); |
| TableColumnLayout tableColumnLayout = new TableColumnLayout(); |
| tableComposite.setLayout(tableColumnLayout); |
| table = new Table(tableComposite, SWT.SINGLE | SWT.FULL_SELECTION); |
| textLayout = new TextLayout(table.getDisplay()); |
| textLayout.setOrientation(defaultOrientation); |
| Font boldFont = resourceManager.createFont(FontDescriptor.createFrom(table.getFont()).setStyle(SWT.BOLD)); |
| textLayout.setFont(table.getFont()); |
| textLayout.setText(QuickAccessMessages.QuickAccess_AvailableCategories); |
| int maxProviderWidth = (textLayout.getBounds().width); |
| textLayout.setFont(boldFont); |
| for (QuickAccessProvider provider : providers) { |
| textLayout.setText(provider.getName()); |
| int width = (textLayout.getBounds().width); |
| if (width > maxProviderWidth) { |
| maxProviderWidth = width; |
| } |
| } |
| tableColumnLayout.setColumnData(new TableColumn(table, SWT.NONE), new ColumnWeightData(0, maxProviderWidth)); |
| tableColumnLayout.setColumnData(new TableColumn(table, SWT.NONE), new ColumnWeightData(100, 100)); |
| table.getShell().addControlListener(new ControlAdapter() { |
| @Override |
| public void controlResized(ControlEvent e) { |
| if (!showAllMatches) { |
| if (!resized) { |
| resized = true; |
| e.display.timerExec(100, () -> { |
| if (table != null && !table.isDisposed() && filterText != null |
| && !filterText.isDisposed()) { |
| updateProposals(filterText.getText().toLowerCase()); |
| } |
| resized = false; |
| }); |
| } |
| } |
| } |
| }); |
| |
| table.addKeyListener(new KeyListener() { |
| @Override |
| public void keyPressed(KeyEvent e) { |
| if (e.keyCode == SWT.ARROW_UP && table.getSelectionIndex() == 0) { |
| filterText.setFocus(); |
| } else if (e.character == SWT.ESC) { |
| doClose(); |
| } |
| } |
| |
| @Override |
| public void keyReleased(KeyEvent e) { |
| // do nothing |
| } |
| }); |
| table.addMouseListener(new MouseAdapter() { |
| @Override |
| public void mouseUp(MouseEvent e) { |
| |
| if (table.getSelectionCount() < 1) |
| return; |
| |
| if (e.button != 1) |
| return; |
| |
| if (table.equals(e.getSource())) { |
| Object o = table.getItem(new Point(e.x, e.y)); |
| TableItem selection = table.getSelection()[0]; |
| if (selection.equals(o)) |
| handleSelection(); |
| } |
| } |
| }); |
| |
| table.addMouseMoveListener(new MouseMoveListener() { |
| TableItem lastItem = null; |
| |
| @Override |
| public void mouseMove(MouseEvent e) { |
| if (table.equals(e.getSource())) { |
| TableItem tableItem = table.getItem(new Point(e.x, e.y)); |
| if (lastItem == null ^ tableItem == null) { |
| table.setCursor(tableItem == null ? null : table.getDisplay().getSystemCursor(SWT.CURSOR_HAND)); |
| } |
| if (tableItem != null) { |
| if (!tableItem.equals(lastItem)) { |
| lastItem = tableItem; |
| table.setSelection(new TableItem[] { lastItem }); |
| } |
| } else { |
| lastItem = null; |
| } |
| } |
| } |
| }); |
| |
| table.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetDefaultSelected(SelectionEvent e) { |
| handleSelection(); |
| } |
| }); |
| |
| final TextStyle boldStyle; |
| if (PlatformUI.getPreferenceStore().getBoolean(IWorkbenchPreferenceConstants.USE_COLORED_LABELS)) { |
| boldStyle = new TextStyle(boldFont, null, null); |
| grayColor = resourceManager |
| .createColor(ColorUtil.blend(table.getBackground().getRGB(), table.getForeground().getRGB())); |
| } else { |
| boldStyle = null; |
| } |
| Listener listener = event -> { |
| QuickAccessEntry entry = (QuickAccessEntry) event.item.getData(); |
| if (entry != null) { |
| switch (event.type) { |
| case SWT.MeasureItem: |
| entry.measure(event, textLayout, resourceManager, boldStyle); |
| break; |
| case SWT.PaintItem: |
| entry.paint(event, textLayout, resourceManager, boldStyle, grayColor); |
| break; |
| case SWT.EraseItem: |
| entry.erase(event); |
| break; |
| } |
| } |
| }; |
| table.addListener(SWT.MeasureItem, listener); |
| table.addListener(SWT.EraseItem, listener); |
| table.addListener(SWT.PaintItem, listener); |
| |
| return table; |
| } |
| |
| /** |
| * Creates a label which will display the key binding to expand the search |
| * results. |
| * |
| * @param parent parent composite with {@link GridLayout} |
| * @return the created label |
| */ |
| public Label createInfoLabel(Composite parent) { |
| infoLabel = new Label(parent, SWT.NONE); |
| infoLabel.setFont(parent.getFont()); |
| infoLabel.setForeground(grayColor); |
| infoLabel.setBackground(table.getBackground()); |
| GridData gd = new GridData(GridData.FILL_HORIZONTAL); |
| gd.horizontalAlignment = SWT.RIGHT; |
| gd.grabExcessHorizontalSpace = false; |
| infoLabel.setLayoutData(gd); |
| updateInfoLabel(); |
| return infoLabel; |
| } |
| |
| QuickAccessProvider getProvider(String providerId) { |
| if (providers == null || providers.length == 0) { |
| return null; |
| } |
| if (providerMap == null || providerMap.size() != providers.length) { |
| providerMap = Arrays.stream(providers) |
| .collect(Collectors.toMap(QuickAccessProvider::getId, Function.identity())); |
| } |
| return providerMap.get(providerId); |
| } |
| |
| QuickAccessProvider getProviderFor(QuickAccessElement quickAccessElement) { |
| return elementsToProviders.get(quickAccessElement); |
| } |
| |
| void registerProviderFor(QuickAccessElement quickAccessElement, QuickAccessProvider quickAccessProvider) { |
| if (quickAccessElement == null || quickAccessProvider == null) { |
| return; |
| } |
| elementsToProviders.put(quickAccessElement, quickAccessProvider); |
| } |
| |
| public Text getFilterText() { |
| return filterText; |
| } |
| |
| public Table getTable() { |
| return table; |
| } |
| } |