From 857eb5e845b62a32ba048695dc6ab51b6786e881 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 24 Feb 2023 22:18:42 -0500 Subject: [PATCH 1/5] Find/Replace: Show icon for matches when its width is small --- spyder/utils/icon_manager.py | 1 + spyder/widgets/findreplace.py | 63 ++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py index 583aa9c6c0f..e8ca151337b 100644 --- a/spyder/utils/icon_manager.py +++ b/spyder/utils/icon_manager.py @@ -181,6 +181,7 @@ def __init__(self): 'replace_next': [('mdi6.arrow-right-bottom',), {'color': self.MAIN_FG_COLOR}], 'replace_all': [('mdi.file-replace-outline',), {'color': self.MAIN_FG_COLOR}], 'replace_selection': [('ph.rectangle-bold',), {'color': self.MAIN_FG_COLOR}], + 'number_matches': [('mdi.pound-box-outline',), {'color': self.MAIN_FG_COLOR}], 'undo': [('mdi.undo',), {'color': self.MAIN_FG_COLOR}], 'redo': [('mdi.redo',), {'color': self.MAIN_FG_COLOR}], 'refresh': [('mdi.refresh',), {'color': self.MAIN_FG_COLOR}], diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py index e23a64c7d48..2ed5f643a00 100644 --- a/spyder/widgets/findreplace.py +++ b/spyder/widgets/findreplace.py @@ -16,7 +16,7 @@ # Third party imports from qtpy.QtCore import QEvent, QSize, Qt, QTimer, Signal, Slot -from qtpy.QtGui import QTextCursor +from qtpy.QtGui import QPixmap, QTextCursor from qtpy.QtWidgets import (QAction, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QToolButton, QSizePolicy, QSpacerItem, QWidget) @@ -81,6 +81,9 @@ def __init__(self, parent, enable_replace=False): ) glayout.addWidget(self.close_button, 0, 0) + # Icon size is the same for all buttons + self.icon_size = self.close_button.iconSize() + # Find layout self.search_text = SearchText(self) @@ -104,6 +107,11 @@ def __init__(self, parent, enable_replace=False): self.search_text.clear_action.triggered.connect( self.number_matches_text.hide ) + self.hide_number_matches_text = False + self.number_matches_pixmap = ( + ima.icon('number_matches').pixmap(self.icon_size) + ) + self.matches_string = "" self.no_matches_icon = ima.icon('no_matches') self.error_icon = ima.icon('error') @@ -358,7 +366,7 @@ def show(self, hide_replace=True): """Overrides Qt Method""" QWidget.show(self) - self._resize_search_text() + self._width_adjustments() self.visibility_changed.emit(True) self.change_number_matches() @@ -397,7 +405,7 @@ def show(self, hide_replace=True): def resizeEvent(self, event): super().resizeEvent(event) - self._resize_search_text() + self._width_adjustments() @Slot() def replace_widget(self, replace_on): @@ -517,6 +525,7 @@ def highlight_matches(self): def clear_matches(self): """Clear all highlighted matches""" + self.matches_string = "" if self.is_code_editor: self.editor.clear_found_results() @@ -748,16 +757,12 @@ def replace_find_selection(self, focus_replace_text=False): def change_number_matches(self, current_match=0, total_matches=0): """Change number of match and total matches.""" if current_match and total_matches: - self.number_matches_text.show() - self.messages_action.setVisible(False) - matches_string = u"{} {} {}".format(current_match, _(u"of"), - total_matches) - self.number_matches_text.setText(matches_string) + self.matches_string = "{} {} {}".format(current_match, _("of"), + total_matches) + self.show_matches() elif total_matches: - self.number_matches_text.show() - self.messages_action.setVisible(False) - matches_string = u"{} {}".format(total_matches, _(u"matches")) - self.number_matches_text.setText(matches_string) + self.matches_string = "{} {}".format(total_matches, _("matches")) + self.show_matches() else: self.number_matches_text.hide() if self.search_text.currentText(): @@ -778,6 +783,21 @@ def show_no_matches(self): """Show a no matches message with an icon.""" self._show_icon_message('no_matches') + def show_matches(self): + """Show the number of matches found in the document.""" + if not self.matches_string: + return + + self.number_matches_text.show() + self.messages_action.setVisible(False) + + if self.hide_number_matches_text: + self.number_matches_text.setPixmap(self.number_matches_pixmap) + self.number_matches_text.setToolTip(self.matches_string) + else: + self.number_matches_text.setPixmap(QPixmap()) + self.number_matches_text.setText(self.matches_string) + def show_error(self, error_msg): """Show a regexp error message with an icon.""" self._show_icon_message('error', extra_info=error_msg) @@ -808,13 +828,26 @@ def _show_icon_message(self, kind, extra_info=None): self.messages_action.setToolTip(tooltip) self.messages_action.setVisible(True) - def _resize_search_text(self): - """Adjust search_text combobox min width according to total one.""" + def _width_adjustments(self): + """Several adjustments according to the widget's total width.""" + # The widgets list includes search_text and number_matches_text. That's + # why we substract a 2 below. + buttons_width = self.icon_size.width() * (len(self.widgets) - 2) + total_width = self.size().width() - if total_width < (self.search_text.recommended_width + 200): + matches_width = self.number_matches_text.size().width() + minimal_width = ( + self.search_text.recommended_width + buttons_width + matches_width + ) + + if total_width < minimal_width: self.search_text.setMinimumWidth(30) + self.hide_number_matches_text = True else: self.search_text.setMinimumWidth(int(total_width / 2)) + self.hide_number_matches_text = False + + self.show_matches() def _resize_replace_text(self, size, old_size): """ From 65786c373906cd336714479793f1e265c02b01ed Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 25 Feb 2023 10:24:49 -0500 Subject: [PATCH 2/5] Find/Replace: Avoid flickering when resizing and matches are shown --- spyder/widgets/findreplace.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py index 2ed5f643a00..cf7f61f9601 100644 --- a/spyder/widgets/findreplace.py +++ b/spyder/widgets/findreplace.py @@ -256,12 +256,21 @@ def __init__(self, parent, enable_replace=False): self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.shortcuts = self.create_shortcuts(parent) + # To highlight found results in the editor self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(300) self.highlight_timer.timeout.connect(self.highlight_matches) + + # Install event filter for search_text self.search_text.installEventFilter(self) + # To avoid painting number_matches_text on every resize event + self.show_matches_timer = QTimer(self) + self.show_matches_timer.setSingleShot(True) + self.show_matches_timer.setInterval(25) + self.show_matches_timer.timeout.connect(self.show_matches) + def eventFilter(self, widget, event): """ Event filter for search_text widget. @@ -520,8 +529,8 @@ def highlight_matches(self): case = self.case_button.isChecked() word = self.words_button.isChecked() regexp = self.re_button.isChecked() - self.editor.highlight_found_results(text, word=word, - regexp=regexp, case=case) + self.editor.highlight_found_results( + text, word=word, regexp=regexp, case=case) def clear_matches(self): """Clear all highlighted matches""" @@ -847,7 +856,10 @@ def _width_adjustments(self): self.search_text.setMinimumWidth(int(total_width / 2)) self.hide_number_matches_text = False - self.show_matches() + # We don't call show_matches directly here to avoid flickering when the + # user hits the widget's minimal width, which changes from text to an + # icon (or vice versa) for number_matches_text. + self.show_matches_timer.start() def _resize_replace_text(self, size, old_size): """ From 7056ddff681aae5a38311fde00d9784067984a59 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 25 Feb 2023 10:41:02 -0500 Subject: [PATCH 3/5] Find/Replace: Clear results in the editor when clicking clear_action Also, remove unused toggle_highlighting method. --- spyder/widgets/findreplace.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py index cf7f61f9601..b5345eed6c9 100644 --- a/spyder/widgets/findreplace.py +++ b/spyder/widgets/findreplace.py @@ -104,9 +104,7 @@ def __init__(self, parent, enable_replace=False): self.search_text.sig_resized.connect(self._resize_replace_text) self.number_matches_text = QLabel(self) - self.search_text.clear_action.triggered.connect( - self.number_matches_text.hide - ) + self.search_text.clear_action.triggered.connect(self.clear_matches) self.hide_number_matches_text = False self.number_matches_pixmap = ( ima.icon('number_matches').pixmap(self.icon_size) @@ -119,9 +117,6 @@ def __init__(self, parent, enable_replace=False): self.messages_action.setVisible(False) self.search_text.lineEdit().addAction( self.messages_action, QLineEdit.TrailingPosition) - self.search_text.clear_action.triggered.connect( - lambda: self.messages_action.setVisible(False) - ) # Button corresponding to the messages_action above self.messages_button = ( @@ -362,15 +357,6 @@ def update_search_combo(self): def update_replace_combo(self): self.replace_text.lineEdit().returnPressed.emit() - @Slot(bool) - def toggle_highlighting(self, state): - """Toggle the 'highlight all results' feature""" - if self.editor is not None: - if state: - self.highlight_matches() - else: - self.clear_matches() - def show(self, hide_replace=True): """Overrides Qt Method""" QWidget.show(self) @@ -535,6 +521,8 @@ def highlight_matches(self): def clear_matches(self): """Clear all highlighted matches""" self.matches_string = "" + self.messages_action.setVisible(False) + self.number_matches_text.hide() if self.is_code_editor: self.editor.clear_found_results() @@ -554,7 +542,6 @@ def find(self, changed=True, forward=True, rehighlight=True, # Clears the selection for WebEngine self.editor.find_text('') self.change_number_matches() - self.messages_action.setVisible(False) self.clear_matches() return None else: From a25ac1f0f844226f206efcda7b311d6ce6b823e0 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 25 Feb 2023 11:16:12 -0500 Subject: [PATCH 4/5] Testing: Fix tests related to the FindReplace widget --- spyder/plugins/editor/widgets/tests/test_editor.py | 2 +- spyder/widgets/tests/test_findreplace.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/editor/widgets/tests/test_editor.py b/spyder/plugins/editor/widgets/tests/test_editor.py index 7cb9abbce8a..c71ddf698b8 100644 --- a/spyder/plugins/editor/widgets/tests/test_editor.py +++ b/spyder/plugins/editor/widgets/tests/test_editor.py @@ -89,7 +89,7 @@ def editor_find_replace_bot(base_editor_bot, qtbot): layout.addWidget(find_replace) # Resize widget and show - widget.resize(480, 360) + widget.resize(900, 360) widget.show() return widget diff --git a/spyder/widgets/tests/test_findreplace.py b/spyder/widgets/tests/test_findreplace.py index 9f93f703db7..89ceb049226 100644 --- a/spyder/widgets/tests/test_findreplace.py +++ b/spyder/widgets/tests/test_findreplace.py @@ -50,7 +50,7 @@ def findreplace_editor(qtbot, request): layout.addWidget(findreplace) # Resize widget and show - widget.resize(480, 360) + widget.resize(900, 360) widget.show() return widget From 68bc3f453d0dc10e87777e42844568de01229510 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 28 Feb 2023 22:46:39 -0500 Subject: [PATCH 5/5] Editor: Update number of matches in find widget when switching files Also, add a test that checks this new functionality --- spyder/plugins/editor/widgets/editor.py | 4 +++ .../editor/widgets/tests/test_editor.py | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/spyder/plugins/editor/widgets/editor.py b/spyder/plugins/editor/widgets/editor.py index 8633a4c82ed..53bf82e8356 100644 --- a/spyder/plugins/editor/widgets/editor.py +++ b/spyder/plugins/editor/widgets/editor.py @@ -2216,10 +2216,14 @@ def current_changed(self, index): pass self.update_plugin_title.emit() + # Make sure that any replace happens in the editor on top # See spyder-ide/spyder#9688. self.find_widget.set_editor(editor, refresh=False) + # Update total number of matches when switching files. + self.find_widget.update_matches() + if editor is not None: # Needed in order to handle the close of files open in a directory # that has been renamed. See spyder-ide/spyder#5157. diff --git a/spyder/plugins/editor/widgets/tests/test_editor.py b/spyder/plugins/editor/widgets/tests/test_editor.py index c71ddf698b8..492e79355c7 100644 --- a/spyder/plugins/editor/widgets/tests/test_editor.py +++ b/spyder/plugins/editor/widgets/tests/test_editor.py @@ -75,6 +75,7 @@ def editor_find_replace_bot(base_editor_bot, qtbot): editor_stack = base_editor_bot layout.addWidget(editor_stack) + widget.editor_stack = editor_stack text = ('spam bacon\n' 'spam sausage\n' @@ -664,6 +665,35 @@ def test_tab_copies_find_to_replace(editor_find_replace_bot, qtbot): assert finder.replace_text.currentText() == 'This is some test text!' +def test_update_matches_in_find_replace(editor_find_replace_bot, qtbot): + """ + Check that the total number of matches in the FindReplace widget is updated + when switching files. + """ + editor_stack = editor_find_replace_bot.editor_stack + finder = editor_find_replace_bot.find_replace + + # Search for "spam" in current file + finder.show(hide_replace=False) + finder.search_text.setFocus() + finder.search_text.set_current_text('spam') + qtbot.wait(500) + qtbot.keyClick(finder.search_text, Qt.Key_Return) + + # Open a new file and only write "spam" on it + editor_stack.new('foo.py', 'utf-8', 'spam') + + # Focus new file and check the number of matches was updated + editor_stack.set_stack_index(1) + assert finder.number_matches_text.text() == '1 matches' + qtbot.wait(500) + + # Focus initial file and check the number of matches was updated + editor_stack.set_stack_index(0) + qtbot.wait(500) + assert finder.number_matches_text.text() == '3 matches' + + def test_autosave_all(editor_bot, mocker): """ Test that `autosave_all()` calls maybe_autosave() on all open buffers.