jpayne@69: """Simple text browser for IDLE jpayne@69: jpayne@69: """ jpayne@69: from tkinter import Toplevel, Text, TclError,\ jpayne@69: HORIZONTAL, VERTICAL, NS, EW, NSEW, NONE, WORD, SUNKEN jpayne@69: from tkinter.ttk import Frame, Scrollbar, Button jpayne@69: from tkinter.messagebox import showerror jpayne@69: jpayne@69: from functools import update_wrapper jpayne@69: from idlelib.colorizer import color_config jpayne@69: jpayne@69: jpayne@69: class AutoHideScrollbar(Scrollbar): jpayne@69: """A scrollbar that is automatically hidden when not needed. jpayne@69: jpayne@69: Only the grid geometry manager is supported. jpayne@69: """ jpayne@69: def set(self, lo, hi): jpayne@69: if float(lo) > 0.0 or float(hi) < 1.0: jpayne@69: self.grid() jpayne@69: else: jpayne@69: self.grid_remove() jpayne@69: super().set(lo, hi) jpayne@69: jpayne@69: def pack(self, **kwargs): jpayne@69: raise TclError(f'{self.__class__.__name__} does not support "pack"') jpayne@69: jpayne@69: def place(self, **kwargs): jpayne@69: raise TclError(f'{self.__class__.__name__} does not support "place"') jpayne@69: jpayne@69: jpayne@69: class ScrollableTextFrame(Frame): jpayne@69: """Display text with scrollbar(s).""" jpayne@69: jpayne@69: def __init__(self, master, wrap=NONE, **kwargs): jpayne@69: """Create a frame for Textview. jpayne@69: jpayne@69: master - master widget for this frame jpayne@69: wrap - type of text wrapping to use ('word', 'char' or 'none') jpayne@69: jpayne@69: All parameters except for 'wrap' are passed to Frame.__init__(). jpayne@69: jpayne@69: The Text widget is accessible via the 'text' attribute. jpayne@69: jpayne@69: Note: Changing the wrapping mode of the text widget after jpayne@69: instantiation is not supported. jpayne@69: """ jpayne@69: super().__init__(master, **kwargs) jpayne@69: jpayne@69: text = self.text = Text(self, wrap=wrap) jpayne@69: text.grid(row=0, column=0, sticky=NSEW) jpayne@69: self.grid_rowconfigure(0, weight=1) jpayne@69: self.grid_columnconfigure(0, weight=1) jpayne@69: jpayne@69: # vertical scrollbar jpayne@69: self.yscroll = AutoHideScrollbar(self, orient=VERTICAL, jpayne@69: takefocus=False, jpayne@69: command=text.yview) jpayne@69: self.yscroll.grid(row=0, column=1, sticky=NS) jpayne@69: text['yscrollcommand'] = self.yscroll.set jpayne@69: jpayne@69: # horizontal scrollbar - only when wrap is set to NONE jpayne@69: if wrap == NONE: jpayne@69: self.xscroll = AutoHideScrollbar(self, orient=HORIZONTAL, jpayne@69: takefocus=False, jpayne@69: command=text.xview) jpayne@69: self.xscroll.grid(row=1, column=0, sticky=EW) jpayne@69: text['xscrollcommand'] = self.xscroll.set jpayne@69: else: jpayne@69: self.xscroll = None jpayne@69: jpayne@69: jpayne@69: class ViewFrame(Frame): jpayne@69: "Display TextFrame and Close button." jpayne@69: def __init__(self, parent, contents, wrap='word'): jpayne@69: """Create a frame for viewing text with a "Close" button. jpayne@69: jpayne@69: parent - parent widget for this frame jpayne@69: contents - text to display jpayne@69: wrap - type of text wrapping to use ('word', 'char' or 'none') jpayne@69: jpayne@69: The Text widget is accessible via the 'text' attribute. jpayne@69: """ jpayne@69: super().__init__(parent) jpayne@69: self.parent = parent jpayne@69: self.bind('', self.ok) jpayne@69: self.bind('', self.ok) jpayne@69: self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700) jpayne@69: jpayne@69: text = self.text = self.textframe.text jpayne@69: text.insert('1.0', contents) jpayne@69: text.configure(wrap=wrap, highlightthickness=0, state='disabled') jpayne@69: color_config(text) jpayne@69: text.focus_set() jpayne@69: jpayne@69: self.button_ok = button_ok = Button( jpayne@69: self, text='Close', command=self.ok, takefocus=False) jpayne@69: self.textframe.pack(side='top', expand=True, fill='both') jpayne@69: button_ok.pack(side='bottom') jpayne@69: jpayne@69: def ok(self, event=None): jpayne@69: """Dismiss text viewer dialog.""" jpayne@69: self.parent.destroy() jpayne@69: jpayne@69: jpayne@69: class ViewWindow(Toplevel): jpayne@69: "A simple text viewer dialog for IDLE." jpayne@69: jpayne@69: def __init__(self, parent, title, contents, modal=True, wrap=WORD, jpayne@69: *, _htest=False, _utest=False): jpayne@69: """Show the given text in a scrollable window with a 'close' button. jpayne@69: jpayne@69: If modal is left True, users cannot interact with other windows jpayne@69: until the textview window is closed. jpayne@69: jpayne@69: parent - parent of this dialog jpayne@69: title - string which is title of popup dialog jpayne@69: contents - text to display in dialog jpayne@69: wrap - type of text wrapping to use ('word', 'char' or 'none') jpayne@69: _htest - bool; change box location when running htest. jpayne@69: _utest - bool; don't wait_window when running unittest. jpayne@69: """ jpayne@69: super().__init__(parent) jpayne@69: self['borderwidth'] = 5 jpayne@69: # Place dialog below parent if running htest. jpayne@69: x = parent.winfo_rootx() + 10 jpayne@69: y = parent.winfo_rooty() + (10 if not _htest else 100) jpayne@69: self.geometry(f'=750x500+{x}+{y}') jpayne@69: jpayne@69: self.title(title) jpayne@69: self.viewframe = ViewFrame(self, contents, wrap=wrap) jpayne@69: self.protocol("WM_DELETE_WINDOW", self.ok) jpayne@69: self.button_ok = button_ok = Button(self, text='Close', jpayne@69: command=self.ok, takefocus=False) jpayne@69: self.viewframe.pack(side='top', expand=True, fill='both') jpayne@69: jpayne@69: self.is_modal = modal jpayne@69: if self.is_modal: jpayne@69: self.transient(parent) jpayne@69: self.grab_set() jpayne@69: if not _utest: jpayne@69: self.wait_window() jpayne@69: jpayne@69: def ok(self, event=None): jpayne@69: """Dismiss text viewer dialog.""" jpayne@69: if self.is_modal: jpayne@69: self.grab_release() jpayne@69: self.destroy() jpayne@69: jpayne@69: jpayne@69: def view_text(parent, title, contents, modal=True, wrap='word', _utest=False): jpayne@69: """Create text viewer for given text. jpayne@69: jpayne@69: parent - parent of this dialog jpayne@69: title - string which is the title of popup dialog jpayne@69: contents - text to display in this dialog jpayne@69: wrap - type of text wrapping to use ('word', 'char' or 'none') jpayne@69: modal - controls if users can interact with other windows while this jpayne@69: dialog is displayed jpayne@69: _utest - bool; controls wait_window on unittest jpayne@69: """ jpayne@69: return ViewWindow(parent, title, contents, modal, wrap=wrap, _utest=_utest) jpayne@69: jpayne@69: jpayne@69: def view_file(parent, title, filename, encoding, modal=True, wrap='word', jpayne@69: _utest=False): jpayne@69: """Create text viewer for text in filename. jpayne@69: jpayne@69: Return error message if file cannot be read. Otherwise calls view_text jpayne@69: with contents of the file. jpayne@69: """ jpayne@69: try: jpayne@69: with open(filename, 'r', encoding=encoding) as file: jpayne@69: contents = file.read() jpayne@69: except OSError: jpayne@69: showerror(title='File Load Error', jpayne@69: message=f'Unable to load file {filename!r} .', jpayne@69: parent=parent) jpayne@69: except UnicodeDecodeError as err: jpayne@69: showerror(title='Unicode Decode Error', jpayne@69: message=str(err), jpayne@69: parent=parent) jpayne@69: else: jpayne@69: return view_text(parent, title, contents, modal, wrap=wrap, jpayne@69: _utest=_utest) jpayne@69: return None jpayne@69: jpayne@69: jpayne@69: if __name__ == '__main__': jpayne@69: from unittest import main jpayne@69: main('idlelib.idle_test.test_textview', verbosity=2, exit=False) jpayne@69: jpayne@69: from idlelib.idle_test.htest import run jpayne@69: run(ViewWindow)