From d1c288ca7f615c88a8bad8ca1ea507f448155c5a Mon Sep 17 00:00:00 2001 From: Maximilian Wittmer Date: Wed, 29 May 2024 21:39:00 +0200 Subject: [PATCH] Find/Replace Overlay: Add a search history Add a search history for the Find/Replace overlay, displayed as a dropdown below the find/replace inputs. fixes #1907 --- .../icons/full/elcl16/open_history.png | Bin 0 -> 331 bytes .../icons/full/elcl16/open_history@2x.png | Bin 0 -> 556 bytes .../findandreplace/FindReplaceMessages.java | 2 + .../FindReplaceMessages.properties | 7 +- .../internal/findandreplace/HistoryStore.java | 4 + .../overlay/FindReplaceOverlay.java | 59 ++-- .../FindReplaceOverlayFirstTimePopup.java | 5 + .../overlay/FindReplaceOverlayImages.java | 6 +- .../overlay/HistoryTextWrapper.java | 201 +++++++++++ .../overlay/SearchHistoryMenu.java | 101 ++++++ .../texteditor/tests/OverlayAccess.java | 328 ++++++++++++++++++ 11 files changed, 689 insertions(+), 24 deletions(-) create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/elcl16/open_history.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/icons/full/elcl16/open_history@2x.png create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/HistoryTextWrapper.java create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/SearchHistoryMenu.java create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/OverlayAccess.java diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/elcl16/open_history.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/elcl16/open_history.png new file mode 100644 index 0000000000000000000000000000000000000000..d3620887168e3a03a05bd5726901fa7b8df13caa GIT binary patch literal 331 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9F5he4R}c>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt(q^T^vI!dhbrM%{uHL(0YH3 z&d!kO*VbmuSD1do%(WwU7iU~^q-)1jt~-(9Cseu9sdgIXk`M%_S4tJ4}1N8w=ia3*2IGYzr5|mTNM{Kkg2C zb=C5F8LwW~qez!+XZDyJ+rr?mJiX-HUV)2qX8KM`y<6{l?|10^?}d|8=0|1Qy*tms z5HRiOoSEicEBg(2>UXbO%}^7a{eAiEZYG8kpZ{obGbP0l+XkKX>*9P literal 0 HcmV?d00001 diff --git a/bundles/org.eclipse.ui.workbench.texteditor/icons/full/elcl16/open_history@2x.png b/bundles/org.eclipse.ui.workbench.texteditor/icons/full/elcl16/open_history@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d047a63ba0910c9a30b49f09830084623b37fe37 GIT binary patch literal 556 zcmV+{0@MA8P)6fqdZ-%A!7K~S&~8!N$1*V?nYUQj^<8?VwvSA@w7A)91p#q9*EgR>LF zf1t(LS&705hqc`mHns}7lf>oe-F>dwy{fpC{vMi5xI-N}b*GaB4DQ^q_09xzsRaLD5*bqXz+qu~Q0PlS_gfLDi z9TGxxl~Ny?<~AC^w7mCMQcCLpUXdK9lpdy(9>*B(X|1chWGw^$0M^gggm3j-`m2k$d#0a}0-pauBv0R8~F4z6_kX$XD*0000>>>>>> d0e86d6 Find/Replace Overlay: Add a search history FindReplaceOverlay_upSearchButton_toolTip=Search backward (Shift + Enter) FindReplaceOverlay_downSearchButton_toolTip=Search forward (Enter) FindReplaceOverlay_searchAllButton_toolTip=Search all (Ctrl + Enter) @@ -56,8 +57,10 @@ FindReplaceOverlay_caseSensitiveButton_toolTip=Match case (Ctrl + Shift + C) FindReplaceOverlay_wholeWordsButton_toolTip=Match whole word (Ctrl + Shift + W) FindReplaceOverlay_replaceButton_toolTip=Replace (Enter) FindReplaceOverlay_replaceAllButton_toolTip=Replace all (Ctrl + Enter) -FindReplaceOverlay_searchBar_message=Find -FindReplaceOverlay_replaceBar_message=Replace +FindReplaceOverlay_searchBar_message=Find (\u2195 for history) +FindReplaceOverlay_replaceBar_message=Replace (\u2195 for history) FindReplaceOverlay_replaceToggle_toolTip=Toggle input for replace (Ctrl + R) +FindReplaceOverlay_searchHistory_toolTip=Show search history +FindReplaceOverlay_replaceHistory_toolTip=Show replace history FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_message=Find and replace can now be done using an overlay embedded inside the editor. If you prefer the dialog, you can disable the overlay in the preferences or disable it now. FindReplaceOverlayFirstTimePopup_FindReplaceOverlayFirstTimePopup_title=New Find/Replace Overlay diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/HistoryStore.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/HistoryStore.java index 5d091011028..15054c9a73f 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/HistoryStore.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/HistoryStore.java @@ -109,4 +109,8 @@ private void writeHistory() { settingsManager.put(sectionName, names); } + public List asList() { + return new ArrayList<>(history); + } + } diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlay.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlay.java index b7d737c403a..7c9ad85cd08 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlay.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlay.java @@ -44,6 +44,8 @@ import org.eclipse.swt.widgets.ToolItem; import org.eclipse.swt.widgets.Widget; +import org.eclipse.core.runtime.Adapters; + import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.layout.GridDataFactory; @@ -61,6 +63,7 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.findandreplace.FindReplaceLogic; import org.eclipse.ui.internal.findandreplace.FindReplaceMessages; +import org.eclipse.ui.internal.findandreplace.HistoryStore; import org.eclipse.ui.internal.findandreplace.SearchOptions; import org.eclipse.ui.internal.findandreplace.status.IFindReplaceStatus; @@ -89,28 +92,36 @@ public class FindReplaceOverlay extends Dialog { private Composite searchContainer; private Composite searchBarContainer; - private Text searchBar; + private HistoryTextWrapper searchBar; private AccessibleToolBar searchTools; private ToolItem searchInSelectionButton; private ToolItem wholeWordSearchButton; private ToolItem caseSensitiveSearchButton; private ToolItem regexSearchButton; + + @SuppressWarnings("unused") private ToolItem searchUpButton; private ToolItem searchDownButton; + + @SuppressWarnings("unused") private ToolItem searchAllButton; private AccessibleToolBar closeTools; private ToolItem closeButton; private Composite replaceContainer; private Composite replaceBarContainer; - private Text replaceBar; + private HistoryTextWrapper replaceBar; private AccessibleToolBar replaceTools; + + @SuppressWarnings("unused") private ToolItem replaceButton; + @SuppressWarnings("unused") private ToolItem replaceAllButton; private Color backgroundToUse; private Color normalTextForegroundColor; private boolean positionAtTop = true; + private static final int HISTORY_SIZE = 15; public FindReplaceOverlay(Shell parent, IWorkbenchPart part, IFindReplaceTarget target) { super(parent); @@ -119,7 +130,6 @@ public FindReplaceOverlay(Shell parent, IWorkbenchPart part, IFindReplaceTarget setShellStyle(SWT.MODELESS); setBlockOnOpen(false); targetPart = part; - } @Override @@ -143,11 +153,14 @@ private void createFindReplaceLogic(IFindReplaceTarget target) { private void performReplaceAll() { BusyIndicator.showWhile(getShell() != null ? getShell().getDisplay() : Display.getCurrent(), () -> findReplaceLogic.performReplaceAll(getFindString(), getReplaceString())); + replaceBar.storeHistory(); + searchBar.storeHistory(); } private void performSelectAll() { BusyIndicator.showWhile(getShell() != null ? getShell().getDisplay() : Display.getCurrent(), () -> findReplaceLogic.performSelectAll(getFindString())); + searchBar.storeHistory(); } private KeyListener shortcuts = KeyListener.keyPressedAdapter(e -> { @@ -382,31 +395,22 @@ private void applyOverlayColors(Color color, boolean tryToColorReplaceBar) { closeButton.setBackground(color); searchTools.setBackground(color); - searchInSelectionButton.setBackground(color); - wholeWordSearchButton.setBackground(color); - regexSearchButton.setBackground(color); - caseSensitiveSearchButton.setBackground(color); - searchAllButton.setBackground(color); - searchUpButton.setBackground(color); - searchDownButton.setBackground(color); - searchBarContainer.setBackground(color); searchBar.setBackground(color); searchContainer.setBackground(color); if (replaceBarOpen && tryToColorReplaceBar) { replaceContainer.setBackground(color); - replaceBar.setBackground(color); replaceBarContainer.setBackground(color); - replaceAllButton.setBackground(color); - replaceButton.setBackground(color); + replaceTools.setBackground(color); + replaceBar.setBackground(color); } } private void unbindListeners() { getShell().removeShellListener(overlayDeactivationListener); if (targetPart != null && targetPart instanceof StatusTextEditor textEditor) { - Control targetWidget = textEditor.getAdapter(ITextViewer.class).getTextWidget(); + Control targetWidget = Adapters.adapt(textEditor, ITextViewer.class).getTextWidget(); if (targetWidget != null) { targetWidget.getShell().removeControlListener(shellMovementListener); targetWidget.removePaintListener(widgetMovementListener); @@ -418,7 +422,7 @@ private void unbindListeners() { private void bindListeners() { getShell().addShellListener(overlayDeactivationListener); if (targetPart instanceof StatusTextEditor textEditor) { - Control targetWidget = textEditor.getAdapter(ITextViewer.class).getTextWidget(); + Control targetWidget = Adapters.adapt(textEditor, ITextViewer.class).getTextWidget(); targetWidget.getShell().addControlListener(shellMovementListener); targetWidget.addPaintListener(widgetMovementListener); @@ -466,17 +470,20 @@ private void retrieveBackgroundColor() { textBarForRetrievingTheRightColor.dispose(); } + private void createSearchTools() { searchTools = new AccessibleToolBar(searchContainer); GridDataFactory.fillDefaults().grab(false, true).align(GridData.END, GridData.END).applyTo(searchTools); + @SuppressWarnings("unused") + ToolItem separator = searchTools.createToolItem(SWT.SEPARATOR); + createCaseSensitiveButton(); createRegexSearchButton(); createWholeWordsButton(); createAreaSearchButton(); - @SuppressWarnings("unused") - ToolItem separator = searchTools.createToolItem(SWT.SEPARATOR); + separator = searchTools.createToolItem(SWT.SEPARATOR); searchUpButton = new AccessibleToolItemBuilder(searchTools).withStyleBits(SWT.PUSH) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_FIND_PREV)) @@ -562,6 +569,10 @@ private void createReplaceTools() { Color warningColor = JFaceColors.getErrorText(getShell().getDisplay()); replaceTools = new AccessibleToolBar(replaceContainer); + + @SuppressWarnings("unused") + ToolItem separator = replaceTools.createToolItem(SWT.SEPARATOR); + GridDataFactory.fillDefaults().grab(false, true).align(GridData.CENTER, GridData.END).applyTo(replaceTools); replaceButton = new AccessibleToolItemBuilder(replaceTools).withStyleBits(SWT.PUSH) .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_REPLACE)) @@ -589,7 +600,9 @@ private void createReplaceTools() { } private void createSearchBar() { - searchBar = new Text(searchBarContainer, SWT.SINGLE); + HistoryStore searchHistory = new HistoryStore(getDialogSettings(), "searchhistory", //$NON-NLS-1$ + HISTORY_SIZE); + searchBar = new HistoryTextWrapper(searchHistory, searchBarContainer, SWT.SINGLE); GridDataFactory.fillDefaults().grab(true, false).align(GridData.FILL, GridData.END).applyTo(searchBar); searchBar.forceFocus(); searchBar.selectAll(); @@ -634,7 +647,8 @@ private void updateIncrementalSearch() { } private void createReplaceBar() { - replaceBar = new Text(replaceBarContainer, SWT.SINGLE); + HistoryStore replaceHistory = new HistoryStore(getDialogSettings(), "replacehistory", HISTORY_SIZE); //$NON-NLS-1$ + replaceBar = new HistoryTextWrapper(replaceHistory, replaceBarContainer, SWT.SINGLE); GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.END).applyTo(replaceBar); replaceBar.setMessage(FindReplaceMessages.FindReplaceOverlay_replaceBar_message); replaceBar.addFocusListener(FocusListener.focusLostAdapter(e -> { @@ -813,7 +827,7 @@ private void positionToPart() { } StatusTextEditor textEditor = (StatusTextEditor) targetPart; - Control targetWidget = textEditor.getAdapter(ITextViewer.class).getTextWidget(); + Control targetWidget = Adapters.adapt(textEditor, ITextViewer.class).getTextWidget(); if (!okayToUse(targetWidget)) { this.close(); return; @@ -848,6 +862,8 @@ private String getReplaceString() { private void performSingleReplace() { findReplaceLogic.performReplaceAndFind(getFindString(), getReplaceString()); + replaceBar.storeHistory(); + searchBar.storeHistory(); } private void performSearch(boolean forward) { @@ -857,6 +873,7 @@ private void performSearch(boolean forward) { findReplaceLogic.performSearch(getFindString()); activateInFindReplacerIf(SearchOptions.FORWARD, oldForwardSearchSetting); findReplaceLogic.activate(SearchOptions.INCREMENTAL); + searchBar.storeHistory(); } private void updateFromTargetSelection() { diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayFirstTimePopup.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayFirstTimePopup.java index 5b8a924b9a9..ea62b8f72a5 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayFirstTimePopup.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayFirstTimePopup.java @@ -41,6 +41,11 @@ * shown, informing the user about the new functionality. This class will track * whether the popup was already shown and will only show the Overlay on the * first time the popup was shown. +<<<<<<< Upstream, based on 4674cdf92b73b519d7f1fc85ba15112cfa2d5a2f + * + * @since 3.17 +======= +>>>>>>> 529474b Find/Replace overlay: move components into internal package */ public class FindReplaceOverlayFirstTimePopup { diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayImages.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayImages.java index 10b8dd71845..1278802720d 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayImages.java +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayImages.java @@ -31,10 +31,11 @@ /** * Provides Icons for the editor overlay used for performing * find/replace-operations. + * + * @since 3.17 */ class FindReplaceOverlayImages { private static final String PREFIX_ELCL = TextEditorPlugin.PLUGIN_ID + ".elcl."; //$NON-NLS-1$ - static final String KEY_CLOSE = PREFIX_ELCL + "close"; //$NON-NLS-1$ static final String KEY_FIND_NEXT = PREFIX_ELCL + "select_next"; //$NON-NLS-1$ static final String KEY_FIND_PREV = PREFIX_ELCL + "select_prev"; //$NON-NLS-1$ @@ -47,6 +48,7 @@ class FindReplaceOverlayImages { static final String KEY_SEARCH_IN_AREA = PREFIX_ELCL + "search_in_selection"; //$NON-NLS-1$ static final String KEY_OPEN_REPLACE_AREA = PREFIX_ELCL + "open_replace"; //$NON-NLS-1$ static final String KEY_CLOSE_REPLACE_AREA = PREFIX_ELCL + "close_replace"; //$NON-NLS-1$ + static final String KEY_OPEN_HISTORY = "open_history"; //$NON-NLS-1$ /** * The image registry containing {@link Image images}. @@ -57,6 +59,7 @@ class FindReplaceOverlayImages { private final static String ELCL = ICONS_PATH + "elcl16/"; //$NON-NLS-1$ + /** * Declare all images */ @@ -73,6 +76,7 @@ private static void declareImages() { declareRegistryImage(KEY_SEARCH_IN_AREA, ELCL + "search_in_area.png"); //$NON-NLS-1$ declareRegistryImage(KEY_OPEN_REPLACE_AREA, ELCL + "open_replace.png"); //$NON-NLS-1$ declareRegistryImage(KEY_CLOSE_REPLACE_AREA, ELCL + "close_replace.png"); //$NON-NLS-1$ + declareRegistryImage(KEY_OPEN_HISTORY, ELCL + "open_history.png"); //$NON-NLS-1$ } /** diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/HistoryTextWrapper.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/HistoryTextWrapper.java new file mode 100644 index 00000000000..7be91d76ea1 --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/HistoryTextWrapper.java @@ -0,0 +1,201 @@ +/******************************************************************************* + * Copyright (c) 2024 Vector Informatik GmbH 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: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.findandreplace.overlay; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Widget; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; + +import org.eclipse.ui.internal.findandreplace.FindReplaceMessages; +import org.eclipse.ui.internal.findandreplace.HistoryStore; + +/** + * Wrap a Text Bar and a ToolItem to add an input history. The text is only + * stored to history when requested by the Client code, the history is stored in + * a {@code HistoryStore} provided by the client. The history bar behaves like a + * normal {@code Text}. + */ +public class HistoryTextWrapper extends Composite { + private Text textBar; + private AccessibleToolBar tools; + private ToolItem dropDown; + private SearchHistoryMenu menu; + private HistoryStore history; + + public HistoryTextWrapper(HistoryStore history, Composite parent, int style) { + super(parent, SWT.NONE); + GridLayoutFactory.fillDefaults().numColumns(2).applyTo(this); + + this.history = history; + + textBar = new Text(this, style); + GridDataFactory.fillDefaults().grab(true, true).align(GridData.FILL, GridData.CENTER).applyTo(textBar); + tools = new AccessibleToolBar(this); + dropDown = new AccessibleToolItemBuilder(tools).withStyleBits(SWT.PUSH) + .withToolTipText(FindReplaceMessages.FindReplaceOverlay_searchHistory_toolTip) + .withImage(FindReplaceOverlayImages.get(FindReplaceOverlayImages.KEY_OPEN_HISTORY)) + .withSelectionListener(SelectionListener.widgetSelectedAdapter(e -> createHistoryMenuDropdown())) + .build(); + + listenForKeyboardHistoryNavigation(); + } + + private void listenForKeyboardHistoryNavigation() { + addKeyListener(KeyListener.keyPressedAdapter(e -> { + if (e.keyCode == SWT.ARROW_UP || e.keyCode == SWT.ARROW_DOWN) { + int stepDirection = e.keyCode == SWT.ARROW_UP ? 1 : -1; + navigateInHistory(stepDirection); + } + })); + } + + private void createHistoryMenuDropdown() { + if (menu != null && okayToUse(menu.getShell()) || !dropDown.isEnabled()) { + return; + } + + dropDown.setEnabled(false); + menu = new SearchHistoryMenu(getShell(), history, SelectionListener.widgetSelectedAdapter(f -> { + Table table = (Table) f.widget; + TableItem[] selection = table.getSelection(); + if (selection.length == 0) { + return; + } + String text = selection[0].getText(); + if (text != null) { + textBar.setText(text); + } + })); + + Point barPosition = textBar.toDisplay(0, 0); + menu.setPosition(barPosition.x, barPosition.y + dropDown.getWidth(), textBar.getSize().x + dropDown.getWidth()); + menu.open(); + + menu.getShell().addDisposeListener(new DisposeListener() { + + @Override + public void widgetDisposed(DisposeEvent e) { + getShell().getDisplay().timerExec(100, HistoryTextWrapper.this::enableDropDown); + } + }); + } + + private void enableDropDown() { + dropDown.setEnabled(true); + } + + private void navigateInHistory(int navigationOffset) { + int offset = history.asList().indexOf(textBar.getText()); + + offset += navigationOffset; + offset = offset % history.asList().size(); + + if (offset + navigationOffset < 0) { + offset = history.asList().size() - 1; + } + + textBar.setText(history.get(offset)); + } + + public void storeHistory() { + String string = textBar.getText(); + history.remove(string); // ensure findString is now on the newest index of the history + history.add(string); + } + + private static boolean okayToUse(final Widget widget) { + return widget != null && !widget.isDisposed(); + } + + public void selectAll() { + textBar.selectAll(); + } + + public void addModifyListener(final ModifyListener listener) { + textBar.addModifyListener(listener); + } + + @Override + public void addFocusListener(final FocusListener listener) { + textBar.addFocusListener(listener); + } + + @Override + public void addKeyListener(final KeyListener listener) { + textBar.addKeyListener(listener); + } + + public void setMessage(final String message) { + textBar.setMessage(message); + } + + public void setSelection(int i, int j) { + textBar.setSelection(i, j); + } + + @Override + public boolean isFocusControl() { + return textBar.isFocusControl(); + } + + public String getText() { + return textBar.getText(); + } + + public void setText(String str) { + textBar.setText(str); + } + + @Override + public void setBackground(Color color) { + super.setBackground(color); + + textBar.setBackground(color); + tools.setBackground(color); + } + + @Override + public void setForeground(Color color) { + super.setForeground(color); + + textBar.setForeground(color); + tools.setForeground(color); + } + + @Override + public boolean forceFocus() { + return textBar.forceFocus(); + } + + public Text getTextBar() { + return textBar; + } + +} diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/SearchHistoryMenu.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/SearchHistoryMenu.java new file mode 100644 index 00000000000..ad0d236e934 --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/findandreplace/overlay/SearchHistoryMenu.java @@ -0,0 +1,101 @@ +package org.eclipse.ui.internal.findandreplace.overlay; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.events.ShellListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.layout.GridDataFactory; + +import org.eclipse.ui.internal.findandreplace.HistoryStore; + +/** + * Menu dropdown for the search history in the find/replace overlay + */ +class SearchHistoryMenu extends Dialog { + private final SelectionListener selectionListener; + private final HistoryStore history; + private final ShellListener shellFocusListener = new ShellAdapter() { + @Override + public void shellDeactivated(ShellEvent e) { + if (!getShell().isDisposed()) { + getShell().getDisplay().asyncExec(SearchHistoryMenu.this::close); + } + } + }; + private Point location; + private int width; + + public SearchHistoryMenu(Shell parent, HistoryStore history, SelectionListener menuItemSelectionListener) { + super(parent); + setShellStyle(SWT.NONE); + setBlockOnOpen(false); + + this.selectionListener = menuItemSelectionListener; + this.history = history; + } + + public void setPosition(int x, int y, int width) { + location = new Point(x, y); + this.width = width; + } + + @Override + public Control createContents(Composite parent) { + Table table = new Table(parent, SWT.NONE); + GridDataFactory.fillDefaults().grab(true, true).align(SWT.FILL, SWT.FILL).applyTo(table); + TableColumn column = new TableColumn(table, SWT.NONE); + + for (String entry : history.get()) { + TableItem item = new TableItem(table, SWT.NONE); + item.setText(entry); + } + + table.addSelectionListener(selectionListener); + table.addMouseListener(MouseListener.mouseDownAdapter(e -> { + table.notifyListeners(SWT.Selection, null); + close(); + })); + table.addKeyListener(KeyListener.keyPressedAdapter(e -> { + if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { + close(); + } + })); + getShell().layout(); + + positionShell(table, column); + return table; + } + + private void positionShell(Table table, TableColumn column) { + if (location != null) { + getShell().setBounds(location.x, location.y, width, + Math.min(table.getItemHeight() * 7, table.getItemHeight() * (table.getItemCount() + 1))); + } + int columnSize = table.getSize().x; + if (table.getVerticalBar() != null) { + columnSize -= table.getVerticalBar().getSize().x; + } + column.setWidth(columnSize); + } + + @Override + public int open() { + int code = super.open(); + + getShell().addShellListener(shellFocusListener); + + return code; + } +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/OverlayAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/OverlayAccess.java new file mode 100644 index 00000000000..84755693c34 --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/OverlayAccess.java @@ -0,0 +1,328 @@ +/******************************************************************************* + * Copyright (c) 2024 Vector Informatik GmbH 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: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.workbench.texteditor.tests; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.ToolItem; + +import org.eclipse.text.tests.Accessor; + +import org.eclipse.jface.text.IFindReplaceTarget; +import org.eclipse.jface.text.IFindReplaceTargetExtension; + +import org.eclipse.ui.internal.findandreplace.FindReplaceLogic; +import org.eclipse.ui.internal.findandreplace.IFindReplaceLogic; +import org.eclipse.ui.internal.findandreplace.SearchOptions; +import org.eclipse.ui.internal.findandreplace.overlay.HistoryTextWrapper; + +class OverlayAccess implements IFindReplaceUIAccess { + FindReplaceLogic findReplaceLogic; + + HistoryTextWrapper find; + + HistoryTextWrapper replace; + + ToolItem inSelection; + + ToolItem caseSensitive; + + ToolItem wholeWord; + + ToolItem regEx; + + ToolItem searchForward; + + ToolItem searchBackward; + + Button openReplaceDialog; + + ToolItem replaceButton; + + ToolItem replaceAllButton; + + private Runnable closeOperation; + + Accessor dialogAccessor; + + private Supplier shellRetriever; + + OverlayAccess(Accessor findReplaceOverlayAccessor) { + dialogAccessor= findReplaceOverlayAccessor; + findReplaceLogic= (FindReplaceLogic) findReplaceOverlayAccessor.get("findReplaceLogic"); + find= (HistoryTextWrapper) findReplaceOverlayAccessor.get("searchBar"); + replace= (HistoryTextWrapper) findReplaceOverlayAccessor.get("replaceBar"); + caseSensitive= (ToolItem) findReplaceOverlayAccessor.get("caseSensitiveSearchButton"); + wholeWord= (ToolItem) findReplaceOverlayAccessor.get("wholeWordSearchButton"); + regEx= (ToolItem) findReplaceOverlayAccessor.get("regexSearchButton"); + searchForward= (ToolItem) findReplaceOverlayAccessor.get("searchDownButton"); + searchBackward= (ToolItem) findReplaceOverlayAccessor.get("searchUpButton"); + closeOperation= () -> findReplaceOverlayAccessor.invoke("close", null); + openReplaceDialog= (Button) findReplaceOverlayAccessor.get("replaceToggle"); + replaceButton= (ToolItem) findReplaceOverlayAccessor.get("replaceButton"); + replaceAllButton= (ToolItem) findReplaceOverlayAccessor.get("replaceAllButton"); + inSelection= (ToolItem) findReplaceOverlayAccessor.get("searchInSelectionButton"); + shellRetriever= () -> ((Shell) findReplaceOverlayAccessor.invoke("getShell", null)); + } + + @Override + public IFindReplaceTarget getTarget() { + return findReplaceLogic.getTarget(); + } + + private void restoreInitialConfiguration() { + find.setText(""); + select(SearchOptions.GLOBAL); + unselect(SearchOptions.REGEX); + unselect(SearchOptions.CASE_SENSITIVE); + unselect(SearchOptions.WHOLE_WORD); + } + + @Override + public void closeAndRestore() { + restoreInitialConfiguration(); + assertInitialConfiguration(); + closeOperation.run(); + } + + @Override + public void close() { + closeOperation.run(); + } + + @Override + public void select(SearchOptions option) { + ToolItem button= getButtonForSearchOption(option); + if (button == null) { + return; + } + button.setSelection(true); + if (option == SearchOptions.GLOBAL) { + button.setSelection(false); + } + button.notifyListeners(SWT.Selection, null); + } + + @Override + public void unselect(SearchOptions option) { + ToolItem button= getButtonForSearchOption(option); + if (button == null) { + return; + } + button.setSelection(false); + if (option == SearchOptions.GLOBAL) { + button.setSelection(true); + } + button.notifyListeners(SWT.Selection, null); + } + + @Override + public void simulateEnterInFindInputField(boolean shiftPressed) { + simulateKeyPressInFindInputField(SWT.CR, shiftPressed); + } + + @Override + public void simulateKeyPressInFindInputField(int keyCode, boolean shiftPressed) { + final Event event= new Event(); + event.type= SWT.KeyDown; + event.keyCode= keyCode; + if (shiftPressed) { + event.stateMask= SWT.SHIFT; + } + find.getTextBar().notifyListeners(SWT.KeyDown, event); + find.getTextBar().traverse(SWT.TRAVERSE_RETURN, event); + FindReplaceTestUtil.runEventQueue(); + } + + @Override + public String getFindText() { + return find.getText(); + } + + @Override + public String getReplaceText() { + return replace.getText(); + } + + @Override + public void setFindText(String text) { + find.setText(text); + find.getTextBar().notifyListeners(SWT.Modify, null); + } + + @Override + public void setReplaceText(String text) { + openReplaceDialog(); + replace.setText(text); + } + + @Override + public ToolItem getButtonForSearchOption(SearchOptions option) { + switch (option) { + case CASE_SENSITIVE: + return caseSensitive; + case REGEX: + return regEx; + case WHOLE_WORD: + return wholeWord; + case GLOBAL: + return inSelection; + //$CASES-OMITTED$ + default: + return null; + } + } + + private Set getEnabledOptions() { + return Arrays.stream(SearchOptions.values()) + .filter(option -> (getButtonForSearchOption(option) != null && getButtonForSearchOption(option).getEnabled())) + .collect(Collectors.toSet()); + } + + private Set getSelectedOptions() { + return Arrays.stream(SearchOptions.values()) + .filter(isOptionSelected()) + .collect(Collectors.toSet()); + } + + private Predicate isOptionSelected() { + return option -> { + ToolItem buttonForSearchOption= getButtonForSearchOption(option); + if (option == SearchOptions.GLOBAL) { + return !buttonForSearchOption.getSelection();// The "Global" option is mapped to a button that + // selects whether to search in the selection, thus inverting the semantic + } + return buttonForSearchOption != null && buttonForSearchOption.getSelection(); + }; + } + + public void pressSearch(boolean forward) { + if (forward) { + searchForward.notifyListeners(SWT.Selection, null); + } else { + searchBackward.notifyListeners(SWT.Selection, null); + } + } + + @Override + public IFindReplaceLogic getFindReplaceLogic() { + return findReplaceLogic; + } + + @Override + public void performReplaceAll() { + openReplaceDialog(); + replaceAllButton.notifyListeners(SWT.Selection, null); + } + + @Override + public void performReplace() { + openReplaceDialog(); + replaceButton.notifyListeners(SWT.Selection, null); + } + + public boolean isReplaceDialogOpen() { + return dialogAccessor.getBoolean("replaceBarOpen"); + } + + public void openReplaceDialog() { + if (!isReplaceDialogOpen() && Objects.nonNull(openReplaceDialog)) { + openReplaceDialog.notifyListeners(SWT.Selection, null); + replace= (HistoryTextWrapper) dialogAccessor.get("replaceBar"); + replaceButton= (ToolItem) dialogAccessor.get("replaceButton"); + replaceAllButton= (ToolItem) dialogAccessor.get("replaceAllButton"); + } + } + + public void closeReplaceDialog() { + if (isReplaceDialogOpen() && Objects.nonNull(openReplaceDialog)) { + openReplaceDialog.notifyListeners(SWT.Selection, null); + replace= null; + replaceButton= null; + replaceAllButton= null; + } + } + + @Override + public void performReplaceAndFind() { + performReplace(); + } + + @Override + public void assertInitialConfiguration() { + assertUnselected(SearchOptions.REGEX); + assertUnselected(SearchOptions.WHOLE_WORD); + assertUnselected(SearchOptions.CASE_SENSITIVE); + if (!doesTextViewerHaveMultiLineSelection(findReplaceLogic.getTarget())) { + assertSelected(SearchOptions.GLOBAL); + assertTrue(findReplaceLogic.isActive(SearchOptions.GLOBAL)); + } else { + assertUnselected(SearchOptions.GLOBAL); + assertFalse(findReplaceLogic.isActive(SearchOptions.GLOBAL)); + } + assertEnabled(SearchOptions.GLOBAL); + assertEnabled(SearchOptions.REGEX); + assertEnabled(SearchOptions.CASE_SENSITIVE); + if (getFindText().equals("") || findReplaceLogic.isWholeWordSearchAvailable(getFindText())) { + assertEnabled(SearchOptions.WHOLE_WORD); + } else { + assertDisabled(SearchOptions.WHOLE_WORD); + } + } + + private boolean doesTextViewerHaveMultiLineSelection(IFindReplaceTarget target) { + if (target instanceof IFindReplaceTargetExtension scopeProvider) { + return scopeProvider.getScope() != null; // null is returned for global scope + } + return false; + } + + @Override + public void assertUnselected(SearchOptions option) { + assertFalse(getSelectedOptions().contains(option)); + } + + @Override + public void assertSelected(SearchOptions option) { + assertTrue(getSelectedOptions().contains(option)); + } + + @Override + public void assertDisabled(SearchOptions option) { + assertFalse(getEnabledOptions().contains(option)); + } + + @Override + public void assertEnabled(SearchOptions option) { + assertTrue(getEnabledOptions().contains(option)); + } + + @Override + public Shell getActiveShell() { + return shellRetriever.get(); + } + +}