PyEdit: A Text Editor Program/Object

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:

Standalone mode

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.

Pop-up mode

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.

Embedded mode

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.

Running PyEdit

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.

PyEdit main window, editing itself

Figure 12-1. PyEdit main window, editing itself

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.

Menus and toolbars

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).

Dialogs

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.

PyEdit with colors, a font, and a few pop ups

Figure 12-2. PyEdit with colors, a font, and a few pop ups

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.

Running program code

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.

Multiple PyEdit sessions at work

Figure 12-3. Multiple PyEdit sessions at work

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 features in version 2.0

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.

PyEdit Source Code

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
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.76.72