Skip to content

Commit

Permalink
Merge from 5.x: PR #20593
Browse files Browse the repository at this point in the history
Fixes #20476
  • Loading branch information
ccordoba12 committed Mar 3, 2023
2 parents 728c90b + be96be0 commit dc90e8f
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 34 deletions.
4 changes: 4 additions & 0 deletions spyder/plugins/editor/widgets/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2175,10 +2175,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.
Expand Down
32 changes: 31 additions & 1 deletion spyder/plugins/editor/widgets/tests/test_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -89,7 +90,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
Expand Down Expand Up @@ -662,6 +663,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.
Expand Down
1 change: 1 addition & 0 deletions spyder/utils/icon_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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}],
Expand Down
96 changes: 64 additions & 32 deletions spyder/widgets/findreplace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -101,19 +104,19 @@ 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)
)
self.matches_string = ""

self.no_matches_icon = ima.icon('no_matches')
self.error_icon = ima.icon('error')
self.messages_action = QAction(self)
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 = (
Expand Down Expand Up @@ -248,12 +251,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.
Expand Down Expand Up @@ -345,20 +357,11 @@ 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)

self._resize_search_text()
self._width_adjustments()
self.visibility_changed.emit(True)
self.change_number_matches()

Expand Down Expand Up @@ -397,7 +400,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):
Expand Down Expand Up @@ -512,11 +515,14 @@ 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"""
self.matches_string = ""
self.messages_action.setVisible(False)
self.number_matches_text.hide()
if self.is_code_editor:
self.editor.clear_found_results()

Expand All @@ -536,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:
Expand Down Expand Up @@ -759,16 +764,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():
Expand All @@ -789,6 +790,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)
Expand Down Expand Up @@ -819,13 +835,29 @@ 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

# 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):
"""
Expand Down
2 changes: 1 addition & 1 deletion spyder/widgets/tests/test_findreplace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit dc90e8f

Please sign in to comment.