| /******************************************************************************* |
| * 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 |
| * 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.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.function.Function; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| import org.eclipse.core.runtime.Adapters; |
| import org.eclipse.jface.bindings.TriggerSequence; |
| import org.eclipse.jface.dialogs.IDialogSettings; |
| import org.eclipse.jface.layout.GridDataFactory; |
| import org.eclipse.jface.layout.TableColumnLayout; |
| import org.eclipse.jface.resource.FontDescriptor; |
| import org.eclipse.jface.resource.ImageDescriptor; |
| 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.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.IWorkbenchGraphicConstants; |
| import org.eclipse.ui.internal.WorkbenchImages; |
| import org.eclipse.ui.internal.WorkbenchPlugin; |
| import org.eclipse.ui.keys.IBindingService; |
| 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$ |
| private static final int INITIAL_COUNT_PER_PROVIDER = 5; |
| private static final int MAX_COUNT_TOTAL = 20; |
| /** Minumum length to suggest the user to search typed text in the Help */ |
| private static final int MIN_SEARCH_LENGTH = 3; |
| |
| protected Text filterText; |
| |
| private QuickAccessProvider[] providers; |
| private Map<String, QuickAccessProvider> providerMap = new HashMap<>(); |
| private Map<QuickAccessElement, QuickAccessProvider> elementsToProviders = new HashMap<>(); |
| private Map<QuickAccessElement, QuickAccessMatcher> elementsToMatchers = 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; |
| |
| 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 refresh(String filter) { |
| if (table != null) { |
| boolean filterTextEmpty = filter.length() == 0; |
| |
| // extra entry added when the user activates help search |
| // (extensible) |
| List<QuickAccessEntry> extraEntries = new ArrayList<>(); |
| if (filter.length() > MIN_SEARCH_LENGTH) { |
| extraEntries.add(makeHelpSearchEntry(filter)); |
| } |
| |
| // perfect match, to be selected in the table if not null |
| QuickAccessElement perfectMatch = getPerfectMatch(filter); |
| |
| List<QuickAccessEntry>[] entries = computeMatchingEntries(filter, perfectMatch, extraEntries); |
| int selectionIndex = refreshTable(perfectMatch, entries, extraEntries); |
| |
| if (table.getItemCount() > 0) { |
| table.setSelection(selectionIndex); |
| hideHintText(); |
| } else if (filterTextEmpty) { |
| showHintText(QuickAccessMessages.QuickAccess_StartTypingToFindMatches, grayColor); |
| } else { |
| showHintText(QuickAccessMessages.QuickAccessContents_NoMatchingResults, grayColor); |
| } |
| |
| // update info as-you-type |
| updateInfoLabel(); |
| |
| updateFeedback(filterTextEmpty, showAllMatches); |
| } |
| } |
| |
| QuickAccessEntry searchHelpEntry = null; |
| QuickAccessProvider searchHelpProvider = null; |
| QuickAccessHelpSearchElement searchHelpElement = null; |
| |
| /** |
| * Instantiate a new {@link QuickAccessEntry} to search the given text in the |
| * eclipse help |
| * |
| * @param text String to search in the Eclipse Help |
| * |
| * @return the {@link QuickAccessEntry} to perform the action |
| */ |
| private QuickAccessEntry makeHelpSearchEntry(String text) { |
| if (searchHelpEntry == null) { |
| searchHelpProvider = new QuickAccessHelpSearchProvider(); |
| searchHelpElement = new QuickAccessHelpSearchElement(); |
| searchHelpEntry = new QuickAccessEntry(searchHelpElement, searchHelpProvider, new int[][] {}, |
| new int[][] {}, QuickAccessEntry.MATCH_PERFECT); |
| searchHelpEntry.firstInCategory = true; |
| } |
| searchHelpElement.searchText = text; |
| return searchHelpEntry; |
| } |
| |
| /** |
| * "Search X in help" element shown at the end of the list. |
| */ |
| static class QuickAccessHelpSearchElement extends QuickAccessElement { |
| |
| /** identifier */ |
| private static final String SEARCH_IN_HELP_ID = "search.in.help"; //$NON-NLS-1$ |
| |
| String searchText; |
| |
| public QuickAccessHelpSearchElement() { |
| } |
| |
| @Override |
| public String getLabel() { |
| return NLS.bind(QuickAccessMessages.QuickAccessContents_SearchInHelpLabel, searchText); |
| } |
| |
| @Override |
| public String getId() { |
| return SEARCH_IN_HELP_ID; |
| } |
| |
| @Override |
| public void execute() { |
| PlatformUI.getWorkbench().getHelpSystem().search(searchText); |
| } |
| |
| @Override |
| public ImageDescriptor getImageDescriptor() { |
| return WorkbenchImages.getImageDescriptor(IWorkbenchGraphicConstants.IMG_ETOOL_HELP_SEARCH); |
| } |
| |
| } |
| |
| /** |
| * Provider for the "Search X in help" element. Only used to have a category |
| * "Help". |
| */ |
| static class QuickAccessHelpSearchProvider extends QuickAccessProvider { |
| @Override |
| public String getName() { |
| return QuickAccessMessages.QuickAccessContents_HelpCategory; |
| } |
| |
| @Override |
| public String getId() { |
| return "search.help"; //$NON-NLS-1$ |
| } |
| |
| @Override |
| public ImageDescriptor getImageDescriptor() { |
| return null; |
| } |
| |
| @Override |
| public QuickAccessElement[] getElements() { |
| return null; |
| } |
| |
| @Override |
| public QuickAccessElement getElementForId(String id) { |
| return null; |
| } |
| |
| @Override |
| protected void doReset() { |
| // empty |
| } |
| } |
| |
| /** |
| * 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(); |
| refresh(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 int refreshTable(QuickAccessElement perfectMatch, List<QuickAccessEntry>[] entries, |
| List<QuickAccessEntry> extraEntries) { |
| // search help extra entry: not from search or previous picks. |
| int nExtraEntries = (extraEntries == null) ? 0 : extraEntries.size(); |
| if (table.getItemCount() > (entries.length + nExtraEntries) |
| && table.getItemCount() - (entries.length + nExtraEntries) > 20) { |
| table.removeAll(); |
| } |
| TableItem[] items = table.getItems(); |
| int selectionIndex = -1; |
| int index = 0; |
| for (int i = 0; i < providers.length; i++) { |
| if (entries[i] != null) { |
| boolean firstEntry = true; |
| for (Iterator<QuickAccessEntry> it = entries[i].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++; |
| } |
| } |
| } |
| // add extra entry |
| for (QuickAccessEntry entry : extraEntries) { |
| TableItem item = new TableItem(table, SWT.NONE); |
| item.setData(entry); |
| } |
| if (index < items.length) { |
| table.remove(index, items.length - 1); |
| } |
| if (selectionIndex == -1) { |
| selectionIndex = 0; |
| } |
| return selectionIndex; |
| } |
| |
| 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 extraEntries extra entries that will be added to the tabular |
| * visualization after computing matching entries, i.e. |
| * Search in Help |
| * @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, |
| List<QuickAccessEntry> extraEntries) { |
| // collect matches in an array of lists |
| @SuppressWarnings("unchecked") |
| List<QuickAccessEntry>[] entries = new List[providers.length]; |
| // extra entries are limiting the number of items for search results |
| int maxCount = computeNumberOfItems() - extraEntries.size(); |
| int[] indexPerProvider = new int[providers.length]; |
| int countPerProvider = Math.min(maxCount / 4, INITIAL_COUNT_PER_PROVIDER); |
| int prevPick = 0; |
| int countTotal = 0; |
| boolean perfectMatchAdded = true; |
| if (perfectMatch != null) { |
| // reserve one entry for the perfect match |
| maxCount--; |
| perfectMatchAdded = false; |
| } |
| boolean done; |
| String category = null; |
| Set<String> prevPickIds = new HashSet<>(); |
| do { |
| // will be set to false if we find a provider with remaining |
| // elements |
| done = true; |
| // check for a category filter, like "Views: " |
| Matcher categoryMatcher = getCategoryPattern().matcher(filter); |
| if (categoryMatcher.matches()) { |
| category = categoryMatcher.group(1); |
| filter = category + " " + categoryMatcher.group(2); //$NON-NLS-1$ |
| } |
| for (int i = 0; i < providers.length && (showAllMatches || countTotal < maxCount); i++) { |
| if (entries[i] == null) { |
| entries[i] = new ArrayList<>(); |
| indexPerProvider[i] = 0; |
| } |
| int count = 0; |
| QuickAccessProvider provider = providers[i]; |
| // when category is specified, skip providers except the |
| // specified one and the previous pick provider |
| boolean isPreviousPickProvider = (provider instanceof PreviousPicksProvider); |
| if (category != null && !category.equalsIgnoreCase(provider.getName()) && !isPreviousPickProvider) { |
| continue; |
| } |
| if (filter.length() > 0 || provider.isAlwaysPresent() || showAllMatches) { |
| QuickAccessElement[] sortedElements = provider.getElementsSorted(); |
| if (!(provider instanceof PreviousPicksProvider)) { |
| for (QuickAccessElement element : sortedElements) { |
| elementsToProviders.put(element, provider); |
| } |
| } |
| |
| // count previous picks and store ids |
| if (isPreviousPickProvider) { |
| prevPick = sortedElements.length; |
| Stream.of(sortedElements).forEach(e -> prevPickIds.add(e.getId())); |
| } |
| |
| int j = indexPerProvider[i]; |
| // loops on all the elements of a provider |
| while (j < sortedElements.length |
| && (showAllMatches || (count < countPerProvider && countTotal < maxCount))) { |
| QuickAccessElement element = sortedElements[j]; |
| |
| // Skip element if already in contained amid previous picks |
| if (!isPreviousPickProvider && prevPickIds.contains(element.getId())) { |
| j++; |
| continue; |
| } |
| |
| QuickAccessEntry entry = null; |
| if (filter.length() == 0) { |
| if (i == 0 || showAllMatches) { |
| entry = new QuickAccessEntry(element, provider, new int[0][0], new int[0][0], |
| QuickAccessEntry.MATCH_PERFECT); |
| } else { |
| entry = null; |
| } |
| } else { |
| QuickAccessEntry possibleMatch = getMatcherFor(element).match(filter, provider); |
| if (possibleMatch != null) { |
| entry = possibleMatch; |
| } |
| |
| } |
| if (entryEnabled(provider, entry)) { |
| entries[i].add(entry); |
| count++; |
| countTotal++; |
| if (i == 0 && entry.element == perfectMatch) { |
| perfectMatchAdded = true; |
| maxCount = MAX_COUNT_TOTAL; |
| } |
| } |
| |
| j++; |
| } |
| |
| indexPerProvider[i] = j; |
| |
| if (j < sortedElements.length) { |
| done = false; |
| } |
| } |
| } |
| |
| // from now on, add one element per provider |
| countPerProvider = 1; |
| |
| } while ((showAllMatches || countTotal < maxCount) && !done); |
| |
| if (!perfectMatchAdded) { |
| QuickAccessEntry entry = getMatcherFor(perfectMatch).match(filter, providers[0]); |
| if (entryEnabled(providers[0], entry)) { |
| if (entries[0] == null) { |
| entries[0] = new ArrayList<>(); |
| indexPerProvider[0] = 0; |
| } |
| entries[0].add(entry); |
| } |
| } |
| |
| // number of items matching the filtered search |
| numberOfFilteredResults = countTotal - prevPick; |
| return entries; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * @param provider |
| * @param entry |
| * @return <code>true</code> if the entry is enabled |
| */ |
| private boolean entryEnabled(QuickAccessProvider provider, QuickAccessEntry entry) { |
| if (entry == null) { |
| return false; |
| } |
| |
| // For a previous pick provider, check that the original provider does |
| // also provide the element |
| if (provider instanceof PreviousPicksProvider) { |
| QuickAccessElement element = entry.element; |
| final QuickAccessProvider originalProvider = getProviderFor(element); |
| QuickAccessElement match = originalProvider.getElementForId(element.getId()); |
| return match != null; |
| } |
| |
| return true; |
| } |
| |
| 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 IDialogSettings getDialogSettings() { |
| final IDialogSettings workbenchDialogSettings = WorkbenchPlugin.getDefault().getDialogSettings(); |
| IDialogSettings result = workbenchDialogSettings.getSection(getId()); |
| if (result == null) { |
| result = workbenchDialogSettings.addNewSection(getId()); |
| } |
| return result; |
| } |
| |
| 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().toLowerCase(); |
| refresh(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 */ |
| void hideHintText() { |
| if (displayHintText) { |
| setHintTextToDisplay(false); |
| } |
| } |
| |
| /** Show the hint text with the given color */ |
| void showHintText(String text, Color color) { |
| if (hintText == null) { |
| // 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()) { |
| refresh(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; |
| } |
| |
| public void resetProviders() { |
| for (QuickAccessProvider provider : providers) { |
| provider.reset(); |
| } |
| } |
| |
| 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); |
| } |
| |
| private QuickAccessMatcher getMatcherFor(QuickAccessElement element) { |
| if (!elementsToMatchers.containsKey(element)) { |
| elementsToMatchers.put(element, new QuickAccessMatcher(element)); |
| } |
| return elementsToMatchers.get(element); |
| } |
| |
| void registerProviderFor(QuickAccessElement quickAccessElement, QuickAccessProvider quickAccessProvider) { |
| if (quickAccessElement == null || quickAccessProvider == null) { |
| return; |
| } |
| elementsToProviders.put(quickAccessElement, quickAccessProvider); |
| } |
| |
| } |