diff --git a/Lib/idlelib/help_about.py b/Lib/idlelib/help_about.py index f0a40e9..6e283ff 100644 --- a/Lib/idlelib/help_about.py +++ b/Lib/idlelib/help_about.py @@ -4,124 +4,28 @@ import os from sys import version -from tkinter import Toplevel, Frame, Label, Button -from tkinter import SUNKEN, TOP, BOTTOM, LEFT, X, BOTH, W, EW, NSEW +from tkinter import Toplevel +from tkinter.ttk import Frame, Button, Label, Separator from idlelib import textview -class AboutDialog(Toplevel): - """Modal about dialog for idle - - """ - def __init__(self, parent, title, _htest=False, _utest=False): - """Create popup, do not return until tk widget destroyed. +class PyButtons(Frame): + "Display frame for buttons for Python about." - parent - parent of this dialog - title - string which is title of popup dialog - _htest - bool, change box location when running htest - _utest - bool, don't wait_window when running unittest - """ - Toplevel.__init__(self, parent) - self.configure(borderwidth=5) - # place dialog below parent if running htest - self.geometry("+%d+%d" % ( - parent.winfo_rootx()+30, - parent.winfo_rooty()+(30 if not _htest else 100))) - self.bg = "#707070" - self.fg = "#ffffff" - self.create_widgets() - self.resizable(height=False, width=False) - self.title(title) - self.transient(parent) - self.grab_set() - self.protocol("WM_DELETE_WINDOW", self.ok) - self.parent = parent - self.button_ok.focus_set() - self.bind('', self.ok) # dismiss dialog - self.bind('', self.ok) # dismiss dialog - self._current_textview = None + def __init__(self, parent, _utest=False): + super().__init__(parent) self._utest = _utest - - if not _utest: - self.deiconify() - self.wait_window() - - def create_widgets(self): - release = version[:version.index(' ')] - frame = Frame(self, borderwidth=2, relief=SUNKEN) - frame_buttons = Frame(self) - frame_buttons.pack(side=BOTTOM, fill=X) - frame.pack(side=TOP, expand=True, fill=BOTH) - self.button_ok = Button(frame_buttons, text='Close', - command=self.ok) - self.button_ok.pack(padx=5, pady=5) - - frame_background = Frame(frame, bg=self.bg) - frame_background.pack(expand=True, fill=BOTH) - - header = Label(frame_background, text='IDLE', fg=self.fg, - bg=self.bg, font=('courier', 24, 'bold')) - header.grid(row=0, column=0, sticky=W, padx=10, pady=10) - byline_text = "Python's Integrated DeveLopment Environment" + 5*'\n' - byline = Label(frame_background, text=byline_text, justify=LEFT, - fg=self.fg, bg=self.bg) - byline.grid(row=2, column=0, sticky=W, columnspan=3, padx=10, pady=5) - email = Label(frame_background, text='email: idle-dev@python.org', - justify=LEFT, fg=self.fg, bg=self.bg) - email.grid(row=6, column=0, columnspan=2, sticky=W, padx=10, pady=0) - docs = Label(frame_background, text='https://docs.python.org/' + - version[:3] + '/library/idle.html', - justify=LEFT, fg=self.fg, bg=self.bg) - docs.grid(row=7, column=0, columnspan=2, sticky=W, padx=10, pady=0) - - Frame(frame_background, borderwidth=1, relief=SUNKEN, - height=2, bg=self.bg).grid(row=8, column=0, sticky=EW, - columnspan=3, padx=5, pady=5) - - pyver = Label(frame_background, text='Python version: ' + release, - fg=self.fg, bg=self.bg) - pyver.grid(row=9, column=0, sticky=W, padx=10, pady=0) - tk_patchlevel = self.tk.call('info', 'patchlevel') - tkver = Label(frame_background, text='Tk version: ' + tk_patchlevel, - fg=self.fg, bg=self.bg) - tkver.grid(row=9, column=1, sticky=W, padx=2, pady=0) - py_buttons = Frame(frame_background, bg=self.bg) - py_buttons.grid(row=10, column=0, columnspan=2, sticky=NSEW) - self.py_license = Button(py_buttons, text='License', width=8, - highlightbackground=self.bg, + self.py_license = Button(self, text='License', width=8, command=self.show_py_license) - self.py_license.pack(side=LEFT, padx=10, pady=10) - self.py_copyright = Button(py_buttons, text='Copyright', width=8, - highlightbackground=self.bg, + self.py_copyright = Button(self, text='Copyright', width=8, command=self.show_py_copyright) - self.py_copyright.pack(side=LEFT, padx=10, pady=10) - self.py_credits = Button(py_buttons, text='Credits', width=8, - highlightbackground=self.bg, + self.py_credits = Button(self, text='Credits', width=8, command=self.show_py_credits) - self.py_credits.pack(side=LEFT, padx=10, pady=10) - - Frame(frame_background, borderwidth=1, relief=SUNKEN, - height=2, bg=self.bg).grid(row=11, column=0, sticky=EW, - columnspan=3, padx=5, pady=5) - - idlever = Label(frame_background, text='IDLE version: ' + release, - fg=self.fg, bg=self.bg) - idlever.grid(row=12, column=0, sticky=W, padx=10, pady=0) - idle_buttons = Frame(frame_background, bg=self.bg) - idle_buttons.grid(row=13, column=0, columnspan=3, sticky=NSEW) - self.readme = Button(idle_buttons, text='README', width=8, - highlightbackground=self.bg, - command=self.show_readme) - self.readme.pack(side=LEFT, padx=10, pady=10) - self.idle_news = Button(idle_buttons, text='NEWS', width=8, - highlightbackground=self.bg, - command=self.show_idle_news) - self.idle_news.pack(side=LEFT, padx=10, pady=10) - self.idle_credits = Button(idle_buttons, text='Credits', width=8, - highlightbackground=self.bg, - command=self.show_idle_credits) - self.idle_credits.pack(side=LEFT, padx=10, pady=10) + + self.py_license.pack(side='left', padx=10, pady=10) + self.py_copyright.pack(side='left', padx=10, pady=10) + self.py_credits.pack(side='left', padx=10, pady=10) # License, copyright, and credits are of type _sitebuiltins._Printer def show_py_license(self): @@ -136,6 +40,37 @@ class AboutDialog(Toplevel): "Handle Python Credits button event." self.display_printer_text('About - Python Credits', credits) + def display_printer_text(self, title, printer): + """Create textview for built-in constants. + + Built-in constants have type _sitebuiltins._Printer. The + text is extracted from the built-in and then sent to a text + viewer with self as the parent and title as the title of + the popup. + """ + printer._Printer__setup() + text = '\n'.join(printer._Printer__lines) + self._current_textview = textview.view_text( + self, title, text, _utest=self._utest) + + +class IdleButtons(Frame): + "Display frame for buttons for IDLE about." + + def __init__(self, parent, _utest=False): + super().__init__(parent) + self._utest = _utest + self.readme = Button(self, text='README', width=8, + command=self.show_readme) + self.idle_news = Button(self, text='NEWS', width=8, + command=self.show_idle_news) + self.idle_credits = Button(self, text='Credits', width=8, + command=self.show_idle_credits) + + self.readme.pack(side='left', padx=10, pady=10) + self.idle_news.pack(side='left', padx=10, pady=10) + self.idle_credits.pack(side='left', padx=10, pady=10) + # Encode CREDITS.txt to utf-8 for proper version of Loewis. # Specify others as ascii until need utf-8, so catch errors. def show_idle_credits(self): @@ -150,19 +85,6 @@ class AboutDialog(Toplevel): "Handle News button event." self.display_file_text('About - NEWS', 'NEWS.txt', 'utf-8') - def display_printer_text(self, title, printer): - """Create textview for built-in constants. - - Built-in constants have type _sitebuiltins._Printer. The - text is extracted from the built-in and then sent to a text - viewer with self as the parent and title as the title of - the popup. - """ - printer._Printer__setup() - text = '\n'.join(printer._Printer__lines) - self._current_textview = textview.view_text( - self, title, text, _utest=self._utest) - def display_file_text(self, title, filename, encoding=None): """Create textview for filename. @@ -174,6 +96,106 @@ class AboutDialog(Toplevel): self._current_textview = textview.view_file( self, title, fn, encoding, _utest=self._utest) + +class AboutFrame(Frame): + "Display frame for about information." + + def __init__(self, parent, _utest=False): + super().__init__(parent) + self.release = version[:version.index(' ')] + self.tk_patchlevel = self.tk.call('info', 'patchlevel') + self._utest = _utest + self['borderwidth'] = 2 + self['relief'] = 'sunken' + + self.create_top_widgets() + self.create_py_widgets() + self.create_idle_widgets() + self.place_widgets() + + def create_top_widgets(self): + self.header = Label(self, text='IDLE', font=('courier', 24, 'bold')) + byline_text = "Python's Integrated DeveLopment Environment" + 5*'\n' + self.byline = Label(self, text=byline_text, justify='left') + self.email = Label(self, text='email: idle-dev@python.org', + justify='left') + self.docs = Label(self, text='https://docs.python.org/' + + version[:3] + '/library/idle.html', justify='left') + + def create_py_widgets(self): + self.pyver = Label(self, text=f'Python version: {self.release}') + self.tkver = Label(self, text=f'Tk version: {self.tk_patchlevel}') + self.py_buttons = PyButtons(self, self._utest) + + def create_idle_widgets(self): + self.idlever = Label(self, text=f'IDLE version: {self.release}') + self.idle_buttons = IdleButtons(self, self._utest) + + def place_widgets(self): + sep1 = sep2 = Separator(self, orient='horizontal') + self.header.grid(row=0, column=0, sticky='w', padx=10, pady=10) + self.byline.grid(row=2, column=0, sticky='w', columnspan=3, + padx=10, pady=5) + self.email.grid(row=6, column=0, columnspan=2, sticky='w', + padx=10, pady=0) + self.docs.grid(row=7, column=0, columnspan=2, sticky='w', + padx=10, pady=0) + sep1.grid(row=8, column=0, columnspan=3, sticky='ew', padx=5, pady=5) + self.pyver.grid(row=9, column=0, sticky='w', padx=10, pady=0) + self.tkver.grid(row=9, column=1, sticky='w', padx=2, pady=0) + self.py_buttons.grid(row=10, column=0, columnspan=2, sticky='nsew') + sep2.grid(row=11, column=0, columnspan=3, sticky='ew', padx=5, pady=5) + self.idlever.grid(row=12, column=0, sticky='w', padx=10, pady=0) + self.idle_buttons.grid(row=13, column=0, columnspan=3, sticky='nsew') + + +class AboutDialog(Toplevel): + "Modal about dialog for idle." + + def __init__(self, parent, title, _htest=False, _utest=False): + """Create popup, do not return until tk widget destroyed. + + parent - parent of this dialog + title - string which is title of popup dialog + _htest - bool, change box location when running htest + _utest - bool, don't wait_window when running unittest + """ + super().__init__(parent) + self['borderwidth'] = 5 + # place dialog below parent if running htest + x = parent.winfo_rootx() + y = parent.winfo_rooty() + (30 if not _htest else 100) + self.geometry(f'+{x}+{y}') + + self.bg = "#707070" + self.fg = "#ffffff" + + self.resizable(height=False, width=False) + self.title(title) + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.ok) + self.bind('', self.ok) + self.bind('', self.ok) + + self.aboutframe = aboutframe = AboutFrame(self, _utest) + self.buttonframe = buttonframe = Frame(self) + + self.button_ok = button_ok = Button(buttonframe, text='Close', + command=self.ok) + button_ok.focus_set() + button_ok.pack(padx=5, pady=5) + + aboutframe.pack(side='top', expand=True, fill='both') + buttonframe.pack(side='bottom', fill='x') + + self._current_textview = None + self._utest = _utest + + if not _utest: + self.deiconify() + self.wait_window() + def ok(self, event=None): "Dismiss help_about dialog." self.destroy() diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index 7a2f7e4..09af75f 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -1,6 +1,6 @@ '''Test idlelib.textview. -Since all methods and functions create (or destroy) a TextViewer, which +Since all methods and functions create (or destroy) a TextviewWindow, which is a widget containing multiple widgets, all tests must be gui tests. Using mock Text would not change this. Other mocks are used to retrieve information about calls. @@ -13,7 +13,8 @@ requires('gui') import unittest import os -from tkinter import Tk, Button +from tkinter import Tk +from tkinter.ttk import Button from idlelib.idle_test.mock_idle import Func from idlelib.idle_test.mock_tk import Mbox_func @@ -28,12 +29,12 @@ def tearDownModule(): root.destroy() # Pyflakes falsely sees root as undefined. del root -# If we call TextViewer or wrapper functions with defaults +# If we call TextviewWindow or wrapper functions with defaults # modal=True, _utest=False, test hangs on call to wait_window. # Have also gotten tk error 'can't invoke "event" command'. -class TV(tv.TextViewer): # Used in TextViewTest. +class TV(tv.TextviewWindow): # Used in TextViewTest. transient = Func() grab_set = Func() wait_window = Func() @@ -70,7 +71,28 @@ class TextViewTest(unittest.TestCase): view.destroy() -# Call TextViewer with modal=False. +class TextviewFrameTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + "By itself, this tests that file parsed without exception." + cls.root = root = Tk() + root.withdraw() + cls.frame = tv.TextviewFrame(root, 'test text') + + @classmethod + def tearDownClass(cls): + del cls.frame + cls.root.update_idletasks() + cls.root.destroy() + del cls.root + + def test_line1(self): + text = self.frame.text + self.assertEqual(text.get('1.0', '1.end'), 'test text') + + +# Call TextviewWindow with modal=False. class ViewFunctionTest(unittest.TestCase): @classmethod @@ -85,13 +107,15 @@ class ViewFunctionTest(unittest.TestCase): def test_view_text(self): view = tv.view_text(root, 'Title', 'test text', modal=False) - self.assertIsInstance(view, tv.TextViewer) + self.assertIsInstance(view, tv.TextviewWindow) + self.assertIsInstance(view.textframe, tv.TextviewFrame) view.ok() def test_view_file(self): view = tv.view_file(root, 'Title', __file__, modal=False) - self.assertIsInstance(view, tv.TextViewer) - self.assertIn('Test', view.text.get('1.0', '1.end')) + self.assertIsInstance(view, tv.TextviewWindow) + self.assertIsInstance(view.textframe, tv.TextviewFrame) + self.assertIn('Test', view.textframe.text.get('1.0', '1.end')) view.ok() def test_bad_file(self): @@ -109,8 +133,7 @@ class ViewFunctionTest(unittest.TestCase): self.assertEqual(tv.showerror.title, 'Unicode Decode Error') - -# Call TextViewer with _utest=True. +# Call TextviewWindow with _utest=True. class ButtonClickTest(unittest.TestCase): def setUp(self): @@ -131,7 +154,7 @@ class ButtonClickTest(unittest.TestCase): self.assertEqual(self.called, True) self.assertEqual(self.view.title(), 'TITLE_TEXT') - self.assertEqual(self.view.text.get('1.0', '1.end'), 'COMMAND') + self.assertEqual(self.view.textframe.text.get('1.0', '1.end'), 'COMMAND') def test_view_file_bind_with_button(self): def _command(): @@ -144,10 +167,10 @@ class ButtonClickTest(unittest.TestCase): self.assertEqual(self.called, True) self.assertEqual(self.view.title(), 'TITLE_FILE') with open(__file__) as f: - self.assertEqual(self.view.text.get('1.0', '1.end'), + self.assertEqual(self.view.textframe.text.get('1.0', '1.end'), f.readline().strip()) f.readline() - self.assertEqual(self.view.text.get('3.0', '3.end'), + self.assertEqual(self.view.textframe.text.get('3.0', '3.end'), f.readline().strip()) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index ab653a9..fce2580 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -1,14 +1,42 @@ """Simple text browser for IDLE """ -from tkinter import Toplevel, Frame, Button, Text -from tkinter import DISABLED, SUNKEN, VERTICAL, WORD -from tkinter import RIGHT, LEFT, TOP, BOTTOM, BOTH, X, Y -from tkinter.ttk import Scrollbar +from tkinter import Toplevel, Text +from tkinter.ttk import Frame, Scrollbar, Button from tkinter.messagebox import showerror -class TextViewer(Toplevel): +class TextviewFrame(Frame): + "Display frame for text and scroll." + + def __init__(self, parent, content): + """Create a frame for Textview. + + parent - parent widget for this frame + content - text to display + """ + super().__init__(parent) + self['relief'] = 'sunken' + self['height'] = 700 + # TODO: get fg/bg from theme. + self.bg = '#ffffff' + self.fg = '#000000' + + self.text = text = Text(self, wrap='word', highlightthickness=0, + fg=self.fg, bg=self.bg) + self.scroll = scroll = Scrollbar(self, orient='vertical', + takefocus=False, command=text.yview) + text['yscrollcommand'] = scroll.set + text.insert(0.0, content) + text['state'] = 'disabled' + text.focus_set() + + + scroll.pack(side='right', fill='y') + text.pack(side='left', expand=True, fill='both') + + +class TextviewWindow(Toplevel): "A simple text viewer dialog for IDLE." def __init__(self, parent, title, text, modal=True, @@ -24,26 +52,26 @@ class TextViewer(Toplevel): _htest - bool; change box location when running htest. _utest - bool; don't wait_window when running unittest. """ - Toplevel.__init__(self, parent) - self.configure(borderwidth=5) + super().__init__(parent) + self['borderwidth'] = 5 # Place dialog below parent if running htest. - self.geometry("=%dx%d+%d+%d" % (750, 500, - parent.winfo_rootx() + 10, - parent.winfo_rooty() + (10 if not _htest else 100))) - # TODO: get fg/bg from theme. - self.bg = '#ffffff' - self.fg = '#000000' + x = parent.winfo_rootx() + 10 + y = parent.winfo_rooty() + (10 if not _htest else 100) + self.geometry(f'=750x500+{x}+{y}') - self.create_widgets() self.title(title) self.protocol("WM_DELETE_WINDOW", self.ok) - self.parent = parent - self.text.focus_set() - # Bind keys for closing this dialog. self.bind('', self.ok) self.bind('', self.ok) - self.text.insert(0.0, text) - self.text.config(state=DISABLED) + self.textframe = textframe = TextviewFrame(self, text) + self.buttonframe = buttonframe = Frame(self) + + self.button_ok = button_ok = Button(buttonframe, text='Close', + command=self.ok, takefocus=False) + button_ok.pack() + + textframe.pack(side='top', expand=True, fill='both') + buttonframe.pack(side='bottom', fill='x') if modal: self.transient(parent) @@ -51,31 +79,13 @@ class TextViewer(Toplevel): if not _utest: self.wait_window() - def create_widgets(self): - "Create Frame with Text (with vertical Scrollbar) and Button." - frame = Frame(self, relief=SUNKEN, height=700) - frame_buttons = Frame(self) - self.button_ok = Button(frame_buttons, text='Close', - command=self.ok, takefocus=False) - self.scrollbar = Scrollbar(frame, orient=VERTICAL, takefocus=False) - self.text = Text(frame, wrap=WORD, highlightthickness=0, - fg=self.fg, bg=self.bg) - self.scrollbar.config(command=self.text.yview) - self.text.config(yscrollcommand=self.scrollbar.set) - - self.button_ok.pack() - self.scrollbar.pack(side=RIGHT, fill=Y) - self.text.pack(side=LEFT, expand=True, fill=BOTH) - frame_buttons.pack(side=BOTTOM, fill=X) - frame.pack(side=TOP, expand=True, fill=BOTH) - def ok(self, event=None): """Dismiss text viewer dialog.""" self.destroy() def view_text(parent, title, text, modal=True, _utest=False): - """Create TextViewer for given text. + """Create text viewer for given text. parent - parent of this dialog title - string which is the title of popup dialog @@ -84,11 +94,11 @@ def view_text(parent, title, text, modal=True, _utest=False): dialog is displayed _utest - bool; controls wait_window on unittest """ - return TextViewer(parent, title, text, modal, _utest=_utest) + return TextviewWindow(parent, title, text, modal, _utest=_utest) def view_file(parent, title, filename, encoding=None, modal=True, _utest=False): - """Create TextViewer for text in filename. + """Create text viewer for text in filename. Return error message if file cannot be read. Otherwise calls view_text with contents of the file. @@ -113,4 +123,4 @@ if __name__ == '__main__': import unittest unittest.main('idlelib.idle_test.test_textview', verbosity=2, exit=False) from idlelib.idle_test.htest import run - run(TextViewer) + run(TextviewWindow)