In the last few decades, I’ve typed text into a lot of programs. Most were closed systems (I had to live with whatever decisions their designers made), and many ran on only one platform. The PyEdit program presented in this section does better on both counts: it implements a full-featured, graphical text editor program in roughly 600 lines of portable Python code (including whitespace, comments, and configuration settings). Despite its size, PyEdit was sufficiently powerful and robust to serve as the primary tool used to code most of the examples in this book.
PyEdit supports all the usual mouse and keyboard text-editing operations: cut and paste, search and replace, open and save, undo and redo, and so on. But really, PyEdit is a bit more than just another text editor—it is designed to be used as both a program and a library component, and it can be run in a variety of roles:
As a standalone text-editor program, with or without the name of a file to be edited passed in on the command line. In this mode, PyEdit is roughly like other text-editing utility programs (e.g., Notepad on Windows), but it also provides advanced functions such as running Python program code being edited, changing fonts and colors, and so on. More important, because it is coded in Python, PyEdit is easy to customize, and it runs portably on Windows, X Windows, and Macintosh.
Within a new pop-up window, allowing an arbitrary number of copies to appear as pop ups at once in a program. Because state information is stored in class instance attributes, each PyEdit object created operates independently. In this mode and the next, PyEdit serves as a library object for use in other scripts, not as a canned application.
As an attached component, to provide a text-editing widget for other GUIs. When attached, PyEdit uses a frame-based menu and can optionally disable some of its menu options for an embedded role. For instance, PyView (later in this chapter) uses PyEdit in embedded mode this way to serve as a note editor for photos, and PyMailGUI (in Chapter 15) attaches it to get an email text editor for free.
While such mixed-mode behavior may sound complicated to implement, most of PyEdit’s modes are a natural byproduct of coding GUIs with the class-based techniques we’ve seen in the last three chapters.
PyEdit sports lots of features, and the best way to learn how it works is to test-drive it for yourself—you can run it by starting the file textEditor.pyw, or from the PyDemos and PyGadgets launcher bars described at the end of Chapter 10 (the launchers themselves live in the top level of the book examples directory tree). To give you a sampling of PyEdit’s interfaces, Figure 12-1 shows the main window’s default appearance, after opening PyEdit’s source code file.
The main part of this window is a Text
widget object, and if you read Chapter 10’s coverage of this
widget, PyEdit text-editing operations will be familiar. It uses
text marks, tags, and indexes, and it implements cut-and-paste
operations with the system clipboard so that PyEdit can paste data
to and from other applications. Both vertical and horizontal scroll
bars are cross-linked to the Text
widget, to support movement through arbitrary files.
If PyEdit’s menu and toolbars look familiar, they
should—it builds the main window with minimal code and appropriate
clipping and expansion policies, by mixing in the GuiMaker
class we coded in the prior
chapter. The toolbar at the bottom contains shortcut buttons for
operations I tend to use most often; if my preferences don’t match
yours, simply change the toolbar list in the source code to show
the buttons you want (this is Python, after all).
As usual for Tkinter menus, shortcut key combinations can be used to invoke menu options quickly too—press Alt plus all the underlined keys of entries along the path to the desired action. Menus can also be torn off at their dashed line to provide quick access to menu options in new top-level windows (handy for options without toolbar buttons).
PyEdit pops up a variety of modal and nonmodal dialogs, both standard and custom. Figure 12-2 shows the custom and nonmodal change dialog, along with a standard dialog used to display file statistics.
The main window in Figure 12-2 has been given new foreground and background colors (with the standard color selection dialog), and a new text font has been selected from a canned list in the script that users can change to suit their preferences (this is Python, after all). The standard file open and save selection dialogs in PyEdit use object-based interfaces to remember the last directory visited, so you don’t have to navigate there every time.
One of the more unique features of PyEdit is that it can actually run Python program code that you are editing. This isn’t as hard as it may sound either—because Python provides built-ins for compiling and running code strings and for launching programs, PyEdit simply has to make the right calls for this to work. For example, it’s easy to code a simple-minded Python interpreter in Python (though you need a bit more to handle multiple-line statements), as shown in Example 12-1.
Example 12-1. PP3EGuiTextEditorsimpleshell.py
namespace= {} while 1: try: line = raw_input('>>> ') # single-line statements only except EOFError: break else: exec line in namespace # or eval( ) and print result
Depending on the user’s preference, PyEdit either does
something similar to this to run code fetched from the text widget
or uses the launchmodes
module
we wrote at the end of Chapter
5 to run the code’s file as an independent program. There
are a variety of options in both schemes that you can customize as
you like (this is Python, after all). See the onRunCode
method for details or simply
edit and run some Python code on your own. When edited code is run
in nonfile mode, you can view its printed output in PyEdit’s
console window.
Figure 12-3 shows three independently started instances of PyEdit running with a variety of color schemes, sizes, and fonts. This figure also captures two PyEdit torn-off menus (lower right) and the PyEdit help pop up (upper right). The edit windows’ backgrounds are shades of blue, green, and red; use the Tools menu’s Pick options to set colors as you like.
Since these three PyEdit sessions are editing Python source-coded text, you can run their contents with the Run Code option in the Tools pull-down menu. Code run from files is spawned independently; the standard streams of code run not from a file (i.e., fetched from the text widget itself) are mapped to the PyEdit session’s console window. This isn’t an IDE by any means; it’s just something I added because I found it to be useful. It’s nice to run code you’re editing without fishing through directories.
New for this edition of the book is a font input dialog—a simple, three-entry, nonmodal dialog where you can type the font family, size, and style, instead of picking them from a list of preset options. (You can find more sophisticated Tk font selection dialogs in both the public domain and within the implementation of Python’s standard IDLE development GUI—as mentioned earlier, it is itself a Python/Tkinter program.)
Also new in this edition, PyEdit supports unlimited edit
undo and redo, as well as an accurate modified check before quit,
open, and new actions to prompt for saves (instead of always
asking naïvely). The underlying Tk 8.4 library provides an API,
which makes this simple—Tk keeps undo and redo stacks
automatically. They are enabled with the Text
widget’s undo
configuration option and are
accessed with the widget methods edit_undo
and edit_redo
. Similarly, edit_reset
clears the stacks (e.g.,
after a new file open), and edit_modified
checks or sets the
automatic text modified flag.
It’s also possible to undo cuts and pastes right after you’ve done them (simply paste back from the clipboard or cut the pasted and selected text), but the new undo/redo operations are more complete and simpler to use. Undo was a suggested exercise in the prior edition of this book, but it has been made almost trivial by the new Tk API.
For usability, this edition’s version of PyEdit also allows
users to set startup configuration options by assigning variables
in a module, textConfig.py
. If
present, these assignments give initial values for font, colors,
text window size, and search case sensitivity. Fonts and colors
can be changed in the menus and windows can be freely resized, so
this is largely just a convenience. Also note that this module’s
settings will be inherited by all instances of PyEdit—even when it
is a pop-up window or an embedded component of another
application. Client applications should configure per their
needs.
The PyEdit program consists of just a small
configuration module and one main source file—a
.py that can be either run or imported. For use
on Windows, there is also a one-line .pyw file
that just executes the .py file’s contents with
an execfile('textEditor.py')
call. The .pyw suffix avoids the DOS console
streams window pop up when launched on Windows.
Today, .pyw files can be both imported
and run, like normal .py files (they can also
be double-clicked, and launched by Python tools such as os.system
and os.startfile
), so we don’t really need a
separate file to support both import and console-less run modes. I
retained the .py, though, in order to see
printed text during development and to use PyEdit as a simple
IDE—when the run code option is selected, in nonfile mode printed
output from code being edited shows up in PyEdit’s DOS console
window in Windows. Clients will normally import the
.py file.
First, PyEdit’s user configuration module is listed in Example 12-2. As mentioned, this is mostly a convenience, for providing an initial look-and-feel other than the default. PyEdit is coded to work even if this module is missing or contains syntax errors.
Example 12-2. PP3EGuiTextEditor extConfig.py
############################################################# # PyEdit (testEditor.py) user startup configuration module # comment-out any of these to accept Tk or program defaults # can also change font/colors from menus, and resize window ############################################################# # initial font # family, size, style font = ('courier', 9, 'normal') # e.g., style: 'bold italic' # initial color # default=white, black bg = 'lightcyan' # colorname or RGB hexstr fg = 'black' # e.g., 'powder blue', '#690f96' # initial size height = 20 # Tk default: 24 lines width = 80 # Tk default: 80 characters # search case-insensitive caseinsens = 1 # default=1 (on)
Next, Example 12-3 gives the .pyw launching file used to suppress a DOS pop up on Windows, but still allow for it when the .py file is run directly (to see the output of edited code run in nonfile mode, for example).
Example 12-3. PP3EGuiTextEditor extEditorNoConsole.pyw
######################################################################## # run without a DOS pop up on Windows # could use just a .pyw for both inports and launch, # but .py file retained for seeing any printed text ######################################################################## execfile('textEditor.py') # as if pasted here (or textEditor.main( ))
And finally, the module in Example 12-4 is PyEdit’s implementation. This file may run directly as a top-level script, or it can be imported from other applications. Its code is organized by the GUI’s main menu options. The main classes used to start and embed a PyEdit object appear at the end of this file. Study this listing while you experiment with PyEdit, to learn about its features and techniques.
Example 12-4. PP3EGuiTextEditor extEditor.py
################################################################################ # PyEdit 2.0: a Python/Tkinter text file editor and component. # # Uses the Tk text widget, plus GuiMaker menus and toolbar buttons to # implement a full-featured text editor that can be run as a standalone # program, and attached as a component to other GUIs. Also used by # PyMailGUI and PyView to edit mail text and image file notes, and # by PyMailGUI and PyDemos2 in pop-up mode to display source files. # # New in 2.0: # -added simple font components input dialog # -use Tk 8.4 undo stack API to add undo, redo modified test # -now verifies on quit, open, new, run, only if text modified and unsaved # -searches are case-insensitive now # -configuration module for initial font/color/size/searchcase # TBD: could also allow search case choice in GUI, and could use regexps. ################################################################################ Version = '2.0' import sys, os # platform, args, run tools from Tkinter import * # base widgets, constants from tkFileDialog import Open, SaveAs # standard dialogs from tkMessageBox import showinfo, showerror, askyesno from tkSimpleDialog import askstring, askinteger from tkColorChooser import askcolor from PP2E.Gui.Tools.guimaker import * # Frame + menu/toolbar builders try: import textConfig # startup font and colors configs = textConfig._ _dict_ _ # work if not on the path or bad except: configs = {} helptext = """PyEdit version %s January, 2006 (1.0: October, 2000) Programming Python, 3rd Edition O'Reilly Media, Inc. A text editor program and embeddable object component, written in Python/Tkinter. Use menu tear-offs and toolbar for quick access to actions, and Alt-key shortcuts for menus. New in version %s: - font pick dialog - unlimited undo/redo - quit/open/new/run prompt save only if changed - searches are case-insensitive - startup configuration module textConfig.py """ START = '1.0' # index of first char: row=1,col=0 SEL_FIRST = SEL + '.first' # map sel tag to index SEL_LAST = SEL + '.last' # same as 'sel.last' FontScale = 0 # use bigger font on Linux if sys.platform[:3] != 'win': # and other non-Windows boxes FontScale = 3 ################################################################################ # Main class: implements editor GUI, actions ################################################################################ class TextEditor: # mix with menu/toolbar Frame class startfiledir = '.' ftypes = [('All files', '*'), # for file open dialog ('Text files', '.txt'), # customize in subclass ('Python files', '.py')] # or set in each instance colors = [{'fg':'black', 'bg':'white'}, # color pick list {'fg':'yellow', 'bg':'black'}, # first item is default {'fg':'white', 'bg':'blue'}, # tailor me as desired {'fg':'black', 'bg':'beige'}, # or do PickBg/Fg chooser {'fg':'yellow', 'bg':'purple'}, {'fg':'black', 'bg':'brown'}, {'fg':'lightgreen', 'bg':'darkgreen'}, {'fg':'darkblue', 'bg':'orange'}, {'fg':'orange', 'bg':'darkblue'}] fonts = [('courier', 9+FontScale, 'normal'), # platform-neutral fonts ('courier', 12+FontScale, 'normal'), # (family, size, style) ('courier', 10+FontScale, 'bold'), # or pop up a listbox ('courier', 10+FontScale, 'italic'), # make bigger on Linux ('times', 10+FontScale, 'normal'), # use 'bold italic' for 2 ('helvetica', 10+FontScale, 'normal'), # also 'underline', etc. ('ariel', 10+FontScale, 'normal'), ('system', 10+FontScale, 'normal'), ('courier', 20+FontScale, 'normal')] def _ _init_ _(self, loadFirst=''): if not isinstance(self, GuiMaker): raise TypeError, 'TextEditor needs a GuiMaker mixin' self.setFileName(None) self.lastfind = None self.openDialog = None self.saveDialog = None self.text.focus( ) # else must click in text if loadFirst: self.onOpen(loadFirst) def start(self): # run by GuiMaker._ _init_ _ self.menuBar = [ # configure menu/toolbar ('File', 0, # a GuiMaker menu def tree [('Open...', 0, self.onOpen), # build in method for self ('Save', 0, self.onSave), # label, shortcut, callback ('Save As...', 5, self.onSaveAs), ('New', 0, self.onNew), 'separator', ('Quit...', 0, self.onQuit)] ), ('Edit', 0, [('Undo', 0, self.onUndo), ('Redo', 0, self.onRedo), 'separator', ('Cut', 0, self.onCut), ('Copy', 1, self.onCopy), ('Paste', 0, self.onPaste), 'separator', ('Delete', 0, self.onDelete), ('Select All', 0, self.onSelectAll)] ), ('Search', 0, [('Goto...', 0, self.onGoto), ('Find...', 0, self.onFind), ('Refind', 0, self.onRefind), ('Change...', 0, self.onChange)] ), ('Tools', 0, [('Pick Font...', 6, self.onPickFont), ('Font List', 0, self.onFontList), 'separator', ('Pick Bg...', 3, self.onPickBg), ('Pick Fg...', 0, self.onPickFg), ('Color List', 0, self.onColorList), 'separator', ('Info...', 0, self.onInfo), ('Clone', 1, self.onClone), ('Run Code', 0, self.onRunCode)] )] self.toolBar = [ ('Save', self.onSave, {'side': LEFT}), ('Cut', self.onCut, {'side': LEFT}), ('Copy', self.onCopy, {'side': LEFT}), ('Paste', self.onPaste, {'side': LEFT}), ('Find', self.onRefind, {'side': LEFT}), ('Help', self.help, {'side': RIGHT}), ('Quit', self.onQuit, {'side': RIGHT})] def makeWidgets(self): # run by GuiMaker._ _init_ _ name = Label(self, bg='black', fg='white') # add below menu, above tool name.pack(side=TOP, fill=X) # menu/toolbars are packed vbar = Scrollbar(self) hbar = Scrollbar(self, orient='horizontal') text = Text(self, padx=5, wrap='none') text.config(undo=1, autoseparators=1) # 2.0, default is 0, 1 vbar.pack(side=RIGHT, fill=Y) hbar.pack(side=BOTTOM, fill=X) # pack text last text.pack(side=TOP, fill=BOTH, expand=YES) # else sbars clipped text.config(yscrollcommand=vbar.set) # call vbar.set on text move text.config(xscrollcommand=hbar.set) vbar.config(command=text.yview) # call text.yview on scroll move hbar.config(command=text.xview) # or hbar['command']=text.xview # 2.0: apply user configs or defaults startfont = configs.get('font', self.fonts[0]) startbg = configs.get('bg', self.colors[0]['bg']) startfg = configs.get('fg', self.colors[0]['fg']) text.config(font=startfont, bg=startbg, fg=startfg) if 'height' in configs: text.config(height=configs['height']) if 'width' in configs: text.config(width =configs['width']) self.text = text self.filelabel = name ############################################################################ # File menu commands ############################################################################ def my_askopenfilename(self): # objects remember last result dir/file if not self.openDialog: self.openDialog = Open(initialdir=self.startfiledir, filetypes=self.ftypes) return self.openDialog.show( ) def my_asksaveasfilename(self): # objects remember last result dir/file if not self.saveDialog: self.saveDialog = SaveAs(initialdir=self.startfiledir, filetypes=self.ftypes) return self.saveDialog.show( ) def onOpen(self, loadFirst=''): doit = (not self.text_edit_modified( ) or # 2.0 askyesno('PyEdit', 'Text has changed: discard changes?')) if doit: file = loadFirst or self.my_askopenfilename( ) if file: try: text = open(file, 'r').read( ) except: showerror('PyEdit', 'Could not open file ' + file) else: self.setAllText(text) self.setFileName(file) self.text.edit_reset( ) # 2.0: clear undo/redo stks self.text.edit_modified(0) # 2.0: clear modified flag def onSave(self): self.onSaveAs(self.currfile) # may be None def onSaveAs(self, forcefile=None): file = forcefile or self.my_asksaveasfilename( ) if file: text = self.getAllText( ) try: open(file, 'w').write(text) except: showerror('PyEdit', 'Could not write file ' + file) else: self.setFileName(file) # may be newly created self.text.edit_modified(0) # 2.0: clear modified flag # don't clear undo/redo stks def onNew(self): doit = (not self.text_edit_modified( ) or # 2.0 askyesno('PyEdit', 'Text has changed: discard changes?')) if doit: self.setFileName(None) self.clearAllText( ) self.text.edit_reset( ) # 2.0: clear undo/redo stks self.text.edit_modified(0) # 2.0: clear modified flag def onQuit(self): doit = (not self.text_edit_modified( ) # 2.0 or askyesno('PyEdit', 'Text has changed: quit and discard changes?')) if doit: self.quit( ) # Frame.quit via GuiMaker def text_edit_modified(self): """ 2.0: self.text.edit_modified( ) broken in Python 2.4: do manually for now (seems to be bool result type bug) """ return self.tk.call((self.text._w, 'edit') + ('modified', None)) ############################################################################ # Edit menu commands ############################################################################ def onUndo(self): # 2.0 try: # tk8.4 keeps undo/redo stacks self.text.edit_undo( ) # exception if stacks empty except TclError: # menu tear-offs for quick undo showinfo('PyEdit', 'Nothing to undo') def onRedo(self): # 2.0: redo an undone try: self.text.edit_redo( ) except TclError: showinfo('PyEdit', 'Nothing to redo') def onCopy(self): # get text selected by mouse, etc. if not self.text.tag_ranges(SEL): # save in cross-app clipboard showerror('PyEdit', 'No text selected') else: text = self.text.get(SEL_FIRST, SEL_LAST) self.clipboard_clear( ) self.clipboard_append(text) def onDelete(self): # delete selected text, no save if not self.text.tag_ranges(SEL): showerror('PyEdit', 'No text selected') else: self.text.delete(SEL_FIRST, SEL_LAST) def onCut(self): if not self.text.tag_ranges(SEL): showerror('PyEdit', 'No text selected') else: self.onCopy( ) # save and delete selected text self.onDelete( ) def onPaste(self): try: text = self.selection_get(selection='CLIPBOARD') except TclError: showerror('PyEdit', 'Nothing to paste') return self.text.insert(INSERT, text) # add at current insert cursor self.text.tag_remove(SEL, '1.0', END) self.text.tag_add(SEL, INSERT+'-%dc' % len(text), INSERT) self.text.see(INSERT) # select it, so it can be cut def onSelectAll(self): self.text.tag_add(SEL, '1.0', END+'-1c') # select entire text self.text.mark_set(INSERT, '1.0') # move insert point to top self.text.see(INSERT) # scroll to top ############################################################################ # Search menu commands ############################################################################ def onGoto(self, forceline=None): line = forceline or askinteger('PyEdit', 'Enter line number') self.text.update( ) self.text.focus( ) if line is not None: maxindex = self.text.index(END+'-1c') maxline = int(maxindex.split('.')[0]) if line > 0 and line <= maxline: self.text.mark_set(INSERT, '%d.0' % line) # goto line self.text.tag_remove(SEL, '1.0', END) # delete selects self.text.tag_add(SEL, INSERT, 'insert + 1l') # select line self.text.see(INSERT) # scroll to line else: showerror('PyEdit', 'Bad line number') def onFind(self, lastkey=None): key = lastkey or askstring('PyEdit', 'Enter search string') self.text.update( ) self.text.focus( ) self.lastfind = key if key: # 2.0: nocase nocase = configs.get('caseinsens', 1) # 2.0: config where = self.text.search(key, INSERT, END, nocase=nocase) if not where: # don't wrap showerror('PyEdit', 'String not found') else: pastkey = where + '+%dc' % len(key) # index past key self.text.tag_remove(SEL, '1.0', END) # remove any sel self.text.tag_add(SEL, where, pastkey) # select key self.text.mark_set(INSERT, pastkey) # for next find self.text.see(where) # scroll display def onRefind(self): self.onFind(self.lastfind) def onChange(self): new = Toplevel(self) Label(new, text='Find text:').grid(row=0, column=0) Label(new, text='Change to:').grid(row=1, column=0) self.change1 = Entry(new) self.change2 = Entry(new) self.change1.grid(row=0, column=1, sticky=EW) self.change2.grid(row=1, column=1, sticky=EW) Button(new, text='Find', command=self.onDoFind).grid(row=0, column=2, sticky=EW) Button(new, text='Apply', command=self.onDoChange).grid(row=1, column=2, sticky=EW) new.columnconfigure(1, weight=1) # expandable entries def onDoFind(self): self.onFind(self.change1.get( )) # Find in change box def onDoChange(self): if self.text.tag_ranges(SEL): # must find first self.text.delete(SEL_FIRST, SEL_LAST) # Apply in change self.text.insert(INSERT, self.change2.get( )) # deletes if empty self.text.see(INSERT) self.onFind(self.change1.get( )) # goto next appear self.text.update( ) # force refresh ############################################################################ # Tools menu commands ############################################################################ def onFontList(self): self.fonts.append(self.fonts[0]) # pick next font in list del self.fonts[0] # resizes the text area self.text.config(font=self.fonts[0]) def onColorList(self): self.colors.append(self.colors[0]) # pick next color in list del self.colors[0] # move current to end self.text.config(fg=self.colors[0]['fg'], bg=self.colors[0]['bg']) def onPickFg(self): self.pickColor('fg') # added on 10/02/00 def onPickBg(self): # select arbitrary color self.pickColor('bg') # in standard color dialog def pickColor(self, part): # this is too easy (triple, hexstr) = askcolor( ) if hexstr: self.text.config(**{part: hexstr}) def onInfo(self): text = self.getAllText( ) # added on 5/3/00 in 15 mins bytes = len(text) # words uses a simple guess: lines = len(text.split(' ')) # any separated by whitespace words = len(text.split( )) index = self.text.index(INSERT) where = tuple(index.split('.')) showinfo('PyEdit Information', 'Current location: ' + 'line: %s column: %s ' % where + 'File text statistics: ' + 'bytes: %d lines: %d words: %d ' % (bytes, lines, words)) def onClone(self): new = Toplevel( ) # a new edit window in same process myclass = self._ _class_ _ # instance's (lowest) class object myclass(new) # attach/run instance of my class def onRunCode(self, parallelmode=True): """ run Python code being edited--not an IDE, but handy; tries to run in file's dir, not cwd (may be PP3E root); inputs and adds command-line arguments for script files; code's stdin/out/err = editor's start window, if any: run with a console window to see code's print outputs; but parallelmode uses start to open a DOS box for I/O; module search path will include '.' dir where started; in non-file mode, code's Tk root window is PyEdit win; """ def askcmdargs( ): return askstring('PyEdit', 'Commandline arguments?') or '' from PP3E.launchmodes import System, Start, Fork filemode = False thefile = str(self.getFileName( )) if os.path.exists(thefile): filemode = askyesno('PyEdit', 'Run from file?') if not filemode: # run text string cmdargs = askcmdargs( ) namespace = {'_ _name_ _': '_ _main_ _'} # run as top-level sys.argv = [thefile] + cmdargs.split( ) # could use threads exec self.getAllText( ) + ' ' in namespace # exceptions ignored elif self.text_edit_modified( ): # 2.0: changed test showerror('PyEdit', 'Text changed: save before run') else: cmdargs = askcmdargs( ) mycwd = os.getcwd( ) # cwd may be root os.chdir(os.path.dirname(thefile) or mycwd) # cd for filenames thecmd = thefile + ' ' + cmdargs if not parallelmode: # run as file System(thecmd, thecmd)( ) # block editor else: if sys.platform[:3] == 'win': # spawn in parallel Start(thecmd, thecmd)( ) # or use os.spawnv else: Fork(thecmd, thecmd)( ) # spawn in parallel os.chdir(mycwd) def onPickFont(self): # 2.0 font spec dialog new = Toplevel(self) Label(new, text='Family:').grid(row=0, column=0) # nonmodal dialog Label(new, text='Size: ').grid(row=1, column=0) # see pick list Label(new, text='Style: ').grid(row=2, column=0) # for valid inputs self.font1 = Entry(new) self.font2 = Entry(new) self.font3 = Entry(new) self.font1.insert(0, 'courier') # suggested vals self.font2.insert(0, '12') self.font3.insert(0, 'bold italic') self.font1.grid(row=0, column=1, sticky=EW) self.font2.grid(row=1, column=1, sticky=EW) self.font3.grid(row=2, column=1, sticky=EW) Button(new, text='Apply', command=self.onDoFont).grid(row=3, columnspan=2) new.columnconfigure(1, weight=1) # expandable entrys def onDoFont(self): try: font = (self.font1.get(), int(self.font2.get()), self.font3.get( )) self.text.config(font=font) except: showerror('PyEdit', 'Bad font specification') ############################################################################ # Utilities, useful outside this class ############################################################################ def isEmpty(self): return not self.getAllText( ) def getAllText(self): return self.text.get('1.0', END+'-1c') # extract text as a string def setAllText(self, text): self.text.delete('1.0', END) # store text string in widget self.text.insert(END, text) # or '1.0' self.text.mark_set(INSERT, '1.0') # move insert point to top self.text.see(INSERT) # scroll to top, insert set def clearAllText(self): self.text.delete('1.0', END) # clear text in widget def getFileName(self): return self.currfile def setFileName(self, name): # also: onGoto(linenum) self.currfile = name # for save self.filelabel.config(text=str(name)) def setBg(self, color): self.text.config(bg=color) # to set manually from code def setFg(self, color): self.text.config(fg=color) # 'black', hexstring def setFont(self, font): self.text.config(font=font) # ('family', size, 'style') def setHeight(self, lines): # default = 24h × 80w self.text.config(height=lines) # may also be from textCongif.py def setWidth(self, chars): self.text.config(width=chars) def clearModified(self): self.text.edit_modified(0) # clear modified flag def isModified(self): return self.text_edit_modified( ) # changed since last reset? def help(self): showinfo('About PyEdit', helptext % ((Version,)*2)) ################################################################################ # ready-to-use editor classes # mix in a Frame subclass that builds menu/toolbars ################################################################################ # # when editor owns the window # class TextEditorMain(TextEditor, GuiMakerWindowMenu): # add menu/toolbar maker def _ _init_ _(self, parent=None, loadFirst=''): # when fills whole window GuiMaker._ _init_ _(self, parent) # use main window menus TextEditor._ _init_ _(self, loadFirst) # self has GuiMaker frame self.master.title('PyEdit ' + Version) # title if standalone self.master.iconname('PyEdit') # catch wm delete button self.master.protocol('WM_DELETE_WINDOW', self.onQuit) class TextEditorMainPopup(TextEditor, GuiMakerWindowMenu): def _ _init_ _(self, parent=None, loadFirst='', winTitle=''): self.popup = Toplevel(parent) # create own window GuiMaker._ _init_ _(self, self.popup) # use main window menus TextEditor._ _init_ _(self, loadFirst) assert self.master == self.popup self.popup.title('PyEdit ' + Version + winTitle) self.popup.iconname('PyEdit') def quit(self): self.popup.destroy( ) # kill this window only # # when embedded in another window # class TextEditorComponent(TextEditor, GuiMakerFrameMenu): def _ _init_ _(self, parent=None, loadFirst=''): # use Frame-based menus GuiMaker._ _init_ _(self, parent) # all menus, buttons on TextEditor._ _init_ _(self, loadFirst) # GuiMaker must init 1st class TextEditorComponentMinimal(TextEditor, GuiMakerFrameMenu): def _ _init_ _(self, parent=None, loadFirst='', deleteFile=1): self.deleteFile = deleteFile GuiMaker._ _init_ _(self, parent) TextEditor._ _init_ _(self, loadFirst) def start(self): TextEditor.start(self) # GuiMaker start call for i in range(len(self.toolBar)): # delete quit in toolbar if self.toolBar[i][0] == 'Quit': # delete file menu items del self.toolBar[i]; break # or just disable file if self.deleteFile: for i in range(len(self.menuBar)): if self.menuBar[i][0] == 'File': del self.menuBar[i]; break else: for (name, key, items) in self.menuBar: if name == 'File': items.append([1,2,3,4,6]) ################################################################################ # standalone program run ################################################################################ def testPopup( ): # see PyView and PyMail for component tests root = Tk( ) TextEditorMainPopup(root) TextEditorMainPopup(root) Button(root, text='More', command=TextEditorMainPopup).pack(fill=X) Button(root, text='Quit', command=root.quit).pack(fill=X) root.mainloop( ) def main( ): # may be typed or clicked try: # or associated on Windows fname = sys.argv[1] # arg = optional filename except IndexError: fname = None TextEditorMain(loadFirst=fname).pack(expand=YES, fill=BOTH) mainloop( ) if _ _name_ _ == '_ _main_ _': # when run as a script #testPopup( ) main( ) # run .pyw for no DOS box
3.17.76.72