From b4fa8ba93fdcf34696bb4abb4202838c04815cbd Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Fri, 23 Oct 2026 13:24:54 +0800 Subject: [PATCH] gh-67407: Improve IDLE Find, Replace and Find in Files dialogs --- Lib/idlelib/grep.py | 6 +-- Lib/idlelib/idle_test/htest.py | 10 ++--- Lib/idlelib/idle_test/test_searchbase.py | 45 +++++++++++++++---- Lib/idlelib/replace.py | 14 +++--- Lib/idlelib/search.py | 7 ++- Lib/idlelib/searchbase.py | 16 +++---- ...4-07-29-20-11-27.gh-issue-67407.nhILB2.rst | 1 + 7 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-07-29-20-11-27.gh-issue-67407.nhILB2.rst diff --git a/Lib/idlelib/grep.py b/Lib/idlelib/grep.py index 5d604faec23f32d..2a8841ab925151b 100644 --- a/Lib/idlelib/grep.py +++ b/Lib/idlelib/grep.py @@ -78,7 +78,7 @@ def findfiles(folder, pattern, recursive): class GrepDialog(SearchDialogBase): "Dialog for searching multiple files." - title = "Find in Files Dialog" + title = "Find in Files" icon = "Grep" needwrapbutton = 0 @@ -132,9 +132,9 @@ def create_other_buttons(self): btn.pack(side="top", fill="both") def create_command_buttons(self): - "Create base command buttons and add button for Search Files." + "Create base command buttons and add button for Search." SearchDialogBase.create_command_buttons(self) - self.make_button("Search Files", self.default_command, isdef=True) + self.make_button("Search", self.default_command, isdef=True) def default_command(self, event=None): """Grep for search pattern in file path. The default command is bound diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index 0bd0378fbfa07a9..8fa61e0e02a61ee 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -266,8 +266,8 @@ 'file': 'replace', 'kwds': {}, 'msg': "Click the 'Replace' button.\n" - "Test various replace options in the 'Replace dialog'.\n" - "Click [Close] or [X] to close the 'Replace Dialog'." + "Test various replace options in the 'Replace' dialog.\n" + "Click [Close] or [X] to close the 'Replace' dialog." } _scrolled_list_spec = { @@ -283,14 +283,14 @@ 'file': 'search', 'kwds': {}, 'msg': "Click the 'Search' button.\n" - "Test various search options in the 'Search dialog'.\n" - "Click [Close] or [X] to close the 'Search Dialog'." + "Test various search options in the 'Find' dialog.\n" + "Click [Close] or [X] to close the 'Find' dialog." } _searchbase_spec = { 'file': 'searchbase', 'kwds': {}, - 'msg': "Check the appearance of the base search dialog\n" + 'msg': "Check the appearance of the base search dialog.\n" "Its only action is to close." } diff --git a/Lib/idlelib/idle_test/test_searchbase.py b/Lib/idlelib/idle_test/test_searchbase.py index aeb85dc3ebf226e..e0ff2be9907ee18 100644 --- a/Lib/idlelib/idle_test/test_searchbase.py +++ b/Lib/idlelib/idle_test/test_searchbase.py @@ -8,6 +8,9 @@ from tkinter.ttk import Frame from idlelib import searchengine as se from idlelib import searchbase as sdb +from idlelib.grep import GrepDialog +from idlelib.replace import ReplaceDialog +from idlelib.search import SearchDialog from idlelib.idle_test.mock_idle import Func ## from idlelib.idle_test.mock_tk import Var @@ -73,7 +76,6 @@ def test_create_widgets(self): self.dialog.create_entries = Func() self.dialog.create_option_buttons = Func() self.dialog.create_other_buttons = Func() - self.dialog.create_command_buttons = Func() self.dialog.default_command = None self.dialog.create_widgets() @@ -81,7 +83,35 @@ def test_create_widgets(self): self.assertTrue(self.dialog.create_entries.called) self.assertTrue(self.dialog.create_option_buttons.called) self.assertTrue(self.dialog.create_other_buttons.called) - self.assertTrue(self.dialog.create_command_buttons.called) + buttons = self.dialog.buttonframe.winfo_children() + self.assertEqual([button['text'] for button in buttons], ['Close']) + buttons[0].invoke() + self.assertEqual(self.dialog.top.state(), 'withdrawn') + + def test_dialog_buttons(self): + for dialog_class, title, labels in ( + (SearchDialog, 'Find', ['Find Next', 'Close']), + (ReplaceDialog, 'Replace', + ['Find Next', 'Replace', 'Replace All', 'Close']), + (GrepDialog, 'Find in Files', ['Search', 'Close'])): + with self.subTest(dialog=title): + args = (None,) if dialog_class is GrepDialog else () + dialog = dialog_class(self.root, self.engine, *args) + dialog.text = Text(self.root) + self.addCleanup(dialog.text.destroy) + dialog.create_widgets() + self.addCleanup(dialog.top.destroy) + self.assertEqual(dialog.top.title(), title) + buttons = sorted(dialog.buttonframe.winfo_children(), + key=lambda b: int(b.grid_info()['row'])) + self.assertEqual([b['text'] for b in buttons], labels) + self.assertEqual(int(dialog.buttonframe.grid_info()['column']), + 2) + self.assertEqual(int(dialog.ent.grid_info()['columnspan']), 1) + default = buttons[1 if dialog_class is ReplaceDialog else 0] + self.assertEqual(str(default['default']), 'active') + buttons[-1].invoke() + self.assertEqual(dialog.top.state(), 'withdrawn') def test_make_entry(self): equal = self.assertEqual @@ -129,6 +159,8 @@ def test_create_option_buttons(self): var.set(state) frame, options = self.btn_test_setup( self.dialog.create_option_buttons) + label = self.dialog.frame.grid_slaves(row=0, column=0)[0] + self.assertEqual(label['text'], 'Options:') for spec, button in zip (options, frame.pack_slaves()): var, label = spec self.assertEqual(button['text'], label) @@ -158,12 +190,9 @@ def test_make_button(self): def test_create_command_buttons(self): self.dialog.frame = Frame(self.root) self.dialog.create_command_buttons() - # Look for close button command in buttonframe - closebuttoncommand = '' - for child in self.dialog.buttonframe.winfo_children(): - if child['text'] == 'Close': - closebuttoncommand = child['command'] - self.assertIn('close', closebuttoncommand) + grid = self.dialog.buttonframe.grid_info() + self.assertEqual(int(grid['row']), 0) + self.assertEqual(int(grid['column']), 2) if __name__ == '__main__': diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/replace.py index 93d4f1c0c6c573e..cdd894c0292e4cd 100644 --- a/Lib/idlelib/replace.py +++ b/Lib/idlelib/replace.py @@ -32,7 +32,7 @@ def replace(text, insert_tags=None): class ReplaceDialog(SearchDialogBase): "Dialog for finding and replacing a pattern in text." - title = "Replace Dialog" + title = "Replace" icon = "Replace" def __init__(self, root, engine): @@ -74,17 +74,15 @@ def create_entries(self): def create_command_buttons(self): """Create base and additional command buttons. - The additional buttons are for Find, Replace, - Replace+Find, and Replace All. + The additional buttons are for Find Next, Replace, and Replace All. """ SearchDialogBase.create_command_buttons(self) - self.make_button("Find", self.find_it) - self.make_button("Replace", self.replace_it) - self.make_button("Replace+Find", self.default_command, isdef=True) + self.make_button("Find Next", self.find_it) + self.make_button("Replace", self.default_command, isdef=True) self.make_button("Replace All", self.replace_all) def find_it(self, event=None): - "Handle the Find button." + "Handle the Find Next button." self.do_find(False) def replace_it(self, event=None): @@ -96,7 +94,7 @@ def replace_it(self, event=None): self.do_replace() def default_command(self, event=None): - """Handle the Replace+Find button as the default command. + """Handle the Replace button as the default command. First performs a replace and then, if the replace was successful, a find next. diff --git a/Lib/idlelib/search.py b/Lib/idlelib/search.py index 935a4832257fa4a..daed5be0c2385bb 100644 --- a/Lib/idlelib/search.py +++ b/Lib/idlelib/search.py @@ -62,10 +62,9 @@ def find_selection(text): class SearchDialog(SearchDialogBase): "Dialog for finding a pattern in text." - def create_widgets(self): - "Create the base search dialog and add a button for Find Next." - SearchDialogBase.create_widgets(self) - # TODO - why is this here and not in a create_command_buttons? + def create_command_buttons(self): + "Create the command frame and add a button for Find Next." + SearchDialogBase.create_command_buttons(self) self.make_button("Find Next", self.default_command, isdef=True) def default_command(self, event=None): diff --git a/Lib/idlelib/searchbase.py b/Lib/idlelib/searchbase.py index 9bb247a6c178c82..91c86ff76c2a814 100644 --- a/Lib/idlelib/searchbase.py +++ b/Lib/idlelib/searchbase.py @@ -26,7 +26,7 @@ class SearchDialogBase: add widgets. ''' - title = "Search Dialog" # replace in subclasses + title = "Find" # replace in subclasses icon = "Search" needwrapbutton = 1 # not in Find in Files @@ -116,6 +116,7 @@ def create_widgets(self): self.create_option_buttons() # next row, cols 0, 1 self.create_other_buttons() # next row, cols 0, 1 self.create_command_buttons() # col 2, all rows + self.make_button("Close", self.close) def make_entry(self, label_text, var): '''Return (entry, label), . @@ -124,10 +125,10 @@ def make_entry(self, label_text, var): label - Label widget, returned for testing. ''' label = Label(self.frame, text=label_text) - label.grid(row=self.row, column=0, sticky="nw") + label.grid(row=self.row, column=0, sticky="e", pady=4) entry = Entry(self.frame, textvariable=var, exportselection=0) - entry.grid(row=self.row, column=1, sticky="nwe") - self.row = self.row + 1 + entry.grid(row=self.row, column=1, padx=4, pady=4, sticky="nwe") + self.row += 1 return entry, label def create_entries(self): @@ -168,7 +169,7 @@ def create_option_buttons(self): A gridded frame from make_frame is filled with a Checkbutton for each pair, bound to the var, with the corresponding label. ''' - frame = self.make_frame("Options")[0] + frame = self.make_frame("Options:")[0] engine = self.engine options = [(engine.revar, "Regular expression"), (engine.casevar, "Match case"), @@ -207,10 +208,7 @@ def make_button(self, label, command, isdef=0): def create_command_buttons(self): "Place buttons in vertical command frame gridded on right." f = self.buttonframe = Frame(self.frame) - f.grid(row=0,column=2,padx=2,pady=2,ipadx=2,ipady=2) - - b = self.make_button("Close", self.close) - b.lower() + f.grid(row=0, column=2, padx=4, pady=4, sticky="ne") class _searchbase(SearchDialogBase): # htest # diff --git a/Misc/NEWS.d/next/Library/2024-07-29-20-11-27.gh-issue-67407.nhILB2.rst b/Misc/NEWS.d/next/Library/2024-07-29-20-11-27.gh-issue-67407.nhILB2.rst new file mode 100644 index 000000000000000..ef1b44e02c4e6e0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-29-20-11-27.gh-issue-67407.nhILB2.rst @@ -0,0 +1 @@ +Improve IDLE Find, Replace and Find in Files dialogs.