From ed7d279c7af20760b07f24774bb2776963658667 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 26 Jul 2019 15:15:33 +0300 Subject: [PATCH 01/20] refactor TextFrame, extracting just the basic scrollable text widget --- Lib/idlelib/idle_test/test_textview.py | 51 ++++++++++- Lib/idlelib/textview.py | 113 ++++++++++++++++++------- 2 files changed, 131 insertions(+), 33 deletions(-) diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index 6f0c1930518a51..a23fc97b742ede 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -11,7 +11,7 @@ requires('gui') import os -from tkinter import Tk +from tkinter import Tk, CHAR, NONE, WORD from tkinter.ttk import Button from idlelib.idle_test.mock_idle import Func from idlelib.idle_test.mock_tk import Mbox_func @@ -69,13 +69,58 @@ def test_ok(self): view.destroy() -class TextFrameTest(unittest.TestCase): +class ScrollableTextFrameTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.root = root = Tk() root.withdraw() - cls.frame = tv.TextFrame(root, 'test text') + + @classmethod + def tearDownClass(cls): + cls.root.update_idletasks() + cls.root.destroy() + del cls.root + + def setUp(self): + self.frame = tv.ScrollableTextFrame(root) + self.text = self.frame.text + + def tearDown(self): + self.frame.update_idletasks() + self.frame.destroy() + + def test_line1(self): + self.text.insert('1.0', 'test text') + self.assertEqual(self.text.get('1.0', '1.end'), 'test text') + + def test_horiz_scrollbar(self): + # The horizontal scrollbar should be shown/hidden according to + # the 'wrap' setting: It should only be shown when 'wrap' is + # set to NONE. + + # Check initial state. + wrap = self.text.cget('wrap') + self.assertEqual(self.frame.xscroll is not None, wrap == NONE) + + # Check after updating the 'wrap' setting in various ways. + self.text.config(wrap=NONE) + self.assertIsNotNone(self.frame.xscroll) + self.text.configure(wrap=CHAR) + self.assertIsNone(self.frame.xscroll) + self.text['wrap'] = NONE + self.assertIsNotNone(self.frame.xscroll) + self.text['wrap'] = WORD + self.assertIsNone(self.frame.xscroll) + + +class ViewFrameTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.root = root = Tk() + root.withdraw() + cls.frame = tv.ViewFrame(root, 'test text') @classmethod def tearDownClass(cls): diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 4867a80db1abe6..7f50a87fbee4b7 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -2,10 +2,11 @@ """ from tkinter import Toplevel, Text, TclError,\ - HORIZONTAL, VERTICAL, N, S, E, W + HORIZONTAL, VERTICAL, N, S, E, W, NSEW, NONE, WORD, SUNKEN from tkinter.ttk import Frame, Scrollbar, Button from tkinter.messagebox import showerror +from functools import update_wrapper from idlelib.colorizer import color_config @@ -28,52 +29,104 @@ def place(self, **kwargs): raise TclError(f'{self.__class__.__name__} does not support "place"') -class TextFrame(Frame): - "Display text with scrollbar." +def add_config_hook(config_callback): + """Class decorator adding a configuration callback for Tk widgets""" + def decorator(cls): + class WidgetWithConfigHook(cls): + def __setitem__(self, key, value): + config_callback({key: value}) + super().__setitem__(key, value) - def __init__(self, parent, rawtext, wrap='word'): + def configure(self, cnf=None, **kw): + if cnf is not None: + config_callback(cnf) + if kw: + config_callback(kw) + super().configure(cnf=cnf, **kw) + + config = configure + + update_wrapper(WidgetWithConfigHook, cls, updated=[]) + return WidgetWithConfigHook + + return decorator + + +class ScrollableTextFrame(Frame): + """Display text with scrollbar(s).""" + + def __init__(self, master, **kwargs): """Create a frame for Textview. - parent - parent widget for this frame - rawtext - text to display + master - master widget for this frame + + The Text widget is accessible via the 'text' attribute. """ - super().__init__(parent) - self['relief'] = 'sunken' - self['height'] = 700 + super().__init__(master, **kwargs) - self.text = text = Text(self, wrap=wrap, highlightthickness=0) - color_config(text) - text.grid(row=0, column=0, sticky=N+S+E+W) + @add_config_hook(self._config_callback) + class TextWithConfigHook(Text): + pass + self.text = TextWithConfigHook(self) + self.text.grid(row=0, column=0, sticky=NSEW) self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) - text.insert(0.0, rawtext) - text['state'] = 'disabled' - text.focus_set() # vertical scrollbar - self.yscroll = yscroll = AutoHiddenScrollbar(self, orient=VERTICAL, - takefocus=False, - command=text.yview) - text['yscrollcommand'] = yscroll.set - yscroll.grid(row=0, column=1, sticky=N+S) - - if wrap == 'none': - # horizontal scrollbar - self.xscroll = xscroll = AutoHiddenScrollbar(self, orient=HORIZONTAL, - takefocus=False, - command=text.xview) - text['xscrollcommand'] = xscroll.set - xscroll.grid(row=1, column=0, sticky=E+W) + self.yscroll = AutoHiddenScrollbar(self, orient=VERTICAL, + takefocus=False, + command=self.text.yview) + self.text['yscrollcommand'] = self.yscroll.set + self.yscroll.grid(row=0, column=1, sticky=N+S) + + # horizontal scrollbar + self.xscroll = None + self._set_wrapping(self.text.cget('wrap')) + + def _config_callback(self, cnf): + if 'wrap' in cnf: + self._set_wrapping(cnf['wrap']) + + def _set_wrapping(self, wrap): + """show/hide the horizontal scrollbar as per the 'wrap' setting""" + # show the scrollbar only when wrap is set to NONE + if wrap == NONE and self.xscroll is None: + self.xscroll = AutoHiddenScrollbar(self, orient=HORIZONTAL, + takefocus=False, + command=self.text.xview) + self.text['xscrollcommand'] = self.xscroll.set + self.xscroll.grid(row=1, column=0, sticky=E+W) + elif wrap != NONE and self.xscroll is not None: + self.text['xscrollcommand'] = '' + self.xscroll.grid_forget() + self.xscroll.destroy() + self.xscroll = None class ViewFrame(Frame): "Display TextFrame and Close button." def __init__(self, parent, text, wrap='word'): + """Create a frame for viewing text with a "Close" button. + + parent - parent widget for this frame + text - text to display + wrap - type of text wrapping to use ('word', 'char' or 'none') + + The Text widget is accessible via the 'text' attribute. + """ super().__init__(parent) self.parent = parent self.bind('', self.ok) self.bind('', self.ok) - self.textframe = TextFrame(self, text, wrap=wrap) + self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700) + + self.text = self.textframe.text + self.text.configure(wrap=wrap, highlightthickness=0) + self.text.insert('1.0', text) + color_config(self.text) + self.text['state'] = 'disabled' + self.text.focus_set() + self.button_ok = button_ok = Button( self, text='Close', command=self.ok, takefocus=False) self.textframe.pack(side='top', expand=True, fill='both') @@ -87,7 +140,7 @@ def ok(self, event=None): class ViewWindow(Toplevel): "A simple text viewer dialog for IDLE." - def __init__(self, parent, title, text, modal=True, wrap='word', + def __init__(self, parent, title, text, modal=True, wrap=WORD, *, _htest=False, _utest=False): """Show the given text in a scrollable window with a 'close' button. From f67f6b99c1b2c7b641d6b346f375e51234517da1 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 26 Jul 2019 15:16:12 +0300 Subject: [PATCH 02/20] use a scrollable text widget for the font config page's preview --- Lib/idlelib/configdialog.py | 15 ++++++++++----- Lib/idlelib/textview.py | 6 +++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 217f8fd0a5fb35..a3d462a4cbccfa 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -33,6 +33,7 @@ from idlelib.parenmatch import ParenMatch from idlelib.format import FormatParagraph from idlelib.squeezer import Squeezer +from idlelib.textview import ScrollableTextFrame changes = ConfigChanges() # Reload changed options in the following classes. @@ -194,7 +195,7 @@ def cancel(self): def destroy(self): global font_sample_text - font_sample_text = self.fontpage.font_sample.get('1.0', 'end') + font_sample_text = self.fontpage.font_sample.text.get('1.0', 'end') self.grab_release() super().destroy() @@ -556,8 +557,11 @@ def create_page_font_tab(self): frame_font_param, variable=self.font_bold, onvalue=1, offvalue=0, text='Bold') # frame_sample. - self.font_sample = Text(frame_sample, width=20, height=20) - self.font_sample.insert(END, font_sample_text) + # self.font_sample = Text(frame_sample, width=20, height=20) + # self.font_sample.insert(END, font_sample_text) + self.font_sample = ScrollableTextFrame(frame_sample) + self.font_sample.text.config(wrap='word', width=1, height=1) + self.font_sample.text.insert('1.0', font_sample_text) # frame_indent. indent_title = Label( frame_indent, justify=LEFT, @@ -568,8 +572,9 @@ def create_page_font_tab(self): # Grid and pack widgets: self.columnconfigure(1, weight=1) + self.rowconfigure(2, weight=1) frame_font.grid(row=0, column=0, padx=5, pady=5) - frame_sample.grid(row=0, column=1, rowspan=2, padx=5, pady=5, + frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5, sticky='nsew') frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew') # frame_font. @@ -657,7 +662,7 @@ def set_samples(self, event=None): font_name = self.font_name.get() font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL new_font = (font_name, self.font_size.get(), font_weight) - self.font_sample['font'] = new_font + self.font_sample.text['font'] = new_font self.highlight_sample['font'] = new_font def load_tab_cfg(self): diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 7f50a87fbee4b7..90d80886689adc 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -2,7 +2,7 @@ """ from tkinter import Toplevel, Text, TclError,\ - HORIZONTAL, VERTICAL, N, S, E, W, NSEW, NONE, WORD, SUNKEN + HORIZONTAL, VERTICAL, NS, EW, NSEW, NONE, WORD, SUNKEN from tkinter.ttk import Frame, Scrollbar, Button from tkinter.messagebox import showerror @@ -76,8 +76,8 @@ class TextWithConfigHook(Text): self.yscroll = AutoHiddenScrollbar(self, orient=VERTICAL, takefocus=False, command=self.text.yview) + self.yscroll.grid(row=0, column=1, sticky=NS) self.text['yscrollcommand'] = self.yscroll.set - self.yscroll.grid(row=0, column=1, sticky=N+S) # horizontal scrollbar self.xscroll = None @@ -94,8 +94,8 @@ def _set_wrapping(self, wrap): self.xscroll = AutoHiddenScrollbar(self, orient=HORIZONTAL, takefocus=False, command=self.text.xview) + self.xscroll.grid(row=1, column=0, sticky=EW) self.text['xscrollcommand'] = self.xscroll.set - self.xscroll.grid(row=1, column=0, sticky=E+W) elif wrap != NONE and self.xscroll is not None: self.text['xscrollcommand'] = '' self.xscroll.grid_forget() From 0092a9338227412bc4d5d955af36af0e7cb981f8 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 26 Jul 2019 15:38:41 +0300 Subject: [PATCH 03/20] highlight page sample also a fixed-sized, scrollable text widget --- Lib/idlelib/configdialog.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index a3d462a4cbccfa..728a95d5cd940e 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -845,9 +845,11 @@ def create_page_highlight(self): frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, text=' Highlighting Theme ') # frame_custom. - text = self.highlight_sample = Text( - frame_custom, relief=SOLID, borderwidth=1, - font=('courier', 12, ''), cursor='hand2', width=21, height=13, + sample_frame = ScrollableTextFrame( + frame_custom, relief=SOLID, borderwidth=1) + text = self.highlight_sample = sample_frame.text + text.configure( + font=('courier', 12, ''), cursor='hand2', width=1, height=1, takefocus=FALSE, highlightthickness=0, wrap=NONE) text.bind('', lambda e: 'break') text.bind('', lambda e: 'break') @@ -925,9 +927,9 @@ def tem(event, elem=element): frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) frame_theme.pack(side=TOP, padx=5, pady=5, fill=X) # frame_custom. - self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X) + self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X) frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0) - self.highlight_sample.pack( + sample_frame.pack( side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3) From 3380193ae9fd10061f2e5d8ddfb79b976fbae027 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 26 Jul 2019 15:44:45 +0300 Subject: [PATCH 04/20] refactor: make self.font_sample and self.highlight_sample consistent --- Lib/idlelib/configdialog.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 728a95d5cd940e..ed5eedf41c000b 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -195,7 +195,7 @@ def cancel(self): def destroy(self): global font_sample_text - font_sample_text = self.fontpage.font_sample.text.get('1.0', 'end') + font_sample_text = self.fontpage.font_sample.get('1.0', 'end') self.grab_release() super().destroy() @@ -557,11 +557,10 @@ def create_page_font_tab(self): frame_font_param, variable=self.font_bold, onvalue=1, offvalue=0, text='Bold') # frame_sample. - # self.font_sample = Text(frame_sample, width=20, height=20) - # self.font_sample.insert(END, font_sample_text) - self.font_sample = ScrollableTextFrame(frame_sample) - self.font_sample.text.config(wrap='word', width=1, height=1) - self.font_sample.text.insert('1.0', font_sample_text) + font_sample_scrollable_frame = ScrollableTextFrame(frame_sample) + self.font_sample = font_sample_scrollable_frame.text + self.font_sample.config(wrap='word', width=1, height=1) + self.font_sample.insert(END, font_sample_text) # frame_indent. indent_title = Label( frame_indent, justify=LEFT, @@ -587,7 +586,7 @@ def create_page_font_tab(self): self.sizelist.pack(side=LEFT, anchor=W) self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) # frame_sample. - self.font_sample.pack(expand=TRUE, fill=BOTH) + font_sample_scrollable_frame.pack(expand=TRUE, fill=BOTH) # frame_indent. indent_title.pack(side=TOP, anchor=W, padx=5) self.indent_scale.pack(side=TOP, padx=5, fill=X) @@ -662,7 +661,7 @@ def set_samples(self, event=None): font_name = self.font_name.get() font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL new_font = (font_name, self.font_size.get(), font_weight) - self.font_sample.text['font'] = new_font + self.font_sample['font'] = new_font self.highlight_sample['font'] = new_font def load_tab_cfg(self): From b1dc2dd2bee37f9b8da6fb9b460fdc39468a53d3 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 26 Jul 2019 15:48:28 +0300 Subject: [PATCH 05/20] rename for clarity: add_config_hook -> add_config_callback --- Lib/idlelib/textview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 90d80886689adc..d6542a67ad6932 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -29,7 +29,7 @@ def place(self, **kwargs): raise TclError(f'{self.__class__.__name__} does not support "place"') -def add_config_hook(config_callback): +def add_config_callback(config_callback): """Class decorator adding a configuration callback for Tk widgets""" def decorator(cls): class WidgetWithConfigHook(cls): @@ -64,7 +64,7 @@ def __init__(self, master, **kwargs): """ super().__init__(master, **kwargs) - @add_config_hook(self._config_callback) + @add_config_callback(self._config_callback) class TextWithConfigHook(Text): pass self.text = TextWithConfigHook(self) From 48e97ce87b9751352c534491d80a7ba68c271cc1 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 26 Jul 2019 16:52:57 -0400 Subject: [PATCH 06/20] Omit unneeded extra line number. If the normal background is not white, it makes a jagged border between sample background and following white block. --- Lib/idlelib/configdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index ed5eedf41c000b..bbc1707d797bf1 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -874,7 +874,7 @@ def create_page_highlight(self): for texttag in text_and_tags: text.insert(END, texttag[0], texttag[1]) n_lines = len(text.get('1.0', END).splitlines()) - for lineno in range(1, n_lines + 1): + for lineno in range(1, n_lines): text.insert(f'{lineno}.0', f'{lineno:{len(str(n_lines))}d} ', 'linenumber') From f78427a31e47e9c9c92924279555056688399ec4 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 26 Jul 2019 17:05:00 -0400 Subject: [PATCH 07/20] Use horizontal scroller for font sample. --- Lib/idlelib/configdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index bbc1707d797bf1..eb839c519009ad 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -559,7 +559,7 @@ def create_page_font_tab(self): # frame_sample. font_sample_scrollable_frame = ScrollableTextFrame(frame_sample) self.font_sample = font_sample_scrollable_frame.text - self.font_sample.config(wrap='word', width=1, height=1) + self.font_sample.config(wrap=NONE, width=1, height=1) self.font_sample.insert(END, font_sample_text) # frame_indent. indent_title = Label( From 221e0513fef49c1356b1cc06ee6af37711c37bbf Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 26 Jul 2019 17:05:50 -0400 Subject: [PATCH 08/20] Condense 'font_sample_scrollable_frame' to 'font_sample__frame. --- Lib/idlelib/configdialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index eb839c519009ad..113494ae4318b0 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -557,8 +557,8 @@ def create_page_font_tab(self): frame_font_param, variable=self.font_bold, onvalue=1, offvalue=0, text='Bold') # frame_sample. - font_sample_scrollable_frame = ScrollableTextFrame(frame_sample) - self.font_sample = font_sample_scrollable_frame.text + font_sample__frame = ScrollableTextFrame(frame_sample) + self.font_sample = font_sample__frame.text self.font_sample.config(wrap=NONE, width=1, height=1) self.font_sample.insert(END, font_sample_text) # frame_indent. @@ -586,7 +586,7 @@ def create_page_font_tab(self): self.sizelist.pack(side=LEFT, anchor=W) self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) # frame_sample. - font_sample_scrollable_frame.pack(expand=TRUE, fill=BOTH) + font_sample__frame.pack(expand=TRUE, fill=BOTH) # frame_indent. indent_title.pack(side=TOP, anchor=W, padx=5) self.indent_scale.pack(side=TOP, padx=5, fill=X) From 34c218a0952b27da8d8ce88fbaa121de027a58fb Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 26 Jul 2019 17:51:29 -0400 Subject: [PATCH 09/20] Blurb. --- Misc/NEWS.d/next/IDLE/2019-07-26-17-51-13.bpo-37628.kX4AUF.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/IDLE/2019-07-26-17-51-13.bpo-37628.kX4AUF.rst diff --git a/Misc/NEWS.d/next/IDLE/2019-07-26-17-51-13.bpo-37628.kX4AUF.rst b/Misc/NEWS.d/next/IDLE/2019-07-26-17-51-13.bpo-37628.kX4AUF.rst new file mode 100644 index 00000000000000..60910c47e65bb0 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2019-07-26-17-51-13.bpo-37628.kX4AUF.rst @@ -0,0 +1 @@ +Settings dialog no longer expands with font size. From b954c06b16a992ba7b1f2dc4bcbe4f948bda75ef Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 26 Jul 2019 21:25:54 -0400 Subject: [PATCH 10/20] Meaningful names for callback functions. --- Lib/idlelib/textview.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index d6542a67ad6932..971ba5cd22ea06 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -64,7 +64,7 @@ def __init__(self, master, **kwargs): """ super().__init__(master, **kwargs) - @add_config_callback(self._config_callback) + @add_config_callback(self._handle_wrap) class TextWithConfigHook(Text): pass self.text = TextWithConfigHook(self) @@ -81,13 +81,13 @@ class TextWithConfigHook(Text): # horizontal scrollbar self.xscroll = None - self._set_wrapping(self.text.cget('wrap')) + self._set_xscroll(self.text.cget('wrap')) - def _config_callback(self, cnf): + def _handle_wrap(self, cnf): if 'wrap' in cnf: - self._set_wrapping(cnf['wrap']) + self._set_xscroll(cnf['wrap']) - def _set_wrapping(self, wrap): + def _set_xscroll(self, wrap): """show/hide the horizontal scrollbar as per the 'wrap' setting""" # show the scrollbar only when wrap is set to NONE if wrap == NONE and self.xscroll is None: From 72015a92b67acecd05009da02bc0e6298274babb Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 27 Jul 2019 11:25:02 +0300 Subject: [PATCH 11/20] rename 'text' input param to 'contents' and use 'text' as local var --- Lib/idlelib/textview.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 971ba5cd22ea06..91cf01b29670a5 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -105,11 +105,11 @@ def _set_xscroll(self, wrap): class ViewFrame(Frame): "Display TextFrame and Close button." - def __init__(self, parent, text, wrap='word'): + def __init__(self, parent, contents, wrap='word'): """Create a frame for viewing text with a "Close" button. parent - parent widget for this frame - text - text to display + contents - text to display wrap - type of text wrapping to use ('word', 'char' or 'none') The Text widget is accessible via the 'text' attribute. @@ -120,12 +120,12 @@ def __init__(self, parent, text, wrap='word'): self.bind('', self.ok) self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700) - self.text = self.textframe.text - self.text.configure(wrap=wrap, highlightthickness=0) - self.text.insert('1.0', text) - color_config(self.text) - self.text['state'] = 'disabled' - self.text.focus_set() + text = self.text = self.textframe.text + text.configure(wrap=wrap, highlightthickness=0) + text.insert('1.0', contents) + color_config(text) + text['state'] = 'disabled' + text.focus_set() self.button_ok = button_ok = Button( self, text='Close', command=self.ok, takefocus=False) @@ -140,7 +140,7 @@ def ok(self, event=None): class ViewWindow(Toplevel): "A simple text viewer dialog for IDLE." - def __init__(self, parent, title, text, modal=True, wrap=WORD, + def __init__(self, parent, title, contents, modal=True, wrap=WORD, *, _htest=False, _utest=False): """Show the given text in a scrollable window with a 'close' button. @@ -149,7 +149,7 @@ def __init__(self, parent, title, text, modal=True, wrap=WORD, parent - parent of this dialog title - string which is title of popup dialog - text - text to display in dialog + contents - text to display in dialog wrap - type of text wrapping to use ('word', 'char' or 'none') _htest - bool; change box location when running htest. _utest - bool; don't wait_window when running unittest. @@ -162,7 +162,7 @@ def __init__(self, parent, title, text, modal=True, wrap=WORD, self.geometry(f'=750x500+{x}+{y}') self.title(title) - self.viewframe = ViewFrame(self, text, wrap=wrap) + self.viewframe = ViewFrame(self, contents, wrap=wrap) self.protocol("WM_DELETE_WINDOW", self.ok) self.button_ok = button_ok = Button(self, text='Close', command=self.ok, takefocus=False) @@ -182,18 +182,18 @@ def ok(self, event=None): self.destroy() -def view_text(parent, title, text, modal=True, wrap='word', _utest=False): +def view_text(parent, title, contents, modal=True, wrap='word', _utest=False): """Create text viewer for given text. parent - parent of this dialog title - string which is the title of popup dialog - text - text to display in this dialog + contents - text to display in this dialog wrap - type of text wrapping to use ('word', 'char' or 'none') modal - controls if users can interact with other windows while this dialog is displayed _utest - bool; controls wait_window on unittest """ - return ViewWindow(parent, title, text, modal, wrap=wrap, _utest=_utest) + return ViewWindow(parent, title, contents, modal, wrap=wrap, _utest=_utest) def view_file(parent, title, filename, encoding, modal=True, wrap='word', From b5c467d0eaa2de42e1c0298d7f79978346257410 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 27 Jul 2019 11:27:43 +0300 Subject: [PATCH 12/20] fix double-underscore typo --- Lib/idlelib/configdialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 113494ae4318b0..4df6ecee69f1d2 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -557,8 +557,8 @@ def create_page_font_tab(self): frame_font_param, variable=self.font_bold, onvalue=1, offvalue=0, text='Bold') # frame_sample. - font_sample__frame = ScrollableTextFrame(frame_sample) - self.font_sample = font_sample__frame.text + font_sample_frame = ScrollableTextFrame(frame_sample) + self.font_sample = font_sample_frame.text self.font_sample.config(wrap=NONE, width=1, height=1) self.font_sample.insert(END, font_sample_text) # frame_indent. @@ -586,7 +586,7 @@ def create_page_font_tab(self): self.sizelist.pack(side=LEFT, anchor=W) self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) # frame_sample. - font_sample__frame.pack(expand=TRUE, fill=BOTH) + font_sample_frame.pack(expand=TRUE, fill=BOTH) # frame_indent. indent_title.pack(side=TOP, anchor=W, padx=5) self.indent_scale.pack(side=TOP, padx=5, fill=X) From 7100ec949993fa9388cb2452e8737644e6347388 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 27 Jul 2019 11:31:18 +0300 Subject: [PATCH 13/20] re-instate use of 'text' local variable in ScrollableTextFrame code --- Lib/idlelib/textview.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 91cf01b29670a5..3bda6bbe0cbed3 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -67,7 +67,7 @@ def __init__(self, master, **kwargs): @add_config_callback(self._handle_wrap) class TextWithConfigHook(Text): pass - self.text = TextWithConfigHook(self) + text = self.text = TextWithConfigHook(self) self.text.grid(row=0, column=0, sticky=NSEW) self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) @@ -75,9 +75,9 @@ class TextWithConfigHook(Text): # vertical scrollbar self.yscroll = AutoHiddenScrollbar(self, orient=VERTICAL, takefocus=False, - command=self.text.yview) + command=text.yview) self.yscroll.grid(row=0, column=1, sticky=NS) - self.text['yscrollcommand'] = self.yscroll.set + text['yscrollcommand'] = self.yscroll.set # horizontal scrollbar self.xscroll = None @@ -89,15 +89,17 @@ def _handle_wrap(self, cnf): def _set_xscroll(self, wrap): """show/hide the horizontal scrollbar as per the 'wrap' setting""" + text = self.text + # show the scrollbar only when wrap is set to NONE if wrap == NONE and self.xscroll is None: self.xscroll = AutoHiddenScrollbar(self, orient=HORIZONTAL, takefocus=False, - command=self.text.xview) + command=text.xview) self.xscroll.grid(row=1, column=0, sticky=EW) - self.text['xscrollcommand'] = self.xscroll.set + text['xscrollcommand'] = self.xscroll.set elif wrap != NONE and self.xscroll is not None: - self.text['xscrollcommand'] = '' + text['xscrollcommand'] = '' self.xscroll.grid_forget() self.xscroll.destroy() self.xscroll = None From 977c2198720590d7db0944aac0a7205a349fb44c Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 27 Jul 2019 11:46:05 +0300 Subject: [PATCH 14/20] simplify ScrollableTextFrame, removing the config callback --- Lib/idlelib/idle_test/test_textview.py | 43 ++++++++++--------- Lib/idlelib/textview.py | 57 +++++--------------------- 2 files changed, 32 insertions(+), 68 deletions(-) diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index a23fc97b742ede..01bb6f61dab797 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -82,36 +82,35 @@ def tearDownClass(cls): cls.root.destroy() del cls.root - def setUp(self): - self.frame = tv.ScrollableTextFrame(root) - self.text = self.frame.text - - def tearDown(self): - self.frame.update_idletasks() - self.frame.destroy() + def make_frame(self, wrap=NONE, **kwargs): + frame = tv.ScrollableTextFrame(self.root, wrap=wrap, **kwargs) + def cleanup_frame(): + frame.update_idletasks() + frame.destroy() + self.addCleanup(cleanup_frame) + return frame def test_line1(self): - self.text.insert('1.0', 'test text') - self.assertEqual(self.text.get('1.0', '1.end'), 'test text') + frame = self.make_frame() + frame.text.insert('1.0', 'test text') + self.assertEqual(frame.text.get('1.0', '1.end'), 'test text') def test_horiz_scrollbar(self): # The horizontal scrollbar should be shown/hidden according to # the 'wrap' setting: It should only be shown when 'wrap' is # set to NONE. - # Check initial state. - wrap = self.text.cget('wrap') - self.assertEqual(self.frame.xscroll is not None, wrap == NONE) - - # Check after updating the 'wrap' setting in various ways. - self.text.config(wrap=NONE) - self.assertIsNotNone(self.frame.xscroll) - self.text.configure(wrap=CHAR) - self.assertIsNone(self.frame.xscroll) - self.text['wrap'] = NONE - self.assertIsNotNone(self.frame.xscroll) - self.text['wrap'] = WORD - self.assertIsNone(self.frame.xscroll) + # wrap = NONE -> with horizontal scrolling + frame = self.make_frame(wrap=NONE) + self.assertEqual(frame.text.cget('wrap'), NONE) + self.assertIsNotNone(frame.xscroll) + + # wrap != NONE -> no horizontal scrolling + for wrap in [CHAR, WORD]: + with self.subTest(wrap=wrap): + frame = self.make_frame(wrap=wrap) + self.assertEqual(frame.text.cget('wrap'), wrap) + self.assertIsNone(frame.xscroll) class ViewFrameTest(unittest.TestCase): diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 3bda6bbe0cbed3..b88d6ae1ac1128 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -29,45 +29,25 @@ def place(self, **kwargs): raise TclError(f'{self.__class__.__name__} does not support "place"') -def add_config_callback(config_callback): - """Class decorator adding a configuration callback for Tk widgets""" - def decorator(cls): - class WidgetWithConfigHook(cls): - def __setitem__(self, key, value): - config_callback({key: value}) - super().__setitem__(key, value) - - def configure(self, cnf=None, **kw): - if cnf is not None: - config_callback(cnf) - if kw: - config_callback(kw) - super().configure(cnf=cnf, **kw) - - config = configure - - update_wrapper(WidgetWithConfigHook, cls, updated=[]) - return WidgetWithConfigHook - - return decorator - - class ScrollableTextFrame(Frame): """Display text with scrollbar(s).""" - def __init__(self, master, **kwargs): + def __init__(self, master, wrap=NONE, **kwargs): """Create a frame for Textview. master - master widget for this frame + wrap - type of text wrapping to use ('word', 'char' or 'none') + + All parameters except for 'wrap' are passed to Frame.__init__(). The Text widget is accessible via the 'text' attribute. + + Note: Changing the wrapping mode of the text widget after + instantiation is not supported. """ super().__init__(master, **kwargs) - @add_config_callback(self._handle_wrap) - class TextWithConfigHook(Text): - pass - text = self.text = TextWithConfigHook(self) + text = self.text = Text(self, wrap=wrap) self.text.grid(row=0, column=0, sticky=NSEW) self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) @@ -79,29 +59,14 @@ class TextWithConfigHook(Text): self.yscroll.grid(row=0, column=1, sticky=NS) text['yscrollcommand'] = self.yscroll.set - # horizontal scrollbar - self.xscroll = None - self._set_xscroll(self.text.cget('wrap')) - - def _handle_wrap(self, cnf): - if 'wrap' in cnf: - self._set_xscroll(cnf['wrap']) - - def _set_xscroll(self, wrap): - """show/hide the horizontal scrollbar as per the 'wrap' setting""" - text = self.text - - # show the scrollbar only when wrap is set to NONE - if wrap == NONE and self.xscroll is None: + # horizontal scrollbar - only when wrap is set to NONE + if wrap == NONE: self.xscroll = AutoHiddenScrollbar(self, orient=HORIZONTAL, takefocus=False, command=text.xview) self.xscroll.grid(row=1, column=0, sticky=EW) text['xscrollcommand'] = self.xscroll.set - elif wrap != NONE and self.xscroll is not None: - text['xscrollcommand'] = '' - self.xscroll.grid_forget() - self.xscroll.destroy() + else: self.xscroll = None From 3d8729d2e6af7cb2243e83be1d9541985bc74662 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sat, 27 Jul 2019 11:52:39 -0400 Subject: [PATCH 15/20] Rename AutoHiddenScrollbar to AutoHideScrollbar --- Lib/idlelib/textview.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index b88d6ae1ac1128..2bd495f3426b70 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -10,7 +10,7 @@ from idlelib.colorizer import color_config -class AutoHiddenScrollbar(Scrollbar): +class AutoHideScrollbar(Scrollbar): """A scrollbar that is automatically hidden when not needed. Only the grid geometry manager is supported. @@ -48,12 +48,12 @@ def __init__(self, master, wrap=NONE, **kwargs): super().__init__(master, **kwargs) text = self.text = Text(self, wrap=wrap) - self.text.grid(row=0, column=0, sticky=NSEW) + text.grid(row=0, column=0, sticky=NSEW) self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) # vertical scrollbar - self.yscroll = AutoHiddenScrollbar(self, orient=VERTICAL, + self.yscroll = AutoHideScrollbar(self, orient=VERTICAL, takefocus=False, command=text.yview) self.yscroll.grid(row=0, column=1, sticky=NS) @@ -61,7 +61,7 @@ def __init__(self, master, wrap=NONE, **kwargs): # horizontal scrollbar - only when wrap is set to NONE if wrap == NONE: - self.xscroll = AutoHiddenScrollbar(self, orient=HORIZONTAL, + self.xscroll = AutoHideScrollbar(self, orient=HORIZONTAL, takefocus=False, command=text.xview) self.xscroll.grid(row=1, column=0, sticky=EW) From 501fde24a1059055cb7ed6eff356b43ce33d8bc0 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sat, 27 Jul 2019 11:55:13 -0400 Subject: [PATCH 16/20] Fix textview htest. --- Lib/idlelib/idle_test/htest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index f2f37e161632a6..6990af519b1f2a 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -349,7 +349,7 @@ def _wrapper(parent): # htest # ViewWindow_spec = { 'file': 'textview', 'kwds': {'title': 'Test textview', - 'text': 'The quick brown fox jumps over the lazy dog.\n'*35, + 'contents': 'The quick brown fox jumps over the lazy dog.\n'*35, '_htest': True}, 'msg': "Test for read-only property of text.\n" "Select text, scroll window, close" From 52cfa6282bf53553d6a778393ed1007ed8d081eb Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sat, 27 Jul 2019 12:12:20 -0400 Subject: [PATCH 17/20] Restore coverage, reduced in prior issue, to 100%. --- Lib/idlelib/idle_test/test_textview.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index 01bb6f61dab797..8bf14f0d2b3e68 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -6,12 +6,12 @@ information about calls. """ from idlelib import textview as tv -import unittest from test.support import requires requires('gui') import os -from tkinter import Tk, CHAR, NONE, WORD +import unittest +from tkinter import Tk, TclError, CHAR, NONE, WORD from tkinter.ttk import Button from idlelib.idle_test.mock_idle import Func from idlelib.idle_test.mock_tk import Mbox_func @@ -69,6 +69,13 @@ def test_ok(self): view.destroy() +class AutoHideScrollbarTest(unittest.TestCase): + # Method set is tested in ScrollableTextFrameTest + def test_forbidden_geometry(self): + scroll = tv.AutoHideScrollbar(root) + self.assertRaises(TclError, scroll.pack) + self.assertRaises(TclError, scroll.place) + class ScrollableTextFrameTest(unittest.TestCase): @classmethod From 2cbf9e52d682818491e12c64d7ab0667a61f9c66 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sat, 27 Jul 2019 12:13:04 -0400 Subject: [PATCH 18/20] Double space between classes. --- Lib/idlelib/idle_test/test_textview.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index 8bf14f0d2b3e68..7189378ab3dd61 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -76,6 +76,7 @@ def test_forbidden_geometry(self): self.assertRaises(TclError, scroll.pack) self.assertRaises(TclError, scroll.place) + class ScrollableTextFrameTest(unittest.TestCase): @classmethod From 59eba516b5e1fe24742aa35aaca97776581fa4f2 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sat, 27 Jul 2019 12:18:28 -0400 Subject: [PATCH 19/20] Spacing --- Lib/idlelib/textview.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 2bd495f3426b70..70abc6a2c5ca13 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -54,16 +54,16 @@ def __init__(self, master, wrap=NONE, **kwargs): # vertical scrollbar self.yscroll = AutoHideScrollbar(self, orient=VERTICAL, - takefocus=False, - command=text.yview) + takefocus=False, + command=text.yview) self.yscroll.grid(row=0, column=1, sticky=NS) text['yscrollcommand'] = self.yscroll.set # horizontal scrollbar - only when wrap is set to NONE if wrap == NONE: self.xscroll = AutoHideScrollbar(self, orient=HORIZONTAL, - takefocus=False, - command=text.xview) + takefocus=False, + command=text.xview) self.xscroll.grid(row=1, column=0, sticky=EW) text['xscrollcommand'] = self.xscroll.set else: From f6ac725bf6ad1e7b1cad75e6b46120858b8fc01f Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sat, 27 Jul 2019 12:28:42 -0400 Subject: [PATCH 20/20] Combine configuration. --- Lib/idlelib/textview.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index 70abc6a2c5ca13..808a2aefab4f71 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -88,10 +88,9 @@ def __init__(self, parent, contents, wrap='word'): self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700) text = self.text = self.textframe.text - text.configure(wrap=wrap, highlightthickness=0) text.insert('1.0', contents) + text.configure(wrap=wrap, highlightthickness=0, state='disabled') color_config(text) - text['state'] = 'disabled' text.focus_set() self.button_ok = button_ok = Button(