Example #1
0
    def run(self):
        dialog = SaveAs(master=None, filetypes=[('Excel', '*.xls')]).show()

        if dialog.find('.xls') == -1:
            dialog = dialog + '.xls'

        wb = xlwt.Workbook()
        ws = wb.add_sheet('test')

        ws.write(0, 0, '№')
        ws.write(0, 1, 'Время')
        ws.write(0, 2, 'Пользователь')
        ws.write(0, 3, 'Экзамен')
        ws.write(0, 4, 'Оценка')

        numberRow = 1
        for obj in self.test:
            ws.write(numberRow, 0, obj.id)
            ws.write(numberRow, 1, obj.time.isoformat(sep='T'))
            ws.write(numberRow, 2, obj.user.username)
            ws.write(numberRow, 3, obj.exam.name)
            ws.write(numberRow, 4,
                     str(obj.mark) + '/' + str(obj.exam.number_questions))
            numberRow += 1

        wb.save(dialog)
Example #2
0
    def _saveIfChanged(self):
        """
        Check to see if the current document has changed. If so, ask the
        user if it should be saved. If so, prompt for a path and save the
        file to that path. If the user does not want to save the file, or
        the Cancel button is pressed in the Save As dialog, do nothing.
        """

        # If the current document is unchanged, return.
        changed = self._sourceLibraryDocumentEditor.getChanged()
        if not changed: return

        # The current document has changed, so ask the user if it
        # should be saved. If not, return.
        path = self._path
        if path is None:
            message = 'The document has been modified.'
        else:
            message = 'The document "%s" has been modified. ' % path
        message += ' Do you want to save your changes?'
        saveFile = askyesno(title = 'ModelEditor', message = message)
        if not saveFile:
            return

        # If the file has no associated path, get one. If the Cancel button
        # is pressed, return without saving the file.
        if path is None:
            path = SaveAs().show()
            if path == '':
                return

        # Save the document.
        self._save(path)
Example #3
0
    def asksaveasfile(mode="w", **options):
        "Ask for a filename to save as, and returned the opened file"

        filename = SaveAs(**options).show()
        if filename:
            return open(filename, mode)
        return None
Example #4
0
    def _onFileExportToObsSim(self):
        if debug: print ('File/Export to ObsSim...')

        # Get the new path for this document. If none, return.
        path = SaveAs().show()
        if path == ():
            return

        # Export the file.
        self._exportToObsSim(path)
Example #5
0
    def _onFileSaveAs(self):
        if debug: print ('File/Save As...')

        # Get the new path for this document. If none, return.
        path = SaveAs().show()
        if path == ():
            return

        # Save the document.
        self._save(path)
class TextEditor:  # mix with menu/toolbar Frame class
    startfiledir = '.'
    editwindows = []
    if __name__ == '__main__':
        from textConfig import (opensAskUser, opensEncoding,
                                savesUseKnownEncoding, savesAskUser,
                                savesEncoding)
    else:
        from .textConfig import (opensAskUser, opensEncoding,
                                 savesUseKnownEncoding, savesAskUser,
                                 savesEncoding)

    ftypes = [('All files', '*'), ('Text files', '.txt'),
              ('Python files', '.py')]

    colors = [{
        'fg': 'black',
        'bg': 'white'
    }, {
        'fg': 'yellow',
        'bg': 'black'
    }, {
        'fg': 'white',
        'bg': 'blue'
    }, {
        'fg': 'black',
        'bg': 'beige'
    }, {
        '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'),
             ('courier', 12 + FontScale, 'normal'),
             ('courier', 10 + FontScale, 'bold'),
             ('courier', 10 + FontScale, 'italic'),
             ('times', 10 + FontScale, 'normal'),
             ('helvetica', 10 + FontScale, 'normal'),
             ('ariel', 10 + FontScale, 'normal'),
             ('system', 10 + FontScale, 'normal'),
             ('courier', 20 + FontScale, 'normal')]

    def __init__(self, loadFirst='', loadEncode=''):
        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.knownEncoding = None
        self.text.focus()
        if loadFirst:
            self.update()
            self.onOpen(loadFirst, loadEncode)

    def start(self):
        self.menuBar = [('File', 0, [('Open...', 0, self.onOpen),
                                     ('Save', 0, self.onSave),
                                     ('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),
                                       ('Grep...', 3, self.onGrep)]),
                        ('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')  # disable line wrapping
        text.config(undo=1, autoseparators=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)
        text.config(xscrollcommand=hbar.set)
        vbar.config(command=text.yview)
        hbar.config(command=text.xview)
        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

    def my_askopenfilename(self):
        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='', loadEncode=''):
        if self.text_edit_modified():
            if not askyesno('SimpleEditor',
                            'Text has changed: discard changes?'):
                return

        file = loadFirst or self.my_askopenfilename()
        if not file:
            return

        if not os.path.isfile(file):
            showerror('SimpleEditor', 'Could not open file ' + file)
            return
        text = None
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):  # lookup: bad name
                pass
        if text == None and self.opensAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('SimpleEditor',
                                'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass
        if text == None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass
        if text == None:
            try:
                text = open(file, 'r',
                            encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass
        if text == None:
            try:
                text = open(file, 'rb').read()
                text = text.replace(b'\r\n', b'\n')
                self.knownEncoding = None
            except IOError:
                pass

        if text == None:
            showerror('SimpleEditor', 'Could not decode and open file ' + file)
        else:
            self.setAllText(text)
            self.setFileName(file)
            self.text.edit_reset()
            self.text.edit_modified(0)

    def onSave(self):
        self.onSaveAs(self.currfile)

    def onSaveAs(self, forcefile=None):
        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return

        text = self.getAllText()
        encpick = None
        if self.knownEncoding and (
            (forcefile and self.savesUseKnownEncoding >= 1) or
            (not forcefile and self.savesUseKnownEncoding >= 2)):
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass
        if not encpick and self.savesAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('SimpleEditor',
                                'Enter Unicode encoding for save',
                                initialvalue=(self.knownEncoding
                                              or self.savesEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            if askuser:
                try:
                    text.encode(askuser)
                    encpick = askuser
                except (UnicodeError, LookupError):
                    pass
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass

        if not encpick:
            showerror('SimpleEditor', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('SimpleEditor', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)
                self.text.edit_modified(0)
                self.knownEncoding = encpick

    def onNew(self):
        if self.text_edit_modified():
            if not askyesno('SimpleEditor',
                            'Text has changed: discard changes?'):
                return
        self.setFileName(None)
        self.clearAllText()
        self.text.edit_reset()
        self.text.edit_modified(0)
        self.knownEncoding = None

    def onQuit(self):
        assert False, 'onQuit must be defined in window-specific sublass'

    def text_edit_modified(self):
        return self.text.edit_modified()

    def onUndo(self):
        try:
            self.text.edit_undo()
        except TclError:
            showinfo('SimpleEditor', 'Nothing to undo')

    def onRedo(self):
        try:
            self.text.edit_redo()
        except TclError:
            showinfo('SimpleEditor', 'Nothing to redo')

    def onCopy(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', '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('SimpleEditor', 'No text selected')
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', '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('SimpleEditor', 'Nothing to paste')
            return
        self.text.insert(INSERT, text)
        self.text.tag_remove(SEL, '1.0', END)
        self.text.tag_add(SEL, INSERT + '-%dc' % len(text), INSERT)
        self.text.see(INSERT)

    def onSelectAll(self):
        self.text.tag_add(SEL, '1.0', END + '-1c')
        self.text.mark_set(INSERT, '1.0')
        self.text.see(INSERT)

    def onGoto(self, forceline=None):
        line = forceline or askinteger('SimpleEditor', '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('SimpleEditor', 'Bad line number')

    def onFind(self, lastkey=None):
        key = lastkey or askstring('SimpleEditor', 'Enter search string')
        self.text.update()
        self.text.focus()
        self.lastfind = key
        if key:
            nocase = configs.get('caseinsens', True)
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:  # don't wrap
                showerror('SimpleEditor', '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)
        new.title('SimpleEditor - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0,
                                                                   column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1,
                                                                   column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():
            self.onFind(entry1.get())

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0,
                                                      column=2,
                                                      sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1,
                                                        column=2,
                                                        sticky=EW)
        new.columnconfigure(1, weight=1)  # expandable entries

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):  # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)  # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)  # goto next appear
            self.text.update()  # force refresh

    def onGrep(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel()
        popup.title('SimpleEditor - grep')
        var1 = makeFormRow(popup,
                           label='Directory root',
                           width=18,
                           browse=False)
        var2 = makeFormRow(popup,
                           label='Filename pattern',
                           width=18,
                           browse=False)
        var3 = makeFormRow(popup,
                           label='Search string',
                           width=18,
                           browse=False)
        var4 = makeFormRow(popup,
                           label='Content encoding',
                           width=18,
                           browse=False)
        var1.set('.')  # current dir
        var2.set('*.py')  # initial values
        var4.set(sys.getdefaultencoding())
        cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(),
                                   var4.get())
        Button(popup, text='Go', command=cb).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey, encoding):
        import threading, queue
        mypopup = Tk()
        mypopup.title('SimpleEditor - grepping')
        status = Label(mypopup,
                       text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close

        # start producer thread, consumer loop
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue)
        threading.Thread(target=self.grepThreadProducer,
                         args=threadargs).start()
        self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding,
                           myqueue):
        from PP4E.Tools.find import find
        matches = []
        try:
            for filepath in find(pattern=filenamepatt, startdir=dirname):
                try:
                    textfile = open(filepath, encoding=encoding)
                    for (linenum, linestr) in enumerate(textfile):
                        if grepkey in linestr:
                            msg = '%s@%d  [%s]' % (filepath, linenum + 1,
                                                   linestr)
                            matches.append(msg)
                except UnicodeError as X:
                    print('Unicode error in:', filepath, X)  # eg: decode, bom
                except IOError as X:
                    print('IO error in:', filepath, X)  # eg: permission
        finally:
            myqueue.put(matches)  # stop consumer loop on find excs: filenames?

    def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup):
        import queue
        try:
            matches = myqueue.get(block=False)
        except queue.Empty:
            myargs = (grepkey, encoding, myqueue, mypopup)
            self.after(250, self.grepThreadConsumer, *myargs)
        else:
            mypopup.destroy()
            self.update()
            if not matches:
                showinfo('SimpleEditor',
                         'Grep found no matches for: %r' % grepkey)
            else:
                self.grepMatchesList(matches, grepkey, encoding)

    def grepMatchesList(self, matches, grepkey, encoding):

        from PP4E.Gui.Tour.scrolledlist import ScrolledList
        print('Matches for %s: %s' % (grepkey, len(matches)))

        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):
                file, line = selection.split('  [', 1)[0].split('@')
                editor = TextEditorMainPopup(loadFirst=file,
                                             winTitle=' grep match',
                                             loadEncode=encoding)
                editor.onGoto(int(line))
                editor.text.focus_force()

        popup = Tk()
        popup.title('SimpleEditor - grep matches: %r (%s)' %
                    (grepkey, encoding))
        ScrolledFilenames(parent=popup, options=matches)

    def onFontList(self):
        self.fonts.append(self.fonts[0])
        del self.fonts[0]
        self.text.config(font=self.fonts[0])

    def onColorList(self):
        self.colors.append(self.colors[0])
        del self.colors[0]
        self.text.config(fg=self.colors[0]['fg'], bg=self.colors[0]['bg'])

    def onPickFg(self):
        self.pickColor('fg')

    def onPickBg(self):
        self.pickColor('bg')

    def pickColor(self, part):
        (triple, hexstr) = askcolor()
        if hexstr:
            self.text.config(**{part: hexstr})

    def onInfo(self):

        text = self.getAllText()
        bytes = len(text)
        lines = len(text.split('\n'))
        words = len(text.split())
        index = self.text.index(INSERT)
        where = tuple(index.split('.'))
        showinfo(
            'SimpleEditor Information',
            'Current location:\n\n' + 'line:\t%s\ncolumn:\t%s\n\n' % where +
            'File text statistics:\n\n' +
            'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self, makewindow=True):
        if not makewindow:
            new = None
        else:
            new = Toplevel()
        myclass = self.__class__
        myclass(new)

    def onRunCode(self, parallelmode=True):
        def askcmdargs():
            return askstring('SimpleEditor', 'Commandline arguments?') or ''

        from PP4E.launchmodes import System, Start, StartArgs, Fork
        filemode = False
        thefile = str(self.getFileName())
        if os.path.exists(thefile):
            filemode = askyesno('SimpleEditor', 'Run from file?')
            self.update()
        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() + '\n', namespace)
        elif self.text_edit_modified():
            showerror('SimpleEditor', 'Text changed: you must save before run')
        else:
            cmdargs = askcmdargs()
            mycwd = os.getcwd()  # cwd may be root
            dirname, filename = os.path.split(thefile)  # get dir, base
            os.chdir(dirname or mycwd)  # cd for filenames
            thecmd = filename + ' ' + cmdargs
            if not parallelmode:  # run as file
                System(thecmd, thecmd)()  # block editor
            else:
                if sys.platform[:3] == 'win':  # spawn in parallel
                    run = StartArgs if cmdargs else Start
                    run(thecmd, thecmd)()  # or always Spawn
                else:
                    Fork(thecmd, thecmd)()  # spawn in parallel
            os.chdir(mycwd)  # go back to my dir

    def onPickFont(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('SimpleEditor - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var1.set('courier')
        var2.set('12')
        var3.set('bold italic')
        Button(popup,
               text='Apply',
               command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        except:
            showerror('SimpleEditor', 'Bad font specification')

    def isEmpty(self):
        return not self.getAllText()

    def getAllText(self):
        return self.text.get('1.0', END + '-1c')

    def setAllText(self, text):
        self.text.delete('1.0', END)
        self.text.insert(END, text)
        self.text.mark_set(INSERT, '1.0')
        self.text.see(INSERT)

    def clearAllText(self):
        self.text.delete('1.0', END)

    def getFileName(self):
        return self.currfile

    def setFileName(self, name):  # see also: onGoto(linenum)
        self.currfile = name  # for save
        self.filelabel.config(text=str(name))

    def setKnownEncoding(self, encoding='utf-8'):
        self.knownEncoding = encoding

    def setBg(self, color):
        self.text.config(bg=color)  # to set manually from code

    def setFg(self, color):
        self.text.config(fg=color)

    def setFont(self, font):
        self.text.config(font=font)  # ('family', size, 'style')

    def setHeight(self, lines):  # default = 24h x 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 SimpleEditor', helptext % ((Version, ) * 2))
Example #7
0
 def my_asksaveasfilename(self):
     if not self.saveDialog:
         self.saveDialog = SaveAs(initialdir=self.startfiledir,
                                  filetypes=self.ftypes)
         return self.saveDialog.show()
class TextEditor:                        # mix with menu/toolbar Frame class
    startfiledir = '.'                  
    editwindows  = []                  
    if __name__ == '__main__':
        from textConfig import (               
            opensAskUser, opensEncoding,
            savesUseKnownEncoding, savesAskUser, savesEncoding)
    else:
        from .textConfig import (             
            opensAskUser, opensEncoding,
            savesUseKnownEncoding, savesAskUser, savesEncoding)

    ftypes = [('All files',     '*'),                
              ('Text files',   '.txt'),               
              ('Python files', '.py')]             

    colors = [{'fg':'black',      'bg':'white'},   
              {'fg':'yellow',     'bg':'black'},     
              {'fg':'white',      'bg':'blue'},       
              {'fg':'black',      'bg':'beige'},    
              {'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'),  
              ('courier',   12+FontScale, 'normal'), 
              ('courier',   10+FontScale, 'bold'),  
              ('courier',   10+FontScale, 'italic'), 
              ('times',     10+FontScale, 'normal'),  
              ('helvetica', 10+FontScale, 'normal'),  
              ('ariel',     10+FontScale, 'normal'),
              ('system',    10+FontScale, 'normal'),
              ('courier',   20+FontScale, 'normal')]

    def __init__(self, loadFirst='', loadEncode=''):
        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.knownEncoding = None                   
        self.text.focus()                           
        if loadFirst:
            self.update()                           
            self.onOpen(loadFirst, loadEncode)

    def start(self):                              
        self.menuBar = [                            
            ('File', 0,                           
                 [('Open...',    0, self.onOpen),  
                  ('Save',       0, self.onSave),   
                  ('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),
                  ('Grep...',    3, self.onGrep)]
            ),
            ('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')        # disable line wrapping
        text.config(undo=1, autoseparators=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)   
        text.config(xscrollcommand=hbar.set)
        vbar.config(command=text.yview)         
        hbar.config(command=text.xview)        
        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

    def my_askopenfilename(self):     
        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='', loadEncode=''):
        if self.text_edit_modified():   
            if not askyesno('SimpleEditor', 'Text has changed: discard changes?'):
                return

        file = loadFirst or self.my_askopenfilename()
        if not file: 
            return
        
        if not os.path.isfile(file):
            showerror('SimpleEditor', 'Could not open file ' + file)
            return
        text = None    
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):         # lookup: bad name
                pass
        if text == None and self.opensAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('SimpleEditor', 'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding or 
                                              sys.getdefaultencoding() or ''))
            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass
        if text == None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass
        if text == None:
            try:
                text = open(file, 'r', encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass
        if text == None:
            try:
                text = open(file, 'rb').read()         
                text = text.replace(b'\r\n', b'\n')   
                self.knownEncoding = None
            except IOError:
                pass

        if text == None:
            showerror('SimpleEditor', 'Could not decode and open file ' + file)
        else:
            self.setAllText(text)
            self.setFileName(file)
            self.text.edit_reset()           
            self.text.edit_modified(0)        

    def onSave(self):
        self.onSaveAs(self.currfile) 

    def onSaveAs(self, forcefile=None):
        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return

        text = self.getAllText()     
        encpick = None                
        if self.knownEncoding and (                                  
           (forcefile     and self.savesUseKnownEncoding >= 1) or   
           (not forcefile and self.savesUseKnownEncoding >= 2)):   
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass
        if not encpick and self.savesAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('SimpleEditor', 'Enter Unicode encoding for save',
                                initialvalue=(self.knownEncoding or 
                                              self.savesEncoding or 
                                              sys.getdefaultencoding() or ''))
            if askuser:
                try:
                    text.encode(askuser)
                    encpick = askuser
                except (UnicodeError, LookupError):   
                    pass                               
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass

        if not encpick:
            showerror('SimpleEditor', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('SimpleEditor', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)        
                self.text.edit_modified(0)         
                self.knownEncoding = encpick        
    def onNew(self):
        if self.text_edit_modified():    
            if not askyesno('SimpleEditor', 'Text has changed: discard changes?'):
                return
        self.setFileName(None)
        self.clearAllText()
        self.text.edit_reset()                 
        self.text.edit_modified(0)            
        self.knownEncoding = None              

    def onQuit(self):
        assert False, 'onQuit must be defined in window-specific sublass' 

    def text_edit_modified(self):
        return self.text.edit_modified()

    def onUndo(self):                           
        try:                                    
            self.text.edit_undo()              
        except TclError:                       
            showinfo('SimpleEditor', 'Nothing to undo')

    def onRedo(self):                           
        try:
            self.text.edit_redo()
        except TclError:
            showinfo('SimpleEditor', 'Nothing to redo')

    def onCopy(self):                           
        if not self.text.tag_ranges(SEL):       
            showerror('SimpleEditor', '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('SimpleEditor', 'No text selected')
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', '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('SimpleEditor', 'Nothing to paste')
            return
        self.text.insert(INSERT, text)         
        self.text.tag_remove(SEL, '1.0', END)
        self.text.tag_add(SEL, INSERT+'-%dc' % len(text), INSERT)
        self.text.see(INSERT)                  

    def onSelectAll(self):
        self.text.tag_add(SEL, '1.0', END+'-1c')   
        self.text.mark_set(INSERT, '1.0')          
        self.text.see(INSERT)                      

    def onGoto(self, forceline=None):
        line = forceline or askinteger('SimpleEditor', '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('SimpleEditor', 'Bad line number')

    def onFind(self, lastkey=None):
        key = lastkey or askstring('SimpleEditor', 'Enter search string')
        self.text.update()
        self.text.focus()
        self.lastfind = key
        if key:                                                   
            nocase = configs.get('caseinsens', True)          
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:                                          # don't wrap
                showerror('SimpleEditor', '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)
        new.title('SimpleEditor - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0, column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1, column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():                        
            self.onFind(entry1.get())      

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find',  command=onFind ).grid(row=0, column=2, sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1, column=2, sticky=EW)
        new.columnconfigure(1, weight=1)      # expandable entries

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):                      # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)          
            self.text.insert(INSERT, changeto)             # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)                          # goto next appear
            self.text.update()                             # force refresh

    def onGrep(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel()
        popup.title('SimpleEditor - grep')
        var1 = makeFormRow(popup, label='Directory root',   width=18, browse=False)
        var2 = makeFormRow(popup, label='Filename pattern', width=18, browse=False)
        var3 = makeFormRow(popup, label='Search string',    width=18, browse=False)
        var4 = makeFormRow(popup, label='Content encoding', width=18, browse=False)
        var1.set('.')      # current dir
        var2.set('*.py')   # initial values
        var4.set(sys.getdefaultencoding())   
        cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(), var4.get())
        Button(popup, text='Go',command=cb).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey, encoding):
        import threading, queue
        mypopup = Tk()
        mypopup.title('SimpleEditor - grepping')
        status = Label(mypopup, text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close

        # start producer thread, consumer loop
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue)
        threading.Thread(target=self.grepThreadProducer, args=threadargs).start()
        self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding, myqueue):
        from PP4E.Tools.find import find
        matches = []
        try:
            for filepath in find(pattern=filenamepatt, startdir=dirname):
                try:
                    textfile = open(filepath, encoding=encoding)
                    for (linenum, linestr) in enumerate(textfile):
                        if grepkey in linestr:
                            msg = '%s@%d  [%s]' % (filepath, linenum + 1, linestr)
                            matches.append(msg)
                except UnicodeError as X:
                    print('Unicode error in:', filepath, X)       # eg: decode, bom
                except IOError as X:
                    print('IO error in:', filepath, X)            # eg: permission
        finally:
            myqueue.put(matches)      # stop consumer loop on find excs: filenames?

    def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup):
        import queue
        try:
            matches = myqueue.get(block=False)
        except queue.Empty:
            myargs  = (grepkey, encoding, myqueue, mypopup)
            self.after(250, self.grepThreadConsumer, *myargs)
        else:
            mypopup.destroy()     
            self.update()        
            if not matches:
                showinfo('SimpleEditor', 'Grep found no matches for: %r' % grepkey)
            else:
                self.grepMatchesList(matches, grepkey, encoding)

    def grepMatchesList(self, matches, grepkey, encoding):

        from PP4E.Gui.Tour.scrolledlist import ScrolledList
        print('Matches for %s: %s' % (grepkey, len(matches)))
        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):  
                file, line = selection.split('  [', 1)[0].split('@')
                editor = TextEditorMainPopup(
                    loadFirst=file, winTitle=' grep match', loadEncode=encoding)
                editor.onGoto(int(line))
                editor.text.focus_force()   

        popup = Tk()
        popup.title('SimpleEditor - grep matches: %r (%s)' % (grepkey, encoding))
        ScrolledFilenames(parent=popup, options=matches)

    def onFontList(self):
        self.fonts.append(self.fonts[0])          
        del self.fonts[0]                        
        self.text.config(font=self.fonts[0])

    def onColorList(self):
        self.colors.append(self.colors[0])        
        del self.colors[0]                       
        self.text.config(fg=self.colors[0]['fg'], bg=self.colors[0]['bg'])

    def onPickFg(self):
        self.pickColor('fg')                    

    def onPickBg(self):                            
        self.pickColor('bg')                      

    def pickColor(self, part):                     
        (triple, hexstr) = askcolor()
        if hexstr:
            self.text.config(**{part: hexstr})

    def onInfo(self):

        text  = self.getAllText()                  
        bytes = len(text)                        
        lines = len(text.split('\n'))             
        words = len(text.split())                
        index = self.text.index(INSERT)          
        where = tuple(index.split('.'))
        showinfo('SimpleEditor Information',
                 'Current location:\n\n' +
                 'line:\t%s\ncolumn:\t%s\n\n' % where +
                 'File text statistics:\n\n' +
                 'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self, makewindow=True):                  
        if not makewindow:
             new = None                 
        else:
             new = Toplevel()          
        myclass = self.__class__        
        myclass(new)                   
    def onRunCode(self, parallelmode=True):
        def askcmdargs():
            return askstring('SimpleEditor', 'Commandline arguments?') or ''

        from PP4E.launchmodes import System, Start, StartArgs, Fork
        filemode = False
        thefile  = str(self.getFileName())
        if os.path.exists(thefile):
            filemode = askyesno('SimpleEditor', 'Run from file?')
            self.update()                                   
        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() + '\n', namespace)       
        elif self.text_edit_modified():                    
            showerror('SimpleEditor', 'Text changed: you must save before run')
        else:
            cmdargs = askcmdargs()
            mycwd   = os.getcwd()                           # cwd may be root
            dirname, filename = os.path.split(thefile)      # get dir, base
            os.chdir(dirname or mycwd)                      # cd for filenames
            thecmd  = filename + ' ' + cmdargs             
            if not parallelmode:                            # run as file
                System(thecmd, thecmd)()                    # block editor
            else:
                if sys.platform[:3] == 'win':               # spawn in parallel
                    run = StartArgs if cmdargs else Start   
                    run(thecmd, thecmd)()                   # or always Spawn
                else:
                    Fork(thecmd, thecmd)()                  # spawn in parallel
            os.chdir(mycwd)                                 # go back to my dir

    def onPickFont(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('SimpleEditor - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size',   browse=False)
        var3 = makeFormRow(popup, label='Style',  browse=False)
        var1.set('courier')
        var2.set('12')              
        var3.set('bold italic')    
        Button(popup, text='Apply', command=
               lambda: self.onDoFont(var1.get(), var2.get(), var3.get())).pack()

    def onDoFont(self, family, size, style):
        try:  
            self.text.config(font=(family, int(size), style))
        except:
            showerror('SimpleEditor', 'Bad font specification')
    def isEmpty(self):
        return not self.getAllText()

    def getAllText(self):
        return self.text.get('1.0', END+'-1c') 
    def setAllText(self, text):
        self.text.delete('1.0', END)              
        self.text.insert(END, text)               
        self.text.mark_set(INSERT, '1.0')       
        self.text.see(INSERT)                    
    def clearAllText(self):
        self.text.delete('1.0', END)              

    def getFileName(self):
        return self.currfile
    def setFileName(self, name):                  # see also: onGoto(linenum)
        self.currfile = name  # for save
        self.filelabel.config(text=str(name))

    def setKnownEncoding(self, encoding='utf-8'):
        self.knownEncoding = encoding          

    def setBg(self, color):
        self.text.config(bg=color)                # to set manually from code
    def setFg(self, color):
        self.text.config(fg=color)              
    def setFont(self, font):
        self.text.config(font=font)               # ('family', size, 'style')

    def setHeight(self, lines):                   # default = 24h x 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 SimpleEditor', helptext % ((Version,)*2))
Example #9
0
        def __init__(self, master, cid_path=None, data_path=None, config=dict(), **keywords):
            """
            Set up a frame with widgets to validate ``id_path`` and ``data_path``.

            :param master: Tk master or root in which the frame should show up
            :param cid_path: optional preset for :guilabel:`CID` widget
            :type cid_path: str or None
            :param data_path: optional preset for :guilabel:`Data` widget
            :type data_path: str or None
            :param config: Tk configuration
            :param keywords: Tk keywords
            """
            assert has_tk
            assert master is not None

            if six.PY2:
                # In Python 2, Frame is an old style class.
                Frame.__init__(self, master, config, **keywords)
            else:
                super().__init__(master, config, **keywords)

            self._master = master

            # Define basic layout.
            self.grid(padx=_PADDING, pady=_PADDING)
            # self.grid_columnconfigure(1, weight=1)
            self.grid_rowconfigure(_VALIDATION_REPORT_ROW, weight=1)

            # Choose CID.
            self._cid_label = Label(self, text='CID:')
            self._cid_label.grid(row=_CID_ROW, column=0, sticky=E)
            self._cid_path_entry = Entry(self, width=55)
            self._cid_path_entry.grid(row=_CID_ROW, column=1, sticky=E + W)
            self._choose_cid_button = Button(self, command=self.choose_cid, text='Choose...')
            self._choose_cid_button.grid(row=_CID_ROW, column=2)
            self.cid_path = cid_path

            # Choose data.
            self._data_label = Label(self, text='Data:')
            self._data_label.grid(row=_DATA_ROW, column=0, sticky=E)
            self._data_path_entry = Entry(self, width=55)
            self._data_path_entry.grid(row=_DATA_ROW, column=1, sticky=E + W)
            self._choose_data_button = Button(self, command=self.choose_data, text='Choose...')
            self._choose_data_button.grid(row=_DATA_ROW, column=2)
            self.data_path = data_path

            # Validate.
            self._validate_button = Button(self, command=self.validate, text='Validate')
            self._validate_button.grid(row=_VALIDATE_BUTTON_ROW, column=0, padx=_PADDING, pady=_PADDING)

            # Validation status text.
            self._validation_status_text = StringVar()
            validation_status_label = Label(self, textvariable=self._validation_status_text)
            validation_status_label.grid(row=_VALIDATE_BUTTON_ROW, column=1)

            # Validation result.
            validation_report_frame = LabelFrame(self, text='Validation report')
            validation_report_frame.grid(row=_VALIDATION_REPORT_ROW, columnspan=3, sticky=E + N + S + W)
            validation_report_frame.grid_columnconfigure(0, weight=1)
            validation_report_frame.grid_rowconfigure(0, weight=1)
            self._validation_report_text = Text(validation_report_frame)
            self._validation_report_text.grid(column=0, row=0, sticky=E + N + S)
            _validation_report_scrollbar = Scrollbar(validation_report_frame)
            _validation_report_scrollbar.grid(column=1, row=0, sticky=N + S + W)
            _validation_report_scrollbar.config(command=self._validation_report_text.yview)
            self._validation_report_text.config(yscrollcommand=_validation_report_scrollbar.set)

            # Set up file dialogs.
            self._choose_cid_dialog = Open(
                initialfile=self.cid_path,
                title='Choose CID',
            )
            self._choose_data_dialog = Open(
                initialfile=self.data_path,
                title='Choose data',
            )
            self._save_log_as_dialog = SaveAs(
                defaultextension='.log',
                initialfile='cutplace.log',
                title='Save validation result',
            )

            menubar = Menu(master)
            master.config(menu=menubar)
            self._file_menu = Menu(menubar, tearoff=False)
            self._file_menu.add_command(command=self.choose_cid, label='Choose CID...')
            self._file_menu.add_command(command=self.choose_data, label='Choose data...')
            self._file_menu.add_command(command=self.save_validation_report_as, label='Save validation report as...')
            self._file_menu.add_command(command=self.quit, label='Quit')
            menubar.add_cascade(label='File', menu=self._file_menu)
            help_menu = Menu(menubar, tearoff=False)
            help_menu.add_command(command=self.show_about, label='About')
            menubar.add_cascade(label='Help', menu=help_menu)

            self._enable_usable_widgets()
Example #10
0
class TextEditor:
    startfiledir = '.'
    editwindows = []

    if __name__ == '__main__':
        from textConfig import (opensAskUser, opensEncoding,
                                savesUseKnownEncoding, savesAskUser,
                                savesEncoding)
    else:
        from .textConfig import (opensAskUser, opensEncoding,
                                 savesUseKnownEncoding, savesAskUser,
                                 savesEncoding)

    ftypes = [('All files', '*'), ('Text files', '.txt'),
              ('Python files', '.py')]

    colors = [{
        'fg': 'black',
        'bg': 'white'
    }, {
        'fg': 'yellow',
        'bg': 'black'
    }, {
        'fg': 'white',
        'bg': 'blue'
    }, {
        'fg': 'black',
        'bg': 'beige'
    }, {
        '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'),
             ('courier', 12 + FontScale, 'normal'),
             ('courier', 10 + FontScale, 'bold'),
             ('courier', 10 + FontScale, 'italic'),
             ('times', 10 + FontScale, 'normal'),
             ('helvetica', 10 + FontScale, 'normal'),
             ('ariel', 10 + FontScale, 'normal'),
             ('system', 10 + FontScale, 'normal'),
             ('courier', 20 + FontScale, 'normal')]

    def __init__(self, loadFirst='', loadEncode=''):
        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.knownEncoding = None
        self.text.focus()
        if loadFirst:
            self.update()
            self.onOpen(loadFirst, loadEncode)

    def start(self):
        self.menuBar = [('File', 0, [('Open...', 0, self.onOpen),
                                     ('Save', 0, self.onSave),
                                     ('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),
                                       ('Grep...', 3, self.onGrep)]),
                        ('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):
        name = Label(self, bg='black', fg='white')
        name.pack(side=TOP, fill=X)

        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='none')
        text.config(undo=1, autoseparators=1)

        vbar.pack(side=RIGHT, fill=Y)
        hbar.pack(side=BOTTOM, fill=X)
        text.pack(side=TOP, fill=BOTH, expand=YES)

        text.config(yscrollcommand=vbar.set)
        text.config(xscrollcommand=hbar.set)
        vbar.config(command=text.yview)
        hbar.config(command=text.xview)
        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

    def my_askopenfilename(self):
        if not self.openDialog:
            self.openDialog = Open(initialdir=self.startfiledir,
                                   filetypes=self.ftypes)
        return self.openDialog.show()

    def my_asksaveasfilename(self):
        if not self.saveDialog:
            self.saveDialog = SaveAs(initialdir=self.startfiledir,
                                     filetypes=self.ftypes)
        return self.saveDialog.show()

    def onOpen(self, loadFirst='', loadEncode=''):

        if self.text_edit_modified():
            if not askyesno('SimpleEditor',
                            'Text has changed: discard changes?'):
                return

        file = loadFirst or self.my_askopenfilename()
        if not file:
            return

        if not os.path.isfile(file):
            showerror('SimpleEditor', 'Could not open file ' + file)
            return
        text = None
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):
                pass

        # try user input, prefill with next choice as default
        if text == None and self.opensAskUser:
            self.update()
            askuser = askstring('SimpleEditor',
                                'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass
        if text == None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass

        if text == None:
            try:
                text = open(file, 'r',
                            encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass
        if text == None:
            try:
                text = open(file, 'rb').read()
                text = text.replace(b'\r\n', b'\n')  # for display, saves
                self.knownEncoding = None
            except IOError:
                pass

        if text == None:
            showerror('SimpleEditor', 'Could not decode and open file ' + file)
        else:
            self.setAllText(text)
            self.setFileName(file)
            self.text.edit_reset()
            self.text.edit_modified(0)

    def onSave(self):
        self.onSaveAs(self.currfile)  # may be None

    def onSaveAs(self, forcefile=None):

        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return

        text = self.getAllText()
        encpick = None  # even if read/inserted as bytes

        # try known encoding at latest Open or Save, if any
        if self.knownEncoding and (  # enc known?
            (forcefile and self.savesUseKnownEncoding >= 1) or  # on Save?
            (not forcefile and self.savesUseKnownEncoding >= 2)):  # on SaveAs?
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass

        if not encpick and self.savesAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('SimpleEditor',
                                'Enter Unicode encoding for save',
                                initialvalue=(self.knownEncoding
                                              or self.savesEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            if askuser:
                try:
                    text.encode(askuser)
                    encpick = askuser
                except (UnicodeError, LookupError):  # LookupError:  bad name
                    pass
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass

        # try platform default (utf8 on windows)
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass
        if not encpick:
            showerror('SimpleEditor', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('SimpleEditor', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)
                self.text.edit_modified(0)
                self.knownEncoding = encpick

    def onNew(self):
        if self.text_edit_modified():
            if not askyesno('SimpleEditor',
                            'Text has changed: discard changes?'):
                return
        self.setFileName(None)
        self.clearAllText()
        self.text.edit_reset()
        self.text.edit_modified(0)
        self.knownEncoding = None  #

    def onQuit(self):
        assert False, 'onQuit must be defined in window-specific sublass'

    def text_edit_modified(self):
        return self.text.edit_modified()

    def onUndo(self):
        try:
            self.text.edit_undo()  # exception if stacks empty
        except TclError:  # menu tear-offs for quick undo
            showinfo('SimpleEditor', 'Nothing to undo')

    def onRedo(self):
        try:
            self.text.edit_redo()
        except TclError:
            showinfo('SimpleEditor', 'Nothing to redo')

    def onCopy(self):  # get text selected by mouse, etc.
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', '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('SimpleEditor', 'No text selected')
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', '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('SimpleEditor', '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

    def onGoto(self, forceline=None):
        line = forceline or askinteger('SimpleEditor', '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('SimpleEditor', 'Bad line number')

    def onFind(self, lastkey=None):
        key = lastkey or askstring('SimpleEditor', 'Enter search string')
        self.text.update()
        self.text.focus()
        self.lastfind = key
        if key:
            nocase = configs.get('caseinsens', True)
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:
                showerror('SimpleEditor', 'String not found')
            else:
                pastkey = where + '+%dc' % len(key)
                self.text.tag_remove(SEL, '1.0', END)
                self.text.tag_add(SEL, where, pastkey)
                self.text.mark_set(INSERT, pastkey)
                self.text.see(where)

    def onRefind(self):
        self.onFind(self.lastfind)

    def onChange(self):

        new = Toplevel(self)
        new.title('SimpleEditor - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0,
                                                                   column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1,
                                                                   column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():  # use my entry in enclosing scope
            self.onFind(entry1.get())  # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0,
                                                      column=2,
                                                      sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1,
                                                        column=2,
                                                        sticky=EW)
        new.columnconfigure(1, weight=1)  # expandable entries

    def onDoChange(self, findtext, changeto):
        if self.text.tag_ranges(SEL):
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)
            self.text.see(INSERT)
            self.onFind(findtext)
            self.text.update()

    def onGrep(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel()
        popup.title('SimpleEditor - grep')
        var1 = makeFormRow(popup,
                           label='Directory root',
                           width=18,
                           browse=False)
        var2 = makeFormRow(popup,
                           label='Filename pattern',
                           width=18,
                           browse=False)
        var3 = makeFormRow(popup,
                           label='Search string',
                           width=18,
                           browse=False)
        var1.set('.')
        var2.set('*.py')
        Button(popup,
               text='Go',
               command=lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey):
        import threading, queue
        mypopup = Tk()
        mypopup.title('SimpleEditor - grepping')
        status = Label(mypopup,
                       text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, myqueue)
        threading.Thread(target=self.grepThreadProducer,
                         args=threadargs).start()
        self.grepThreadConsumer(grepkey, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, myqueue):
        from PP4E.Tools.find import find
        matches = []
        for filepath in find(pattern=filenamepatt, startdir=dirname):
            try:
                for (linenum, linestr) in enumerate(open(filepath)):
                    if grepkey in linestr:
                        message = '%s@%d  [%s]' % (filepath, linenum + 1,
                                                   linestr)
                        matches.append(message)
            except UnicodeDecodeError:
                print('Unicode error in:', filepath)
        myqueue.put(matches)

    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 x 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 SimpleEditor', helptext % ((Version, ) * 2))
Example #11
0
    def __init__(self,
                 master,
                 cid_path=None,
                 data_path=None,
                 config=dict(),
                 **keywords):
        assert has_tk
        if six.PY2:
            Frame.__init__(self, master, config, **keywords)
        else:
            super().__init__(master, config, **keywords)

        # Define basic layout.
        self.grid(padx=_PADDING, pady=_PADDING)
        # self.grid_columnconfigure(1, weight=1)
        self.grid_rowconfigure(_VALIDATION_RESULT_ROW, weight=1)

        # Choose CID.
        self._cid_label = Label(self, text='CID:')
        self._cid_label.grid(row=_CID_ROW, column=0, sticky=E)
        self._cid_path_entry = Entry(self, width=55)
        self._cid_path_entry.grid(row=_CID_ROW, column=1, sticky=E + W)
        self._choose_cid_button = Button(self,
                                         command=self.choose_cid,
                                         text='Choose...')
        self._choose_cid_button.grid(row=_CID_ROW, column=2)
        self.cid_path = cid_path

        # Choose data.
        self._data_label = Label(self, text='Data:')
        self._data_label.grid(row=_DATA_ROW, column=0, sticky=E)
        self._data_path_entry = Entry(self, width=55)
        self._data_path_entry.grid(row=_DATA_ROW, column=1, sticky=E + W)
        self._choose_data_button = Button(self,
                                          command=self.choose_data,
                                          text='Choose...')
        self._choose_data_button.grid(row=_DATA_ROW, column=2)
        self.data_path = data_path

        # Validate.
        self._validate_button = Button(self,
                                       command=self.validate,
                                       text='Validate')
        self._validate_button.grid(row=_VALIDATE_BUTTON_ROW,
                                   column=0,
                                   padx=_PADDING,
                                   pady=_PADDING)

        # Validation status text.
        self._validation_status_text = StringVar()
        validation_status_label = Label(
            self, textvariable=self._validation_status_text)
        validation_status_label.grid(row=_VALIDATE_BUTTON_ROW, column=1)

        # Validation result.
        validation_result_frame = LabelFrame(self, text='Validation result')
        validation_result_frame.grid(row=_VALIDATION_RESULT_ROW,
                                     columnspan=3,
                                     sticky=E + N + S + W)
        validation_result_frame.grid_columnconfigure(0, weight=1)
        validation_result_frame.grid_rowconfigure(0, weight=1)
        self._validation_result_text = Text(validation_result_frame)
        self._validation_result_text.grid(column=0, row=0, sticky=E + N + S)
        _validation_result_scrollbar = Scrollbar(validation_result_frame)
        _validation_result_scrollbar.grid(column=1, row=0, sticky=N + S + W)
        _validation_result_scrollbar.config(
            command=self._validation_result_text.yview)
        self._validation_result_text.config(
            yscrollcommand=_validation_result_scrollbar.set)

        # "Save validation result as" button.
        self._save_log_button = Button(self,
                                       command=self.save_log_as,
                                       text='Save validation result as...')
        self._save_log_button.grid(row=_SAVE_ROW,
                                   column=1,
                                   columnspan=2,
                                   sticky=E + S)

        # Set up file dialogs.
        self._choose_cid_dialog = Open(
            initialfile=self.cid_path,
            title='Choose CID',
        )
        self._choose_data_dialog = Open(
            initialfile=self.data_path,
            title='Choose data',
        )
        self._save_log_as_dialog = SaveAs(
            defaultextension='.log',
            initialfile='cutplace.log',
            title='Save validation result',
        )

        self.enable_usable_widgets()
Example #12
0
class TextEditor:  # mix with menu/toolbar Frame class
    startfiledir = '.'  # for dialogs
    editwindows = []  # for process-wide quit check

    # Unicode configurations
    # imported in class to allow overrides in subclass or self
    if __name__ == '__main__':
        from textConfig import (opensAskUser, opensEncoding,
                                savesUseKnownEncoding, savesAskUser,
                                savesEncoding)
    else:
        from textConfig import (opensAskUser, opensEncoding,
                                savesUseKnownEncoding, savesAskUser,
                                savesEncoding)

    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='', loadEncode=''):
        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.knownEncoding = None
        self.text.focus()
        if loadFirst:
            self.update()
            self.onOpen(loadFirst, loadEncode)

    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),
                           ('Grep...', 3, self.onGrep)]),
            ('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
        # GuiMaker frame packs itself
        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='none')  # disable line wrapping
        text.config(undo=1, autoseparators=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='', loadEncode=''):
        if self.text_edit_modified():
            if not askyesno('PyEdit', 'Text has changed: discard changes?'):
                return
        file = loadFirst or self.my_askopenfilename()
        if not file:
            return
        if not os.path.isfile(file):
            showerror('PyEdit', 'Could not open file ' + file)
            return

        # try known encoding if passed and accurate (e.g., email)
        text = None  # empty file = '' = False: test for None!
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):  # lookup: bad name
                pass

        # try user input, prefill with next choice as default
        if text == None and self.opensAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('PyEdit',
                                'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
        if askuser:
            try:
                text = open(file, 'r', encoding=askuser).read()
                self.knownEncoding = askuser
            except (UnicodeError, LookupError, IOError):
                pass

        # try config file (or before ask user?)
        if text == None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass

        # try platform default (utf-8 on windows; try utf8 always?)
        if text == None:
            try:
                text = open(file, 'r',
                            encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass

        # last resort: use binary bytes and rely on Tk to decode
        if text == None:
            try:
                text = open(file, 'rb').read()  # bytes for Unicode
                text = text.replace(b'\r\n', b'\n')  # for display, saves
                self.knownEncoding = None
            except IOError:
                pass

        if text == None:
            showerror('PyEdit', 'Could not decode and 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)

    def onSave(self):
        self.onSaveAs(self.currfile)  # may be None

    def onSaveAs(self, forcefile=None):
        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return
        text = self.getAllText()
        encpick = None  # even if read/inserted as bytes
        # try known encoding at latest Open or Save, if any
        if self.knownEncoding and (  # enc known?
            (forcefile and self.savesUseKnownEncoding >= 1) or  # on Save?
            (not forcefile and self.savesUseKnownEncoding >= 2)):  # on SaveAs?
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass
        # try user input, prefill with known type, else next choice
        if not encpick and self.savesAskUser:
            self.update()  # else dialog doesn't appear in rare cases
        askuser = askstring('PyEdit',
                            'Enter Unicode encoding for save',
                            initialvalue=(self.knownEncoding
                                          or self.savesEncoding
                                          or sys.getdefaultencoding() or ''))
        if askuser:
            try:
                text.encode(askuser)
                encpick = askuser
            except (UnicodeError, LookupError):  # LookupError: bad name
                pass  # UnicodeError: can't encode
        # try config file
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass
        # try platform default (utf8 on windows)
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass
        # open in text mode for endlines + encoding
        if not encpick:
            showerror('PyEdit', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('PyEdit', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)  # may be newly created
                self.text.edit_modified(0)  # 2.0: clear modified flag
                self.knownEncoding = encpick  # 2.1: keep enc for next save
                # don't clear undo/redo stks!

    def onNew(self):
        if self.text_edit_modified():
            if not askyesno('PyEdit', 'Text has changed: discard changes?'):
                return
        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
        self.knownEncoding = None  # 2.1: Unicode type unknown

    def onQuit(self):
        assert False, 'onQuit must be defined in window-specific sublass'

    def text_edit_modified(self):
        return self.text.edit_modified()

    ############################################################################
    # Edit menu commands
    ############################################################################
    def onUndo(self):
        try:
            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:
            nocase = configs.get('caseinsens', True)
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:
                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)
        new.title('PyEdit - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0,
                                                                   column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1,
                                                                   column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():  # use my entry in enclosing scope
            self.onFind(entry1.get())  # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0,
                                                      column=2,
                                                      sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1,
                                                        column=2,
                                                        sticky=EW)
        new.columnconfigure(1, weight=1)

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):  # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)  # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)  # goto next appear
            self.text.update()  # force refresh

    def onGrep(self):
        from formrows import makeFormRow
        popup = Toplevel()
        popup.title('PyEdit - grep')
        var1 = makeFormRow(popup,
                           label='Directory root',
                           width=18,
                           browse=False)
        var2 = makeFormRow(popup,
                           label='Filename pattern',
                           width=18,
                           browse=False)
        var3 = makeFormRow(popup,
                           label='Search string',
                           width=18,
                           browse=False)
        var4 = makeFormRow(popup,
                           label='Content encoding',
                           width=18,
                           browse=False)
        var1.set('.')  # current dir
        var2.set('*.py')  # initial values
        var4.set(sys.getdefaultencoding())  # for file content, not filenames
        cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(),
                                   var4.get())
        Button(popup, text='Go', command=cb).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey, encoding):
        import threading, queue

        # make non-modal un-closeable dialog
        mypopup = Tk()
        mypopup.title('PyEdit - grepping')
        status = Label(mypopup,
                       text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close
        # start producer thread, consumer loop
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue)
        threading.Thread(target=self.grepThreadProducer,
                         args=threadargs).start()
        self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding,
                           myqueue):
        from find import find
        matches = []
        try:
            for filepath in find(pattern=filenamepatt, startdir=dirname):
                try:
                    textfile = open(filepath, encoding=encoding)
                    for (linenum, linestr) in enumerate(textfile):
                        if grepkey in linestr:
                            msg = '%s@%d [%s]' % (filepath, linenum + 1,
                                                  linestr)
                            matches.append(msg)
                except UnicodeError as X:
                    print('Unicode error in:', filepath, X)  # eg: decode, bom
                except IOError as X:
                    print('IO error in:', filepath, X)  # eg: permission
        finally:
            myqueue.put(matches)  # stop consumer loop on find excs: filenames?

    def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup):
        import queue
        try:
            matches = myqueue.get(block=False)
        except queue.Empty:
            myargs = (grepkey, encoding, myqueue, mypopup)
            self.after(250, self.grepThreadConsumer, *myargs)
        else:
            mypopup.destroy()  # close status
            self.update()  # erase it now
        if not matches:
            showinfo('PyEdit', 'Grep found no matches for: %r' % grepkey)
        else:
            self.grepMatchesList(matches, grepkey, encoding)

    def grepMatchesList(self, matches, grepkey, encoding):
        from scrolledlist import ScrolledList
        print('Matches for %s: %s' % (grepkey, len(matches)))

        # catch list double-click
        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):
                file, line = selection.split(' [', 1)[0].split('@')
                editor = TextEditorMainPopup(loadFirst=file,
                                             winTitle=' grep match',
                                             loadEncode=encoding)
                editor.onGoto(int(line))
                editor.text.focus_force()

        # new non-modal widnow
        popup = Tk()
        popup.title('PyEdit - grep matches: %r (%s)' % (grepkey, encoding))
        ScrolledFilenames(parent=popup, options=matches)

    ############################################################################
    # 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('\n'))  # any separated by whitespace
        words = len(text.split())  # 3.x: bytes is really chars
        index = self.text.index(INSERT)  # str is unicode code points
        where = tuple(index.split('.'))
        showinfo(
            'PyEdit Information',
            'Current location:\n\n' + 'line:\t%s\ncolumn:\t%s\n\n' % where +
            'File text statistics:\n\n' +
            'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self, makewindow=True):
        if not makewindow:
            new = None  # assume class makes its own window
        else:
            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):
        def askcmdargs():
            return askstring('PyEdit', 'Commandline arguments?') or ''

        from launchmodes import System, Start, StartArgs, Fork
        filemode = False
        thefile = str(self.getFileName())
        if os.path.exists(thefile):
            filemode = askyesno('PyEdit', 'Run from file?')
            self.update()  # 2.1: run update()
            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() + '\n', namespace)  # exceptions ignored
            elif self.text_edit_modified():  # 2.0: changed test
                showerror('PyEdit', 'Text changed: you must save before run')
        else:
            cmdargs = askcmdargs()
            mycwd = os.getcwd()  # cwd may be root
            dirname, filename = os.path.split(thefile)  # get dir, base
            os.chdir(dirname or mycwd)  # cd for filenames
            thecmd = filename + ' ' + cmdargs  # 2.1: not theFile
            if not parallelmode:  # run as file
                System(thecmd, thecmd)()  # block editor
            else:
                if sys.platform[:3] == 'win':  # spawn in parallel
                    run = StartArgs if cmdargs else Start  # 2.1: support args
                    run(thecmd, thecmd)()  # or always Spawn
                else:
                    Fork(thecmd, thecmd)()
            os.chdir(mycwd)

    def onPickFont(self):
        from formrows import makeFormRow

        popup = Toplevel(self)
        popup.title('PyEdit - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var1.set('courier')
        var2.set('12')  # suggested vals
        var3.set('bold italic')  # see pick list for valid inputs
        Button(popup,
               text='Apply',
               command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        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 str string

    def setAllText(self, text):
        self.text.delete('1.0', END)  # store text string in widget
        self.text.insert(END, text)  # or '1.0'; text=bytes or str
        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):  # see also: onGoto(linenum)
        self.currfile = name  # for save
        self.filelabel.config(text=str(name))

    def setKnownEncoding(self, encoding='utf-8'):  # 2.1: for saves if inserted
        self.knownEncoding = encoding  # else saves use config, ask?

    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 x 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)
Example #13
0
class TextEditor:  # 和菜单/工具栏混合成 Frame 类
    startfiledir = '.'  # 给对话框
    editwindows = []  # 给全过程进行退出检查

    # Unicode 配置
    # 导入到类当中,以便允许子类或自身重写
    if __name__ == '__main__':
        from textConfig import (opensAskUser, opensEncoding,
                                savesUseKnownEncoding, savesAskUser,
                                savesEncoding)
    else:
        from .textConfig import (opensAskUser, opensEncoding,
                                 savesUseKnownEncoding, savesAskUser,
                                 savesEncoding)

    ftypes = [
        ('All files', '*'),  # 用于文件打开对话框
        ('Text files', '.txt'),  # 自定义子类
        ('Python files', '.py')
    ]  # 或在每个实例中设置

    colors = [
        {
            'fg': 'black',
            'bg': 'white'
        },  # 颜色选择列表
        {
            'fg': 'yellow',
            'bg': 'black'
        },  # 第一项是默认的
        {
            'fg': 'white',
            'bg': 'blue'
        },  # 根据需要进行调整
        {
            'fg': 'black',
            'bg': 'beige'
        },  # 或开启 PickBg/Fg 选择器
        {
            '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'),  # 与平台无关的字体
        ('courier', 12 + FontScale, 'normal'),  # (字族、大小、字体)
        ('courier', 10 + FontScale, 'bold'),  # 或弹出一个列表框
        ('courier', 10 + FontScale, 'italic'),  # Linux 系统上变得更大
        ('times', 10 + FontScale, 'normal'),  # 给 2 使用 "粗斜体"
        ('helvetica', 10 + FontScale, 'normal'),  # 也有 "下划线" 等
        ('ariel', 10 + FontScale, 'normal'),
        ('system', 10 + FontScale, 'normal'),
        ('courier', 20 + FontScale, 'normal')
    ]

    def __init__(self, loadFirst='', loadEncode=''):
        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.knownEncoding = None  # 2.1 Unicode: 直到打开或保存
        self.text.focus()  # 否则必须单击文本
        if loadFirst:
            self.update()  # 2.1: 其它@行2
            self.onOpen(loadFirst, loadEncode)

    def start(self):  # 由GuiMaker.__init__运行
        self.menuBar = [
            (
                'File',
                0,
                [
                    ('Open...', 0, self.onOpen),  # 配置菜单/工具栏
                    ('Save', 0, self.onSave),  # 一个GuiMaker菜单def树
                    ('Save As...', 0, 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', 0, 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),
                           ('Grep...', 0, self.onGrep)]),
            ('Tools', 0, [('Pick Font...', 0, self.onPickFont),
                          ('Font List', 0, self.onFontList), 'separator',
                          ('Pick Bg...', 0, self.onPickBg),
                          ('Pick Fg...', 0, self.onPickFg),
                          ('Color List', 0, self.onColorList), 'separator',
                          ('Info...', 0, self.onInfo),
                          ('Clone', 0, 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):  # 由GuiMaker.__init__运行
        name = Label(self, bg='black', fg='white')  # 在下面添加菜单,在上面添加工具
        name.pack(side=TOP, fill=X)  # 封装菜单/工具栏
        vbar = Scrollbar(self)  # GuiMaker框架自身就封装好了
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='none')  # 禁用自动换行
        text.config(undo=1, autoseparators=1)  # 默认为0, 1

        vbar.pack(side=RIGHT, fill=Y)
        hbar.pack(side=BOTTOM, fill=X)  # 最后封装文本
        text.pack(side=TOP, fill=BOTH, expand=YES)  # 否则修剪scrollbars
        text.config(yscrollcommand=vbar.set)  # 文本移动时调用vbar.set
        text.config(xscrollcommand=hbar.set)
        vbar.config(command=text.yview)  # 滚动式调用text.yview
        hbar.config(command=text.xview)  # 或用hbar['command']=text.xview代码

        # 运用用户的配置或默认值
        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

    ###################################################################################
    # 文件菜单命令
    ###################################################################################

    def my_askopenfilename(self):  # 对象记住最后结果目录/文件
        if not self.openDialog:
            self.openDialog = Open(initialdir=self.startfiledir,
                                   filetypes=self.ftypes)
        return self.openDialog.show()

    def my_asksaveasfilename(self):  # 对象记住最后结果目录/文件
        if not self.saveDialog:
            self.saveDialog = SaveAs(initialdir=self.startfiledir,
                                     filetypes=self.ftypes)
        return self.saveDialog.show()

    def onOpen(self, loadFirst='', loadEncode=''):
        """
        随着编码传入,文本模式下开启,以textconfig或平台默认值从用户端输入,
        或最后以任意Unicode编码来打开,并终止Windows最后一行上的\r,条件就是
        文本正常显示,读取的内容作为str返回,因此保存时需要编码:保存在此使用
        过的编码。

        提早测试一下文件是否正常以试着避免打开,也可以加载和手动将字节解码为字
        符串,以避免打开多个的尝试,但这样不太可能试用所有情况。

        编码行为在本地的textConfig.py中时可配置的:
        1) 如果由客户端(邮件字符集)传递则优先尝试已知的类型
        2) 如果opensAskUser为真,下一步尝试用户输入(由默认值填充)
        3) 如果opensEncoding非空,下一步尝试这些编码: "latin-1", "cp500" 等
        4) 下一步尝试平台默认的 sys.getdefaultencoding()
        5) 最后使用二进制模式的字节和Tk策略
        """
        if self.text_edit_modified():
            if not askyesno('PyEdit', 'Text has changed: discard changes?'):
                return

        file = loadFirst or self.my_askopenfilename()
        if not file:
            return
        if not os.path.isfile(file):
            showerror('PyEdit', 'Could not open file ' + file)
            return

        # 如果传递过来了且准确无误则尝试已知编码(例如邮件)
        text = None  # 空的文件 = '' = 假: 测试 None!
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):  # 检查: 坏的名字
                pass

        # 尝试用户输入,预设下一个选择为默认值
        if text is None and self.opensAskUser:
            self.update()  # 否则在少有的情况下对话框不会出现
            askuser = askstring('PyEdit',
                                'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass

        # 尝试配置文件(或者在询问用户之前?)
        if text is None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass

        # 尝试平台默认值(窗口中是utf-8,一直尝试utf-8?)
        if text is None:
            try:
                text = open(file, 'r',
                            encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass

        # 最后一步:使用二进制字节并依赖Tk去解码
        if text is None:
            try:
                text = open(file, 'rb').read()  # 用于Unicode的字节
                text = text.replace(b'\r\n', b'\n')  # 用于显示,保存
                self.knownEncoding = None
            except IOError:
                pass

        if text is None:
            showerror('PyEdit', 'Could not decode and open file ' + file)
        else:
            self.setAllText(text)
            self.setFileName(file)
            self.text.edit_reset()  # 清除撤销/重做 stks
            self.text.edit_modified(0)  # 清除修改标志

    def onSave(self):
        self.onSaveAs(self.currfile)  # 或许为None

    def onSaveAs(self, forcefile=None):
        """
        文本内容中是作为一个字符串返回,因此我们必须处理编码,以便保存到这里的文件,
        而不管输出文件的打开模式(二进制需要字节,而文本必须进行编码),打开或保存(如
        果已经知晓)时,尝试使用过的编码,用户输入,配置文件设置,最后尝试平台默认值,
        大多数用户都可以使用平台默认值。

        这里保留了成功的编码名以便下次保持,因为这可能是 "新建" 后第一次 "保存" 或是
        手动的文本插入;"保存" 和 "另存为" 可能每个配置文件都使用最后一个已知的编码(它
        可能应该被用于 "保存",但 "另存为" 的用法尚不清楚);如果有图形用户界面的提示,
        将其预设为已知的编码。

        手动 text.encode() 来避免创建文件,文本模式的文件执行平台特殊尾行转换:如果打开
        即呈现文本模式(自动)和二进制模式(手动),那么放弃 Windows \r;如果手动插入内容,
        就必须删除 \r,否则会在此进行复制;如果以二进制打开,则在第一次 "打开" 或 "保存"
        前,"新建" 后将 knownEncoding 置为空(knownEncoding=None)。

        在本地的 textConfig.py中,编码行为是可配置的:
        1) 如果 savesUseKnownEncoding > 0,尝试最后打开或保存的编码
        2) 如果 savesAsUser 为真,下一步尝试用户输入(用已知的?来预先填充)
        3) 如果 savesEncoding 为空,下一步尝试此编码: "utf-8" 等
        4) 最后尝试 sys.getdefaultencoding()
        """
        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return
        text = self.getAllText()  # 一个有着\n的str字符串
        encpick = None  # 即便按字节读取/插入

        # 尝试使用最近打开或保存的编码
        if self.knownEncoding and (  # 已知编码?
            (forcefile and self.savesUseKnownEncoding >= 1) or  # 在 "保存" 时?
            (not forcefile and self.savesUseKnownEncoding >= 2)):  # 在 "另存为" 时?
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass

        # 尝试用户输入,预设为已知的类型,否则下一个选择
        if not encpick and self.savesAskUser:
            self.update()
            askuser = askstring('PyEdit',
                                'Enter Unicode encoding for save',
                                initialvalue=(self.knownEncoding
                                              or self.savesEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            if askuser:
                try:
                    text.encode(askuser)
                    encpick = askuser
                except (UnicodeError, LookupError):  # LookupError: bad name
                    pass  # UnicodeError: can't encode

        # 尝试配置文件
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass

        # 尝试默认平台(窗口上是utf-8)
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass

        # 文本模式下打开端线 + 编码
        if not encpick:
            showerror('PyEdit', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('PyEdit', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)  # 或许是新创建的
                self.text.edit_modified(0)  # 清除修改标志
                self.knownEncoding = encpick  # 为下次保存保留编码
                # 不要清除撤销/重做 stks!

    def onNew(self):
        """
        在当前窗口从头开始编辑一个新文件,
        请参见 onClone 以弹出一个新的独立的编辑窗口。
        """
        if self.text_edit_modified():
            if not askyesno('PyEdit', 'Text has changed: discard changes?'):
                return self.setFileName(None)
        self.clearAllText()  # 清除撤销/重做 stks
        self.text.edit_reset()  # 清除修改标志
        self.knownEncoding = None  # Unicode 类型未知

    def onQuit(self):
        """
        "退出" 菜单/工具栏上选择并在顶层窗口中 WM 边界的 X 按钮;
        如果其它东西有改动,不退出应用程序;如果自身没有改动则不需询问;最后移至
        顶层窗口类,因为用法各有不同:GUI中的一个 "退出" 可能是使用quit()函数来退
        出,destroy()只有一个顶层,Tk,或编辑框架,作为一个附加组件来运行时或许根
        本没有提供,改进后进行检查,如果使用quit(),主窗口应该检查同在处理列表中的
        其它窗口,以便查看它们是否也有改动。
        """
        assert False, 'onQuit must be defined in window-specific subclass'

    def text_edit_modified(self):
        """
        现在这个正运行!在tkinter中似乎一直有个bool结果类型;
        """
        # 返回 self.tk.call((self.text._w, 'edit') + ('modified', None))
        return self.text.edit_modified()

    ####################################################################################
    # 编辑菜单命令
    ####################################################################################

    def onUndo(self):
        try:  # tk8.4 保留撤销/重做堆栈
            self.text.edit_undo()  # 堆栈为空则抛出异常
        except TclError:  # 可分离的菜单供快速撤销使用
            showinfo('PyEdit', 'Nothing to undo')

    def onRedo(self):
        try:
            self.text.edit_redo()
        except TclError:
            showinfo('PyEdit', 'Nothing to redo')

    def onCopy(self):  # 使用鼠标选中文本等
        if not self.text.tag_ranges(SEL):  # 保存进跨应用程序的剪贴板
            showerror('PyEdit', 'No text selected')
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

    def onDelete(self):  # 删除选中的文本,不保存
        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()  # 保存并删除选中的文本
            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)  # 增加当前嵌入的光标
        self.text.tag_remove(SEL, '1.0', END)
        self.text.tag_add(SEL, INSERT + '-%dc' % len(text), INSERT)
        self.text.see(INSERT)  # 选择它,因此它可以被削减

    def onSelectAll(self):
        self.text.tag_add(SEL, '1.0', END + '-1c')  # 选择整个文本
        self.text.mark_set(INSERT, '1.0')  # 移动插入点至顶部
        self.text.see(INSERT)  # 滚动至顶部

    ####################################################################################
    # 搜索菜单命令
    ####################################################################################

    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 0 < line <= maxline:
                self.text.mark_set(INSERT, '%d.0' % line)  # 转到行
                self.text.tag_remove(SEL, '1.0', END)  # 删除选择
                self.text.tag_add(SEL, INSERT, 'insert + 1l')  # 选择行
                self.text.see(INSERT)  # 滚至行
            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:
            nocase = configs.get('caseinsens', True)  # 不区分大小写
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:
                showerror('PyEdit', 'String not found')  # 不要换行
            else:
                pastkey = where + '+%dc' % len(key)  # 索引过去的键
                self.text.tag_remove(SEL, '1.0', END)  # 移除任何 sel
                self.text.tag_add(SEL, where, pastkey)  # 选择键
                self.text.mark_set(INSERT, pastkey)  # 为了下一次查找
                self.text.see(where)  # 滚动显示

    def onRefind(self):
        self.onFind(self.lastfind)

    def onChange(self):
        """
        非模态查找/变换对话框;
        将每个对话框的输入传递给回调函数,也许 > 1 变换对话框会打开。
        """
        new = Toplevel(self)
        new.title('PyEdit - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0,
                                                                   column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1,
                                                                   column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():  # 封闭范围中使用我的条目
            self.onFind(entry1.get())  # 运行正常查找对话框回调函数

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0,
                                                      column=2,
                                                      sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1,
                                                        column=2,
                                                        sticky=EW)
        new.columnconfigure(1, weight=1)  # 扩展项目

    def onDoChange(self, findtext, changeto):
        # 变换对话框中使用 "应用":变换和重查找
        if self.text.tag_ranges(SEL):  # 必须先寻找
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)  # 为空则删除
            self.text.see(INSERT)
            self.onFind(findtext)  # 跳转到下一个出现的地方
            self.text.update()  # 强制刷新

    def onGrep(self):
        """
        线程外部文件搜索;在目录树中以字符串来搜索匹配的文件名;在出现的行单击列表框
        打开相匹配的文件;

        搜索是线程的,因此 GUI 仍然活跃并且不堵塞,以便允许多个 grep 及时的重叠;可以
        使用线程工具,但要避免不活的 grep 中的循环;

        grep Unicode 策略:搜索树中文本文件的内容可能是任意的Unicode编码,在这里只打开,
        但允许此编码用于整个树的输入,将它预设为平台文件系统或默认的文本,并跳过编码失败
        的文件,最坏的情况是如果有几率存在N种编码,用户可能需要允许 N 次grep;否则打开可
        能会引发异常,在二进制模式下打开可能无法将编码文本域搜索字符相匹配;

        待定:如果任何文件都解码失败,更好的发现错误?但在 "记事本" 中创建的 utf-16 2个
        字节/字符格式可能经 utf-8 解码而不会有错误,搜索字符串不会被发现;
        待定:可能运行多种编码名称的输入,用逗号分开,每个文件尝试一种编码,不需要打开
        loadEncode?
        """
        from Gui.ShellGui.formrows import makeFormRow

        # 非模态的对话框(get dirname, filenamepatt, grepkey)
        popup = Toplevel()
        popup.title('PyEdit - grep')
        var1 = makeFormRow(popup, label='Directory', width=18, browse=False)
        var2 = makeFormRow(popup,
                           label='Filename pattern',
                           width=18,
                           browse=False)
        var3 = makeFormRow(popup,
                           label='Search string',
                           width=18,
                           browse=False)
        var4 = makeFormRow(popup,
                           label='Content encoding',
                           width=18,
                           browse=False)
        var1.set('.')  # 当前目录
        var2.set('*.py')  # 初始值
        var4.set(sys.getdefaultencoding())  # 用于文件内容,而非文件名
        cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(),
                                   var4.get())
        Button(popup, text='Go', command=cb).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey, encoding):
        """
        跳至 grep 对话框:填充匹配的滚动列表
        待定:发生器线程应该守护进程吗,这样它会与应用程序一起终止?
        """
        import threading
        import queue

        # 制作非模态不可关闭的对话框
        mypopup = Tk()
        mypopup.title('PyEdit - grepping')
        status = Label(mypopup,
                       text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close
        # 启动发生器线程,消费器循环
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue)
        threading.Thread(target=self.grepThreadProducer,
                         args=threadargs).start()
        self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding,
                           myqueue):
        """
        在一个非 GUI 的并列线程:使find.find结果列表排序;
        也可以将查找的匹配项排序,但需要保留窗口;
        文件内容和文件名称在这可能都无法解码;

        待定:可以通过解码字节来查找以避免os.walk/listdir中文件名解码异常,
        但使用哪个编码? sys.getfilesystemcoding()如果不为空呢?
        """
        from Tools.find import find
        matches = []
        try:
            for filepath in find(pattern=filenamepatt, startdir=dirname):
                try:
                    textfile = open(filepath, encoding=encoding)
                    for (linenum, linestr) in enumerate(textfile):
                        if grepkey in linestr:
                            msg = '%s@%d [%s]' % (filepath, linenum + 1,
                                                  linestr)
                            matches.append(msg)
                except UnicodeError as x:
                    print('Unicode error in:', filepath, x)  # 例如:解码,bom
                except IOError as x:
                    print('IO error in:', filepath, x)  # 例如:许可
        finally:
            myqueue.put(matches)  # 发现异常就停止消费器循环: 文件名?

    def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup):
        """
        在主 GUI 线程:观察结果队列或[];可能有许多活跃的 grep 线程/循环/队列;
        可能还有其它线程类型/进行中的检查器,尤其是 PyEdit 作为附加组件时(PyMailGUI);
        """
        import queue
        try:
            matches = myqueue.get(block=False)
        except queue.Empty:
            myargs = (grepkey, encoding, myqueue, mypopup)
            self.after(250, self.grepThreadConsumer, *myargs)
        else:
            mypopup.destroy()  # 关闭状态
            self.update()  # 现在就删除它
            if not matches:
                showinfo('PyEdit', 'Grep found no matches for: %r' % grepkey)
            else:
                self.grepMatchesList(matches, grepkey, encoding)

    def grepMatchesList(self, matches, grepkey, encoding):
        """
        成功匹配后填充列表;
        从搜索中我们已经知道 Unicode 编码;
        单击文件名时在这使用它,因此打开不询问用户。
        """
        from Gui.Tour.scrolledlist import ScrolledList

        print('Matches for %s: %s' % (grepkey, len(matches)))

        # 双击捕获列表文件并打开
        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):
                file, line = selection.split('[', 1)[0].split('@')
                editor = TextEditorMainPopup(loadFirst=file,
                                             winTitle='grep match',
                                             loadEncode=encoding)
                editor.onGoto(int(line))
                editor.text.focus_force()  # no, really

        # 信件非模态窗口
        popup = Tk()
        popup.title('PyEdit - grep matches: %r (%s)' % (grepkey, encoding))
        ScrolledFilenames(parent=popup, options=matches)

    ##############################################################################################
    # 工具菜单命令
    ##############################################################################################

    def onFontList(self):
        self.fonts.append(self.fonts[0])  # 在列表中挑选下一个字体
        self.fonts.remove(self.fonts[0])  # 调整文本区域的大小
        self.text.config(font=self.fonts[0])

    def onColorList(self):
        self.colors.append(self.colors[0])  # 在列表中挑选下一种颜色
        self.colors.remove(self.colors[0])  # 移动目前的至结尾
        self.text.config(fg=self.colors[0]['fg'], bg=self.colors[0]['bg'])

    def onPickFg(self):
        self.pickColor('fg')  # 加入10/02/00

    def onPickBg(self):  # 挑选任意的颜色
        self.pickColor('bg')  # 在标准颜色对话框中

    def pickColor(self, part):
        (triple, hexstr) = askcolor()
        if hexstr:
            self.text.config(**{part: hexstr})

    def onInfo(self):
        """
        弹出对话框给出文字统计和光标位置。
        警告:Tk插入位置的列将制表符作为一个字符:转换成 8倍 以匹配视觉吗?
        """
        text = self.getAllText()  # 15 分钟内加入 5/3/00
        tbytes = len(text)  # 使用一个简单的猜测
        lines = len(text.split('\n'))  # 通过空白字符进行分割
        words = len(text.split())  # 3.X:字节实际上是字符
        index = self.text.index(INSERT)  # 字符串是 unicode 编码点
        where = tuple(index.split('.'))
        showinfo(
            'PyEdit Information',
            'Current location:\n\n' + 'line:\t%s\ncolumn:\t%s\n\n' % where +
            'File text statistics:\n\n' +
            'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (tbytes, lines, words))

    def onClone(self, makewindow=True):
        """
        打开一个新的编辑窗口,而不是改变一个已经打开的(onNew);
        继承了退出,克隆窗口的其他行为;
        如果让它自己弹出,子类必须重新定义/代替这个类,否则这个
        类会在此创建一个假的额外的窗口,这个窗口时空的。
        """
        if not makewindow:
            new = None  # 假设类成为了自己的窗口
        else:
            new = Toplevel()  # 同一过程中的编辑窗口
        myclass = self.__class__  # 实例的(最低)类对象
        myclass(new)  # 连接/运行我的类的实例

    def onRunCode(self, parallelmode=True):
        """
        运行正在编辑的代码 ---- 不是一个IDE,却很方便;尝试在文件目录下运行,而不是 cwd,
        为脚本文件输入并添加命令行参数;

        编码的标准输入/输出/错误 = 编辑器的启动窗口,如有的话:运行一个控制台窗口以看到代
        码的打印输出;但并行模式使用开始去给输入/输出打开一个DOS窗口;模块搜索路径将包括
        '.' 目录,那是开始的地方;
        在非文件模式,编码的 Tk 根可能是PyEdit的窗口。子过程或多重处理模块可能也在此运行;

        在子目录后固定使用基文件名,而非路径后;
        使用StartArgs来容许Windows系统文件模式中的参数;
        在第一次对话后运行一个update(),否则少数情况下第二对话有时不会出现;
        """
        from launchmodes import System, Start, StartArgs, Fork

        def askcmdargs():
            return askstring('PyEdit', 'Commandline arguments?') or ''

        filemode = False
        thefile = str(self.getFileName())
        if os.path.exists(thefile):
            filemode = askyesno('PyEdit', 'Run from file?')
            self.update()  # 运行update()
        if not filemode:  # 运行文本字符串
            cmdargs = askcmdargs()
            namespace = {'__name__': '__main__'}  # 作为顶层运行
            sys.argv = [thefile] + cmdargs.split()  # 可以使用线程
            exec(self.getAllText() + '\n', namespace)  # 忽略异常
        elif self.text_edit_modified():  # 已变的测试
            showerror('PyEdit', 'Text changed: you must save before run')
        else:
            cmdargs = askcmdargs()
            mycwd = os.getcwd()  # cwd 可能是根
            dirname, filename = os.path.split(thefile)  # 得到目录,基
            os.chdir(dirname or mycwd)  # 对于文件名 cd
            thecmd = filename + ' ' + cmdargs  # 不是这个文件
            if not parallelmode:  # 作为文件运行
                System(thecmd, thecmd)()  # 阻塞编辑器
            else:
                if sys.platform[:3] == 'win':  # 并行生产
                    run = StartArgs if cmdargs else Start  # 支持参数
                    run(thecmd, thecmd)()  # 或者一直生产
                else:
                    Fork(thecmd, thecmd)()  # 并行生产
            os.chdir(mycwd)  # 返回我的目录

    def onPickFont(self):
        """
        每个对话框的输入传递至回调函数,或许 > 1 字体对话框打开
        """
        from Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('PyEdit - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var2.set('12')  # 建议的 vals
        var3.set('bold italic')  # 参加选择列表用于有效输入
        Button(popup,
               text='Apply',
               command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        except:
            showerror('PyEdit', 'Bar font sepecification')

    ##################################################################################
    # 实用程序,此类之外的用处
    ##################################################################################

    def isEmpty(self):
        return not self.getAllText()

    def getAllText(self):
        return self.text.get('1.0', END + '-1c')  # 作为 str 字符串抽取文本

    def setAllText(self, text):
        """
        调用程序:如果恰好已封装则首先调用 self.update(),否则初始位置可能会在第2行,
        而不是第一行(Tk的错误?)。
        """
        self.text.delete('1.0', END)  # 在小部件中存储文本字符串
        self.text.insert(END, text)  # 或'1.0',文本=字节或字符串
        self.text.mark_set(INSERT, '1.0')  # 将插入点移至顶部
        self.text.see(INSERT)  # 滚至顶部,插入集

    def clearAllText(self):
        self.text.delete('1.0', END)  # 在小部件中清楚文本

    def getFileName(self):
        return self.currfile

    def setFileName(self, name):  # 请参阅 onGoto(linenum)
        self.currfile = name  # 用于保存
        self.filelabel.config(text=str(name))

    def setKnowEncoding(self, encoding='utf-8'):  # 如果插入则用于保存
        self.knownEncoding = encoding  # 否则保存使用配置,询问?

    def setBg(self, color):
        self.text.config(bg=color)  # 从代码 def 手动设置

    def setFg(self, color):
        self.text.config(fg=color)  # "黑", 十六进制串

    def setFont(self, font):
        self.text.config(font=font)  # ("字族", 大小, "风格")

    def setHeight(self, lines):  # 默认= 24h x 80w
        self.text.config(height=lines)  # 也可能来自于 textConfig.py

    def setWidth(self, chars):
        self.text.config(width=chars)

    def clearModified(self):
        self.text.edit_modified(0)  # 清楚修改的标记

    def isModified(self):
        return self.text_edit_modified()  # 从上次重设开始改变?

    def help(self):
        showinfo('About PyEdit', helptext % ((Version, ) * 2))
Example #14
0
class TextEditor:  # mix with menu/toolbar Frame class
    startfiledir = "."  # for dialogs
    editwindows = []  # for process-wide quit check

    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.update()  # 2.1: else @ line 2; see book
            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),
                    ("Grep...", 3, self.onGrep),
                ],
            ),
            (
                "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
        # GuiMaker frame packs itself
        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient="horizontal")
        text = Text(self, padx=5, wrap="none")  # disable line wrapping
        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 askyesno("PyEdit", "Text has changed: discard changes?")  # 2.0
        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 askyesno("PyEdit", "Text has changed: discard changes?")  # 2.0
        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):
        """
        on Quit menu/toolbar select and wm border X button in toplevel windows;
        2.1: don't exit app if others changed;  2.0: don't ask if self unchanged;
        moved to the top-level window classes at the end since may vary per usage:
        a Quit in GUI might quit() to exit, destroy() just one Toplevel, Tk, or 
        edit frame, or not be provided at all when run as an attached component;
        check self for changes, and if might quit(), main windows should check
        other windows in the process-wide list to see if they have changed too; 
        """
        assert False, "onQuit must be defined in window-specific sublass"

    def text_edit_modified(self):
        """
        2.1: this now works! seems to have been a bool result type issue in tkinter;
        2.0: self.text.edit_modified() broken in Python 2.4: do manually for now; 
        """
        return self.text.edit_modified()

    # 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", True)  # 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):
        """
        non-modal find/change dialog 
        2.1: pass per-dialog inputs to callbacks, may be > 1 change dialog open
        """
        new = Toplevel(self)
        new.title("PyEdit - change")
        Label(new, text="Find text?", relief=RIDGE, width=15).grid(row=0, column=0)
        Label(new, text="Change to?", relief=RIDGE, width=15).grid(row=1, column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():  # use my entry in enclosing scope
            self.onFind(entry1.get())  # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text="Find", command=onFind).grid(row=0, column=2, sticky=EW)
        Button(new, text="Apply", command=onApply).grid(row=1, column=2, sticky=EW)
        new.columnconfigure(1, weight=1)  # expandable entries

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):  # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)  # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)  # goto next appear
            self.text.update()  # force refresh

    def onGrep(self):
        """
        new in version 2.1: external file search
        search matched filenames in directory tree for string;
        listbox clicks open matched file at line of occurrence;
        caveat: search blocks GUI - should use thread and queue;
        caveat: this is not very robust - exceptions not caught;
        """
        from PP4E.Gui.ShellGui.formrows import makeFormRow

        # nonmodal dialog: get dirnname, filenamepatt, grepkey
        popup = Toplevel()
        popup.title("PyEdit - grep")
        var1 = makeFormRow(popup, label="Directory root", width=18, browse=False)
        var2 = makeFormRow(popup, label="Filename pattern", width=18, browse=False)
        var3 = makeFormRow(popup, label="Search string", width=18, browse=False)
        var1.set(".")  # current dir
        var2.set("*.py")  # initial values
        Button(popup, text="Go", command=lambda: self.onDoGrep(var1.get(), var2.get(), var3.get())).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey):
        # on Go in grep dialog: populate scrolled list with matches
        from PP4E.Tools.find import find
        from PP4E.Gui.Tour.scrolledlist import ScrolledList

        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):
                # on list double-click
                file, line = selection.split("  [", 1)[0].split("@")
                editor = TextEditorMainPopup(loadFirst=file, winTitle=" grep match")
                editor.onGoto(int(line))
                editor.text.focus_force()  # no, really

        # should thread/queue/after me
        showinfo("PyEdit Wait", "Ready to search files (a pause may follow)...")
        matches = []
        for filepath in find(pattern=filenamepatt, startdir=dirname):
            try:
                for (linenum, linestr) in enumerate(open(filepath)):
                    if grepkey in linestr:
                        matches.append("%s@%d  [%s]" % (filepath, linenum + 1, linestr))
            except:
                print("Failed:", filepath)  # Unicode errors, probably

        if not matches:
            showinfo("PyEdit", "No matches found")
        else:
            popup = Tk()
            popup.title("PyEdit - grep matches: %r" % grepkey)
            ScrolledFilenames(parent=popup, options=matches)

    ############################################################################
    # 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("\n"))  # any separated by whitespace
        words = len(text.split())  # 3.x: bytes is really chars
        index = self.text.index(INSERT)  # str is unicode code points
        where = tuple(index.split("."))
        showinfo(
            "PyEdit Information",
            "Current location:\n\n"
            + "line:\t%s\ncolumn:\t%s\n\n" % where
            + "File text statistics:\n\n"
            + "chars:\t%d\nlines:\t%d\nwords:\t%d\n" % (bytes, lines, words),
        )

    def onClone(self):
        """
        open a new edit window without changing one already open
        inherits quit and other behavior of window that it clones
        """
        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 PP4E 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 may be PyEdit's window;
        subprocess or multiprocessing modules may work here too;
        2.1: fixed to use base file name after chdir, not path;
        2.1: use StartArs to allow args in file mode on Windows;
        """

        def askcmdargs():
            return askstring("PyEdit", "Commandline arguments?") or ""

        from PP4E.launchmodes import System, Start, StartArgs, 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() + "\n", 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
            dirname, filename = os.path.split(thefile)  # get dir, base
            os.chdir(dirname or mycwd)  # cd for filenames
            thecmd = filename + " " + cmdargs  # 2.1: not theFile
            if not parallelmode:  # run as file
                System(thecmd, thecmd)()  # block editor
            else:
                if sys.platform[:3] == "win":  # spawn in parallel
                    run = StartArgs if cmdargs else Start  # 2.1: support args
                    run(thecmd, thecmd)()  # or always Spawn
                else:
                    Fork(thecmd, thecmd)()  # spawn in parallel
            os.chdir(mycwd)  # go back to my dir

    def onPickFont(self):
        """
        2.0 non-modal font spec dialog
        2.1: pass per-dialog inputs to callback, may be > 1 font dialog open
        """
        from PP4E.Gui.ShellGui.formrows import makeFormRow

        popup = Toplevel(self)
        popup.title("PyEdit - font")
        var1 = makeFormRow(popup, label="Family", browse=False)
        var2 = makeFormRow(popup, label="Size", browse=False)
        var3 = makeFormRow(popup, label="Style", browse=False)
        var1.set("courier")
        var2.set("12")  # suggested vals
        var3.set("bold italic")  # see pick list for valid inputs
        Button(popup, text="Apply", command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get())).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        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 x 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))
Example #15
0
    class CutplaceFrame(Frame):
        """
        Tk frame to validate a CID and data file.
        """
        def __init__(self,
                     master,
                     cid_path=None,
                     data_path=None,
                     config=dict(),
                     **keywords):
            """
            Set up a frame with widgets to validate ``id_path`` and ``data_path``.

            :param master: Tk master or root in which the frame should show up
            :param cid_path: optional preset for :guilabel:`CID` widget
            :type cid_path: str or None
            :param data_path: optional preset for :guilabel:`Data` widget
            :type data_path: str or None
            :param config: Tk configuration
            :param keywords: Tk keywords
            """
            assert has_tk
            assert master is not None

            if six.PY2:
                # In Python 2, Frame is an old style class.
                Frame.__init__(self, master, config, **keywords)
            else:
                super().__init__(master, config, **keywords)

            self._master = master

            # Define basic layout.
            self.grid(padx=_PADDING, pady=_PADDING)
            # self.grid_columnconfigure(1, weight=1)
            self.grid_rowconfigure(_VALIDATION_REPORT_ROW, weight=1)

            # Choose CID.
            self._cid_label = Label(self, text='CID:')
            self._cid_label.grid(row=_CID_ROW, column=0, sticky=E)
            self._cid_path_entry = Entry(self, width=55)
            self._cid_path_entry.grid(row=_CID_ROW, column=1, sticky=E + W)
            self._choose_cid_button = Button(self,
                                             command=self.choose_cid,
                                             text='Choose...')
            self._choose_cid_button.grid(row=_CID_ROW, column=2)
            self.cid_path = cid_path

            # Choose data.
            self._data_label = Label(self, text='Data:')
            self._data_label.grid(row=_DATA_ROW, column=0, sticky=E)
            self._data_path_entry = Entry(self, width=55)
            self._data_path_entry.grid(row=_DATA_ROW, column=1, sticky=E + W)
            self._choose_data_button = Button(self,
                                              command=self.choose_data,
                                              text='Choose...')
            self._choose_data_button.grid(row=_DATA_ROW, column=2)
            self.data_path = data_path

            # Validate.
            self._validate_button = Button(self,
                                           command=self.validate,
                                           text='Validate')
            self._validate_button.grid(row=_VALIDATE_BUTTON_ROW,
                                       column=0,
                                       padx=_PADDING,
                                       pady=_PADDING)

            # Validation status text.
            self._validation_status_text = StringVar()
            validation_status_label = Label(
                self, textvariable=self._validation_status_text)
            validation_status_label.grid(row=_VALIDATE_BUTTON_ROW, column=1)

            # Validation result.
            validation_report_frame = LabelFrame(self,
                                                 text='Validation report')
            validation_report_frame.grid(row=_VALIDATION_REPORT_ROW,
                                         columnspan=3,
                                         sticky=E + N + S + W)
            validation_report_frame.grid_columnconfigure(0, weight=1)
            validation_report_frame.grid_rowconfigure(0, weight=1)
            self._validation_report_text = Text(validation_report_frame)
            self._validation_report_text.grid(column=0,
                                              row=0,
                                              sticky=E + N + S)
            _validation_report_scrollbar = Scrollbar(validation_report_frame)
            _validation_report_scrollbar.grid(column=1,
                                              row=0,
                                              sticky=N + S + W)
            _validation_report_scrollbar.config(
                command=self._validation_report_text.yview)
            self._validation_report_text.config(
                yscrollcommand=_validation_report_scrollbar.set)

            # Set up file dialogs.
            self._choose_cid_dialog = Open(
                initialfile=self.cid_path,
                title='Choose CID',
            )
            self._choose_data_dialog = Open(
                initialfile=self.data_path,
                title='Choose data',
            )
            self._save_log_as_dialog = SaveAs(
                defaultextension='.log',
                initialfile='cutplace.log',
                title='Save validation result',
            )

            menubar = Menu(master)
            master.config(menu=menubar)
            self._file_menu = Menu(menubar, tearoff=False)
            self._file_menu.add_command(command=self.choose_cid,
                                        label='Choose CID...')
            self._file_menu.add_command(command=self.choose_data,
                                        label='Choose data...')
            self._file_menu.add_command(command=self.save_validation_report_as,
                                        label='Save validation report as...')
            self._file_menu.add_command(command=self.quit, label='Quit')
            menubar.add_cascade(label='File', menu=self._file_menu)
            help_menu = Menu(menubar, tearoff=False)
            help_menu.add_command(command=self.show_about, label='About')
            menubar.add_cascade(label='Help', menu=help_menu)

            self._enable_usable_widgets()

        def _enable_usable_widgets(self):
            def state_for(possibly_empty_text):
                if (possibly_empty_text
                        is not None) and (possibly_empty_text.rstrip() != ''):
                    result = 'normal'
                else:
                    result = 'disabled'
                return result

            def set_state(widget_to_set_state_for, possibly_empty_text):
                widget_to_set_state_for.config(
                    state=state_for(possibly_empty_text))

            set_state(self._validate_button, self.cid_path)
            set_state(self._validation_report_text, self.validation_report)
            set_state(self._data_path_entry, self.cid_path)
            set_state(self._choose_data_button, self.cid_path)

            cid_path_state = state_for(self.cid_path)
            self._file_menu.entryconfig(_CHOOSE_DATA_PATH_MENU_INDEX,
                                        state=cid_path_state)
            self._file_menu.entryconfig(_SAVE_VALIDATION_REPORT_AS_MENU_INDEX,
                                        state=state_for(
                                            self.validation_report))

        def choose_cid(self):
            """
            Open a dialog to set the CID path.
            """
            cid_path = self._choose_cid_dialog.show()
            if cid_path != '':
                self.cid_path = cid_path
                self._enable_usable_widgets()

        def choose_data(self):
            """
            Open a dialog to set the data path.
            """
            data_path = self._choose_data_dialog.show()
            if data_path != '':
                self.data_path = data_path
                self._enable_usable_widgets()

        def save_validation_report_as(self):
            """
            Open a dialog to set specify where the validation results should be
            stored and write to this file.
            """
            validation_report_path = self._save_log_as_dialog.show()
            if validation_report_path != '':
                try:
                    with io.open(validation_report_path, 'w',
                                 encoding='utf-8') as validation_result_file:
                        validation_result_file.write(
                            self._validation_report_text.get(1.0, END))
                except Exception as error:
                    showerror('Cutplace error',
                              'Cannot save validation results:\n%s' % error)

        def quit(self):
            self._master.destroy()

        def show_about(self):
            showinfo('Cutplace', 'Version ' + __version__)

        def clear_validation_report_text(self):
            """
            Clear the text area containing the validation results.
            """
            self._validation_report_text.configure(state='normal')
            self._validation_report_text.delete(1.0, END)
            self._validation_report_text.see(END)
            self._enable_usable_widgets()

        def _cid_path(self):
            return self._cid_path_entry.get()

        def _set_cid_path(self, value):
            self._cid_path_entry.delete(0, END)
            if value is not None:
                self._cid_path_entry.insert(0, value)

        cid_path = property(_cid_path, _set_cid_path, None,
                            'Path of the CID to use for validation')

        def _data_path(self):
            return self._data_path_entry.get()

        def _set_data_path(self, value):
            self._data_path_entry.delete(0, END)
            if value is not None:
                self._data_path_entry.insert(0, value)

        data_path = property(_data_path, _set_data_path, None,
                             'Path of the data to validate')

        @property
        def validation_report(self):
            return self._validation_report_text.get(0.0, END)

        def validate(self):
            """
            Validate the CID and (if specified) data file and update the
            :py:attr:`validation_result`. Show any errors unrelated to data in a
            dialog.
            """
            assert self.cid_path != ''

            def add_log_line(line):
                self._validation_report_text.config(state=NORMAL)
                try:
                    self._validation_report_text.insert(END, line + '\n')
                    self._validation_report_text.see(END)
                finally:
                    self._validation_report_text.config(state=DISABLED)

            def add_log_error_line(line_or_error):
                add_log_line('ERROR: %s' % line_or_error)

            def show_status_line(line):
                self._validation_status_text.set(line)
                self.master.update()

            assert self.cid_path != ''

            cid_name = os.path.basename(self.cid_path)
            self.clear_validation_report_text()
            add_log_line('%s: validating' % cid_name)
            self._enable_usable_widgets()
            cid = None
            try:
                cid = interface.Cid(self.cid_path)
                add_log_line('%s: ok' % cid_name)
            except errors.InterfaceError as error:
                add_log_error_line(error)
            except Exception as error:
                add_log_error_line('cannot read CID: %s' % error)

            if (cid is not None) and (self.data_path != ''):
                try:
                    data_name = os.path.basename(self.data_path)
                    add_log_line('%s: validating' % data_name)
                    validator = validio.Reader(cid,
                                               self.data_path,
                                               on_error='yield')
                    show_status_line('Validation started')
                    last_update_time = time.time()
                    for row_or_error in validator.rows():
                        now = time.time()
                        if (now - last_update_time) >= 3:
                            last_update_time = now
                            show_status_line('%d rows validated' %
                                             (validator.accepted_rows_count +
                                              validator.rejected_rows_count))
                        if isinstance(row_or_error, errors.DataError):
                            add_log_error_line(row_or_error)
                    show_status_line('%d rows validated - finished' %
                                     (validator.accepted_rows_count +
                                      validator.rejected_rows_count))
                    add_log_line('%s: %d rows accepted, %d rows rejected' %
                                 (data_name, validator.accepted_rows_count,
                                  validator.rejected_rows_count))
                except Exception as error:
                    add_log_error_line('cannot validate data: %s' % error)
class TextEditor:  # mix with menu/toolbar Frame class
    startfiledir = '.'  # for dialogs
    editwindows = []  # for process-wide quit check

    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.update()  # 2.1: else @ line 2; see book
            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),
                           ('Grep...', 3, self.onGrep)]),
            ('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
        # GuiMaker frame packs itself
        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='none')  # disable line wrapping
        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):
        """
        on Quit menu/toolbar select and wm border X button in toplevel windows;
        2.1: don't exit app if others changed;  2.0: don't ask if self unchanged;
        moved to the top-level window classes at the end since may vary per usage:
        a Quit in GUI might quit() to exit, destroy() just one Toplevel, Tk, or 
        edit frame, or not be provided at all when run as an attached component;
        check self for changes, and if might quit(), main windows should check
        other windows in the process-wide list to see if they have changed too; 
        """
        assert False, 'onQuit must be defined in window-specific sublass'

    def text_edit_modified(self):
        """
        2.1: this now works! seems to have been a bool result type issue in tkinter;
        2.0: self.text.edit_modified() broken in Python 2.4: do manually for now; 
        """
        return self.text.edit_modified()
    #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', True)  # 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):
        """
        non-modal find/change dialog 
        2.1: pass per-dialog inputs to callbacks, may be > 1 change dialog open
        """
        new = Toplevel(self)
        new.title('PyEdit - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0,
                                                                   column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1,
                                                                   column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():  # use my entry in enclosing scope
            self.onFind(entry1.get())  # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0,
                                                      column=2,
                                                      sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1,
                                                        column=2,
                                                        sticky=EW)
        new.columnconfigure(1, weight=1)  # expandable entries

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):  # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)  # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)  # goto next appear
            self.text.update()  # force refresh

    def onGrep(self):
        """
        new in version 2.1: threaded external file search;
        search matched filenames in directory tree for string;
        listbox clicks open matched file at line of occurrence;
        search is threaded so the GUI remains active and is not
        blocked, and to allow multiple greps to overlap in time;
        could use threadtools, but avoid loop in no active grep;
        """
        from PP4E.Gui.ShellGui.formrows import makeFormRow

        # nonmodal dialog: get dirnname, filenamepatt, grepkey
        popup = Toplevel()
        popup.title('PyEdit - grep')
        var1 = makeFormRow(popup,
                           label='Directory root',
                           width=18,
                           browse=False)
        var2 = makeFormRow(popup,
                           label='Filename pattern',
                           width=18,
                           browse=False)
        var3 = makeFormRow(popup,
                           label='Search string',
                           width=18,
                           browse=False)
        var1.set('.')  # current dir
        var2.set('*.py')  # initial values
        Button(popup,
               text='Go',
               command=lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey):
        # on Go in grep dialog: populate scrolled list with matches
        # tbd: should producer thread be daemon so dies with app?
        import threading, queue

        # make non-modal un-closeable dialog
        mypopup = Tk()
        mypopup.title('PyEdit - grepping')
        status = Label(mypopup,
                       text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close

        # start producer thread, consumer loop
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, myqueue)
        threading.Thread(target=self.grepThreadProducer,
                         args=threadargs).start()
        self.grepThreadConsumer(grepkey, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, myqueue):
        """
        in a non-GUI parallel thread: queue find.find results list;
        could also queue matches as found, but need to keep window;
        """
        from PP4E.Tools.find import find
        matches = []
        for filepath in find(pattern=filenamepatt, startdir=dirname):
            try:
                for (linenum, linestr) in enumerate(open(filepath)):
                    if grepkey in linestr:
                        message = '%s@%d  [%s]' % (filepath, linenum + 1,
                                                   linestr)
                        matches.append(message)
            except UnicodeDecodeError:
                print('Unicode error in:', filepath)
        myqueue.put(matches)

    def grepThreadConsumer(self, grepkey, myqueue, mypopup):
        """
        in the main GUI thread: watch queue for results or [];
        there may be multiple active grep threads/loops/queues;
        there may be other types of threads/checkers in process,
        especially when PyEdit is attached component (PyMailGUI);
        """
        import queue
        try:
            matches = myqueue.get(block=False)
        except queue.Empty:
            self.after(250, self.grepThreadConsumer, grepkey, myqueue, mypopup)
        else:
            mypopup.destroy()  # close status
            self.update()  # erase it now
            if not matches:
                showinfo('PyEdit', 'Grep found no matches for: %r' % grepkey)
            else:
                self.grepMatchesList(matches, grepkey)

    def grepMatchesList(self, matches, grepkey):
        # populate list after successful matches
        from PP4E.Gui.Tour.scrolledlist import ScrolledList
        print('Matches for %s: %s' % (grepkey, len(matches)))

        # catch list double-click
        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):
                file, line = selection.split('  [', 1)[0].split('@')
                editor = TextEditorMainPopup(loadFirst=file,
                                             winTitle=' grep match')
                editor.onGoto(int(line))
                editor.text.focus_force()  # no, really

        # new non-modal widnow
        popup = Tk()
        popup.title('PyEdit - grep matches: %r' % grepkey)
        ScrolledFilenames(parent=popup, options=matches)

    ############################################################################
    # 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('\n'))  # any separated by whitespace
        words = len(text.split())  # 3.x: bytes is really chars
        index = self.text.index(INSERT)  # str is unicode code points
        where = tuple(index.split('.'))
        showinfo(
            'PyEdit Information',
            'Current location:\n\n' + 'line:\t%s\ncolumn:\t%s\n\n' % where +
            'File text statistics:\n\n' +
            'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self):
        """
        open a new edit window without changing one already open
        inherits quit and other behavior of window that it clones
        """
        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 PP4E 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 may be PyEdit's window;
        subprocess or multiprocessing modules may work here too;
        2.1: fixed to use base file name after chdir, not path;
        2.1: use StartArs to allow args in file mode on Windows;
        """
        def askcmdargs():
            return askstring('PyEdit', 'Commandline arguments?') or ''

        from PP4E.launchmodes import System, Start, StartArgs, 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() + '\n', 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
            dirname, filename = os.path.split(thefile)  # get dir, base
            os.chdir(dirname or mycwd)  # cd for filenames
            thecmd = filename + ' ' + cmdargs  # 2.1: not theFile
            if not parallelmode:  # run as file
                System(thecmd, thecmd)()  # block editor
            else:
                if sys.platform[:3] == 'win':  # spawn in parallel
                    run = StartArgs if cmdargs else Start  # 2.1: support args
                    run(thecmd, thecmd)()  # or always Spawn
                else:
                    Fork(thecmd, thecmd)()  # spawn in parallel
            os.chdir(mycwd)  # go back to my dir

    def onPickFont(self):
        """
        2.0 non-modal font spec dialog
        2.1: pass per-dialog inputs to callback, may be > 1 font dialog open
        """
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('PyEdit - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var1.set('courier')
        var2.set('12')  # suggested vals
        var3.set('bold italic')  # see pick list for valid inputs
        Button(popup,
               text='Apply',
               command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        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 x 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))
class TextEditor:                          # mix with menu/toolbar Frame class
    startfiledir = '.'   # for dialogs
    editwindows = []     # for process-wide quit check

    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()                          
        if loadFirst:
            self.update()                          
            self.onOpen(loadFirst)

    def start(self):                                
        self.menuBar = [                           
            ('File', 0,                           
                 [('Open...',    0, self.onOpen),  
                  ('Save',       0, self.onSave),   
                  ('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),
                  ('Grep...',    3, self.onGrep)]
            ),
            ('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):                         
        name = Label(self, bg='black', fg='white')  
        name.pack(side=TOP, fill=X)                 
                                                
        vbar  = Scrollbar(self)
        hbar  = Scrollbar(self, orient='horizontal')
        text  = Text(self, padx=5, wrap='none')       
        text.config(undo=1, autoseparators=1)         

        vbar.pack(side=RIGHT,  fill=Y)
        hbar.pack(side=BOTTOM, fill=X)                 
        text.pack(side=TOP,    fill=BOTH, expand=YES) 

        text.config(yscrollcommand=vbar.set)    
        text.config(xscrollcommand=hbar.set)
        vbar.config(command=text.yview)         
        hbar.config(command=text.xview)         
        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

    def my_askopenfilename(self):     
        if not self.openDialog:
           self.openDialog = Open(initialdir=self.startfiledir,
                                  filetypes=self.ftypes)
        return self.openDialog.show()

    def my_asksaveasfilename(self):  
        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      
                askyesno('SimpleEditor', 'Text has changed: discard changes?'))
        if doit:
            file = loadFirst or self.my_askopenfilename()
            if file:
                try:
                    text = open(file, 'r').read()
                except:
                    showerror('SimpleEditor', 'Could not open file ' + file)
                else:
                    self.setAllText(text)
                    self.setFileName(file)
                    self.text.edit_reset()          
                    self.text.edit_modified(0)      

    def onSave(self):
        self.onSaveAs(self.currfile) 

    def onSaveAs(self, forcefile=None):
        file = forcefile or self.my_asksaveasfilename()
        if file:
            text = self.getAllText()
            try:
                open(file, 'w').write(text)
            except:
                showerror('SimpleEditor', 'Could not write file ' + file)
            else:
                self.setFileName(file)             # may be newly created
                self.text.edit_modified(0)         
                                                  
    def onNew(self):
        doit = (not self.text_edit_modified() or 
                askyesno('SimpleEditor', 'Text has changed: discard changes?'))
        if doit:
            self.setFileName(None)
            self.clearAllText()
            self.text.edit_reset()                
            self.text.edit_modified(0)             

    def onQuit(self):
        assert False, 'onQuit must be defined in window-specific sublass' 

    def text_edit_modified(self):
        return self.text.edit_modified()

    def onUndo(self):                        
        try:                                  
            self.text.edit_undo()               
        except TclError:                   
            showinfo('SimpleEditor', 'Nothing to undo')

    def onRedo(self):                          
        try:
            self.text.edit_redo()
        except TclError:
            showinfo('SimpleEditor', 'Nothing to redo')

    def onCopy(self):                           
        if not self.text.tag_ranges(SEL):       
            showerror('SimpleEditor', 'No text selected')
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

    def onDelete(self):                        
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', 'No text selected')
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', 'No text selected')
        else:
            self.onCopy()                       
            self.onDelete()

    def onPaste(self):
        try:
            text = self.selection_get(selection='CLIPBOARD')
        except TclError:
            showerror('SimpleEditor', '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')   
        self.text.mark_set(INSERT, '1.0')          
        self.text.see(INSERT)                      

    def onGoto(self, forceline=None):
        line = forceline or askinteger('SimpleEditor', '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)      
                self.text.tag_remove(SEL, '1.0', END)        
                self.text.tag_add(SEL, INSERT, 'insert + 1l')  
                self.text.see(INSERT)                        
            else:
                showerror('SimpleEditor', 'Bad line number')

    def onFind(self, lastkey=None):
        key = lastkey or askstring('SimpleEditor', 'Enter search string')
        self.text.update()
        self.text.focus()
        self.lastfind = key
        if key:                                                   
            nocase = configs.get('caseinsens', True)              
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:                                    
                showerror('SimpleEditor', 'String not found')
            else:
                pastkey = where + '+%dc' % len(key)           
                self.text.tag_remove(SEL, '1.0', END)         
                self.text.tag_add(SEL, where, pastkey)       
                self.text.mark_set(INSERT, pastkey)          
                self.text.see(where)                         

    def onRefind(self):
        self.onFind(self.lastfind)

    def onChange(self):
        new = Toplevel(self)
        new.title('SimpleEditor - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0, column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1, column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():                        
            self.onFind(entry1.get())       

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find',  command=onFind ).grid(row=0, column=2, sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1, column=2, sticky=EW)
        new.columnconfigure(1, weight=1)     

    def onDoChange(self, findtext, changeto):
        if self.text.tag_ranges(SEL):                     
            self.text.delete(SEL_FIRST, SEL_LAST)          
            self.text.insert(INSERT, changeto)             
            self.text.see(INSERT)
            self.onFind(findtext)                         
            self.text.update()                             

    def onGrep(self):   
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel()
        popup.title('SimpleEditor - grep')
        var1 = makeFormRow(popup, label='Directory root',   width=18, browse=False)
        var2 = makeFormRow(popup, label='Filename pattern', width=18, browse=False)
        var3 = makeFormRow(popup, label='Search string',    width=18, browse=False)
        var1.set('.')     
        var2.set('*.py')  
        Button(popup, text='Go',
           command=lambda: self.onDoGrep(var1.get(), var2.get(), var3.get())).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey):
        from PP4E.Tools.find import find
        from PP4E.Gui.Tour.scrolledlist import ScrolledList

        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):             
                file, line = selection.split('  [', 1)[0].split('@')
                editor = TextEditorMainPopup(loadFirst=file, winTitle=' grep match')
                editor.onGoto(int(line))
                editor.text.focus_force()   
        showinfo('SimpleEditor Wait', 'Ready to search files (a pause may follow)...')
        matches = []
        for filepath in find(pattern=filenamepatt, startdir=dirname):
            try:
                for (linenum, linestr) in enumerate(open(filepath)):
                    if grepkey in linestr:
                        matches.append('%s@%d  [%s]' % (filepath, linenum + 1, linestr))
            except:
                print('Failed:', filepath)  # Unicode errors, probably

        if not matches:
            showinfo('SimpleEditor', 'No matches found')
        else:
            popup = Tk()
            popup.title('SimpleEditor - grep matches: %r' % grepkey)
            ScrolledFilenames(parent=popup, options=matches)

    def onFontList(self):
        self.fonts.append(self.fonts[0])          
        del self.fonts[0]                          
        self.text.config(font=self.fonts[0])

    def onColorList(self):
        self.colors.append(self.colors[0])         
        del self.colors[0]                         
        self.text.config(fg=self.colors[0]['fg'], bg=self.colors[0]['bg'])

    def onPickFg(self):
        self.pickColor('fg')                     

    def onPickBg(self):                            # select arbitrary color
        self.pickColor('bg')                       # in standard color dialog

    def pickColor(self, part):                   
        (triple, hexstr) = askcolor()
        if hexstr:
            self.text.config(**{part: hexstr})

    def onInfo(self):
        text  = self.getAllText()                
        bytes = len(text)                        
        lines = len(text.split('\n'))              # any separated by whitespace
        words = len(text.split())             
        index = self.text.index(INSERT)            # str is unicode code points
        where = tuple(index.split('.'))
        showinfo('SimpleEditor Information',
                 'Current location:\n\n' +
                 'line:\t%s\ncolumn:\t%s\n\n' % where +
                 'File text statistics:\n\n' +
                 'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self):                  
        """
        open a new edit window without changing one already open
        inherits quit and other behavior of window that it clones
        """
        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):
        def askcmdargs():
            return askstring('SimpleEditor', 'Commandline arguments?') or ''

        from PP4E.launchmodes import System, Start, StartArgs, Fork
        filemode = False
        thefile  = str(self.getFileName())
        if os.path.exists(thefile):
            filemode = askyesno('SimpleEditor', 'Run from file?')
        if not filemode:                                    
            cmdargs   = askcmdargs()
            namespace = {'__name__': '__main__'}            
            sys.argv  = [thefile] + cmdargs.split()      
            exec(self.getAllText() + '\n', namespace)       
        elif self.text_edit_modified():                    
            showerror('SimpleEditor', 'Text changed: save before run')
        else:
            cmdargs = askcmdargs()
            mycwd   = os.getcwd()                           # cwd may be root
            dirname, filename = os.path.split(thefile)      # get dir, base
            os.chdir(dirname or mycwd)                      
            thecmd  = filename + ' ' + cmdargs             
            if not parallelmode:                            # run as file
                System(thecmd, thecmd)()                    # block editor
            else:
                if sys.platform[:3] == 'win':               # spawn in parallel
                    run = StartArgs if cmdargs else Start  
                    run(thecmd, thecmd)()                   # or always Spawn
                else:
                    Fork(thecmd, thecmd)()                  # spawn in parallel
            os.chdir(mycwd)                                

    def onPickFont(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('SimpleEditor - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size',   browse=False)
        var3 = makeFormRow(popup, label='Style',  browse=False)
        var1.set('courier')
        var2.set('12')              # suggested vals
        var3.set('bold italic')     # see pick list for valid inputs
        Button(popup, text='Apply', command=
               lambda: self.onDoFont(var1.get(), var2.get(), var3.get())).pack()

    def onDoFont(self, family, size, style):
        try:  
            self.text.config(font=(family, int(size), style))
        except:
            showerror('SimpleEditor', 'Bad font specification')

    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 x 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 ', helptext % ((Version,)*2))
Example #18
0
def ask_saveas(title, message, filetypes, defaultDir=None, defaultFile=None):
    dlg = SaveAs(title=title, message=message, filetypes=filetypes)
    filename = dlg.show()
    return filename
Example #19
0
 def asksaveasfilename(**options):
     "Ask for a filename to save as."
     return SaveAs(**options).show()
class TextEditor:  # mix with menu/toolbar Frame class
    startfiledir = '.'  # for dialogs
    editwindows = []  # for process-wide quit check

    # Unicode configurations
    # imported in class to allow overrides in subclass or self
    if __name__ == '__main__':
        from . import textConfig
    else:
        from .textConfig import (  # 2.1: always from this package
            opensAskUser, opensEncoding, savesUseKnownEncoding, savesAskUser,
            savesEncoding)

    ftypes = [
        ('All files', '*'),  # for file open dialog
        ('Text files', '.txt')
    ]  # customize in subclass

    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='', loadEncode=''):
        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.knownEncoding = None  # 2.1 Unicode: till Open or Save
        self.text.focus()  # else must click in text
        if loadFirst:
            self.update()  # 2.1: else @ line 2; see book
            self.onOpen(loadFirst, loadEncode)

    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
                     )  #,('Grep...',    3, self.onGrep)
                ]),
            (
                '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 = [
            ('Break', self.break_tag, {
                'side': LEFT
            }),
            ('Pitch Hz', self.prosody_pitch_h, {
                'side': LEFT
            }),
            ('Pitch st', self.prosody_pitch_s, {
                'side': LEFT
            }),
            ('Rate words', self.prosody_pitch_w, {
                'side': LEFT
            }),
            ('Rate %', self.prosody_pitch_r, {
                'side': LEFT
            }),
            ('Number', self.say_as_nu, {
                'side': LEFT
            }),
            ('Ordinal', self.say_as_no, {
                'side': LEFT
            }),
            ('Tel.', self.say_as_nt, {
                'side': LEFT
            }),
            ('Digits', self.say_as_di, {
                'side': LEFT
            }),
            ('Letters', self.say_as_l, {
                'side': LEFT
            }),
            ('Date f', self.say_as_df, {
                'side': LEFT
            }),
            ('Date vx', self.say_as_vxd, {
                'side': LEFT
            }),
            ('Currency', self.say_as_vxc, {
                'side': LEFT
            }),
            ('IPA', self.say_as_ipa, {
                'side': LEFT
            }),

            # 'separator',
            ('To tags', self.decode_and_download, {
                'side': RIGHT
            }),
            # 'separator',
            ('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
        # GuiMaker frame packs itself
        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='word')  # disable line wrapping
        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='', loadEncode=''):
        """
        2.1: total rewrite for Unicode support; open in text mode with 
        an encoding passed in, input from the user, in textconfig, or  
        platform default, or open as binary bytes for arbitrary Unicode
        encodings as last resort and drop \r in Windows end-lines if 
        present so text displays normally; content fetches are returned
        as str, so need to  encode on saves: keep encoding used here;

        tests if file is okay ahead of time to try to avoid opens;
        we could also load and manually decode bytes to str to avoid 
        multiple open attempts, but this is unlikely to try all cases;

        encoding behavior is configurable in the local textConfig.py:
        1) tries known type first if passed in by client (email charsets)
        2) if opensAskUser True, try user input next (prefill wih defaults)
        3) if opensEncoding nonempty, try this encoding next: 'latin-1', etc.
        4) tries sys.getdefaultencoding() platform default next
        5) uses binary mode bytes and Tk policy as the last resort
        """

        if self.text_edit_modified():  # 2.0
            if not askyesno('SSMLtagEdit',
                            'Text has changed: discard changes?'):
                return

        file = loadFirst or self.my_askopenfilename()
        if not file:
            return

        if not os.path.isfile(file):
            showerror('SSMLtagEdit', 'Could not open file ' + file)
            return

        # try known encoding if passed and accurate (e.g., email)
        text = None  # empty file = '' = False: test for None!
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):  # lookup: bad name
                pass

        # try user input, prefill with next choice as default
        if text == None and self.opensAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('SSMLtagEdit',
                                'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            self.text.focus()  # else must click
            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass

        # try config file (or before ask user?)
        if text == None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass

        # try platform default (utf-8 on windows; try utf8 always?)
        if text == None:
            try:
                text = open(file, 'r',
                            encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass

        # last resort: use binary bytes and rely on Tk to decode
        if text == None:
            try:
                text = open(file, 'rb').read()  # bytes for Unicode
                text = text.replace(b'\r\n', b'\n')  # for display, saves
                self.knownEncoding = None
            except IOError:
                pass

        if text == None:
            showerror('SSMLtagEdit', 'Could not decode and 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):
        """
        2.1: total rewrite for Unicode support: Text content is always 
        returned as a str, so we must deal with encodings to save to
        a file here, regardless of open mode of the output file (binary
        requires bytes, and text must encode); tries the encoding used
        when opened or saved (if known), user input, config file setting,
        and platform default last; most users can use platform default; 

        retains successful encoding name here for next save, because this
        may be the first Save after New or a manual text insertion;  Save
        and SaveAs may both use last known encoding, per config file (it
        probably should be used for Save, but SaveAs usage is unclear);
        gui prompts are prefilled with the known encoding if there is one;
        
        does manual text.encode() to avoid creating file; text mode files
        perform platform specific end-line conversion: Windows \r dropped 
        if present on open by text mode (auto) and binary mode (manually);
        if manual content inserts, must delete \r else duplicates here;
        knownEncoding=None before first Open or Save, after New, if binary Open;

        encoding behavior is configurable in the local textConfig.py:
        1) if savesUseKnownEncoding > 0, try encoding from last open or save
        2) if savesAskUser True, try user input next (prefill with known?)
        3) if savesEncoding nonempty, try this encoding next: 'utf-8', etc
        4) tries sys.getdefaultencoding() as a last resort
        """

        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return

        text = self.getAllText()  # 2.1: a str string, with \n eolns,
        encpick = None  # even if read/inserted as bytes

        # try known encoding at latest Open or Save, if any
        if self.knownEncoding and (  # enc known?
            (forcefile and self.savesUseKnownEncoding >= 1) or  # on Save?
            (not forcefile and self.savesUseKnownEncoding >= 2)):  # on SaveAs?
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass

        # try user input, prefill with known type, else next choice
        if not encpick and self.savesAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('SSMLtagEdit',
                                'Enter Unicode encoding for save',
                                initialvalue=(self.knownEncoding
                                              or self.savesEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            self.text.focus()  # else must click
            if askuser:
                try:
                    text.encode(askuser)
                    encpick = askuser
                except (UnicodeError, LookupError):  # LookupError:  bad name
                    pass  # UnicodeError: can't encode

        # try config file
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass

        # try platform default (utf8 on windows)
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass

        # open in text mode for endlines + encoding
        if not encpick:
            showerror('SSMLtagEdit', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('SSMLtagEdit', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)  # may be newly created
                self.text.edit_modified(0)  # 2.0: clear modified flag
                self.knownEncoding = encpick  # 2.1: keep enc for next save
                # don't clear undo/redo stks!
    def onNew(self):
        """
        start editing a new file from scratch in current window;
        see onClone to pop-up a new independent edit window instead;
        """
        if self.text_edit_modified():  # 2.0
            if not askyesno('SSMLtagEdit',
                            'Text has changed: discard changes?'):
                return
        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
        self.knownEncoding = None  # 2.1: Unicode type unknown

    def onQuit(self):
        """
        on Quit menu/toolbar select and wm border X button in toplevel windows;
        2.1: don't exit app if others changed;  2.0: don't ask if self unchanged;
        moved to the top-level window classes at the end since may vary per usage:
        a Quit in GUI might quit() to exit, destroy() just one Toplevel, Tk, or 
        edit frame, or not be provided at all when run as an attached component;
        check self for changes, and if might quit(), main windows should check
        other windows in the process-wide list to see if they have changed too; 
        """
        assert False, 'onQuit must be defined in window-specific sublass'

    def text_edit_modified(self):
        """
        2.1: this now works! seems to have been a bool result type issue in tkinter;
        2.0: self.text.edit_modified() broken in Python 2.4: do manually for now; 
        """
        return self.text.edit_modified()
    #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('SSMLtagEdit', 'Nothing to undo')

    def onRedo(self):  # 2.0: redo an undone
        try:
            self.text.edit_redo()
        except TclError:
            showinfo('SSMLtagEdit', '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('SSMLtagEdit', '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('SSMLtagEdit', 'No text selected')
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            showerror('SSMLtagEdit', '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('SSMLtagEdit', '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

    ############################################################################
    # SSML tags and TTS functhins
    ############################################################################

    def _bolder(self, text, counter):

        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)

        if counter != 1:
            self.text.tag_add("bold", "sel.first", "sel.last")
            bold_font = Font(self.text, self.text.cget("font"))
            size = int(self.text.cget("font").split(' ')[1])

            bold_font.configure(weight="bold", size=size - 2, slant="italic")
            self.text.tag_configure("bold", font=bold_font)

    def break_tag(self):

        text = '**{50}**'
        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.tag_add("bold", "sel.first", "sel.last")
        bold_font = Font(self.text, self.text.cget("font"))
        size = int(self.text.cget("font").split(' ')[1])

        bold_font.configure(weight="bold", size=size - 2, slant="italic")
        self.text.tag_configure("bold", font=bold_font)

        # self.text.see(INSERT)                   # select it, so it can be cut

    def prosody_pitch_h(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['[[{150h}', self.selection_get(selection='CLIPBOARD'), ']]']:
            self._bolder(i, counter)
            counter += 1

    def prosody_pitch_s(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['[[{15s}', self.selection_get(selection='CLIPBOARD'), ']]']:
            self._bolder(i, counter)
            counter += 1

    def prosody_pitch_w(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['[[{150w}', self.selection_get(selection='CLIPBOARD'), ']]']:
            self._bolder(i, counter)
            counter += 1

    def prosody_pitch_r(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['[[{150%}', self.selection_get(selection='CLIPBOARD'), ']]']:
            self._bolder(i, counter)
            counter += 1

    def say_as_nu(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['##{nu}', self.selection_get(selection='CLIPBOARD'), '##']:
            self._bolder(i, counter)
            counter += 1

    def say_as_no(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['##{no}', self.selection_get(selection='CLIPBOARD'), '##']:
            self._bolder(i, counter)
            counter += 1

    def say_as_nt(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['##{nt}', self.selection_get(selection='CLIPBOARD'), '##']:
            self._bolder(i, counter)
            counter += 1

    def say_as_di(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['##{di}', self.selection_get(selection='CLIPBOARD'), '##']:
            self._bolder(i, counter)
            counter += 1

    def say_as_l(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['##{l}', self.selection_get(selection='CLIPBOARD'), '##']:
            self._bolder(i, counter)
            counter += 1

    def say_as_df(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in [
                '##{dfXXX}',
                self.selection_get(selection='CLIPBOARD'), '##'
        ]:
            self._bolder(i, counter)
            counter += 1

    def say_as_vxc(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['##{vxc}', self.selection_get(selection='CLIPBOARD'), '##']:
            self._bolder(i, counter)
            counter += 1

    def say_as_vxd(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['##{vxd}', self.selection_get(selection='CLIPBOARD'), '##']:
            self._bolder(i, counter)
            counter += 1

    def say_as_ipa(self):

        if not self.text.tag_ranges(SEL):  # save in cross-app clipboard
            showerror('SSMLtagEdit', 'No text selected')
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

            self.onDelete()
        counter = 0
        for i in ['##{ipa}', self.selection_get(selection='CLIPBOARD'), '##']:
            self._bolder(i, counter)
            counter += 1

    def decode_and_download(self):
        text = self.getAllText()

        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

        self.clipboard_append(text)

        self.onDelete()

        text = decode(text)

        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

    ############################################################################
    # Search menu commands
    ############################################################################

    def onGoto(self, forceline=None):
        line = forceline or askinteger('SSMLtagEdit', '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('SSMLtagEdit', 'Bad line number')

    def onFind(self, lastkey=None):
        key = lastkey or askstring('SSMLtagEdit', 'Enter search string')
        self.text.update()
        self.text.focus()
        self.lastfind = key
        if key:  # 2.0: nocase
            nocase = configs.get('caseinsens', True)  # 2.0: config
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:  # don't wrap
                showerror('SSMLtagEdit', '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):
        """
        non-modal find/change dialog 
        2.1: pass per-dialog inputs to callbacks, may be > 1 change dialog open
        """
        new = Toplevel(self)
        new.title('SSMLtagEdit - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0,
                                                                   column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1,
                                                                   column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():  # use my entry in enclosing scope
            self.onFind(entry1.get())  # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0,
                                                      column=2,
                                                      sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1,
                                                        column=2,
                                                        sticky=EW)
        new.columnconfigure(1, weight=1)  # expandable entries

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):  # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)  # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)  # 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):
        """
        pop-up dialog giving text statistics and cursor location;
        caveat (2.1): Tk insert position column counts a tab as one 
        character: translate to next multiple of 8 to match visual?
        """
        text = self.getAllText()  # added on 5/3/00 in 15 mins
        bytes = len(text)  # words uses a simple guess:
        lines = len(text.split('\n'))  # any separated by whitespace
        words = len(text.split())  # 3.x: bytes is really chars
        index = self.text.index(INSERT)  # str is unicode code points
        where = tuple(index.split('.'))
        showinfo(
            'SSMLtagEdit Information',
            'Current location:\n\n' + 'line:\t%s\ncolumn:\t%s\n\n' % where +
            'File text statistics:\n\n' +
            'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self, makewindow=True):
        """
        open a new edit window without changing one already open (onNew);
        inherits quit and other behavior of the window that it clones;
        2.1: subclass must redefine/replace this if makes its own popup, 
        else this creates a bogus extra window here which will be empty;
        """
        if not makewindow:
            new = None  # assume class makes its own window
        else:
            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 onPickFont(self):
        """
        2.0 non-modal font spec dialog
        2.1: pass per-dialog inputs to callback, may be > 1 font dialog open
        """
        from . import formrows
        popup = Toplevel(self)
        popup.title('SSMLtagEdit - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var1.set('courier')
        var2.set('12')  # suggested vals
        var3.set('bold italic')  # see pick list for valid inputs
        Button(popup,
               text='Apply',
               command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        except:
            showerror('SSMLtagEdit', '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 str string

    def setAllText(self, text):
        """
        caller: call self.update() first if just packed, else the
        initial position may be at line 2, not line 1 (2.1; Tk bug?)
        """
        self.text.delete('1.0', END)  # store text string in widget
        self.text.insert(END, text)  # or '1.0'; text=bytes or str
        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):  # see also: onGoto(linenum)
        self.currfile = name  # for save
        self.filelabel.config(text=str(name))

    def setKnownEncoding(self, encoding='utf-8'):  # 2.1: for saves if inserted
        self.knownEncoding = encoding  # else saves use config, ask?

    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 x 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 SSMLtagEdit', helptext)  # % ((Version,)*2))
Example #21
0
def ask_saveas(title, message, filetypes, defaultDir=None, defaultFile=None):
    dlg = SaveAs(title=title, message=message, filetypes=filetypes)
    filename = dlg.show()
    return filename
Example #22
0
    class CutplaceFrame(Frame):
        """
        Tk frame to validate a CID and data file.
        """
        def __init__(self, master, cid_path=None, data_path=None, config=dict(), **keywords):
            """
            Set up a frame with widgets to validate ``id_path`` and ``data_path``.

            :param master: Tk master or root in which the frame should show up
            :param cid_path: optional preset for :guilabel:`CID` widget
            :type cid_path: str or None
            :param data_path: optional preset for :guilabel:`Data` widget
            :type data_path: str or None
            :param config: Tk configuration
            :param keywords: Tk keywords
            """
            assert has_tk
            assert master is not None

            if six.PY2:
                # In Python 2, Frame is an old style class.
                Frame.__init__(self, master, config, **keywords)
            else:
                super().__init__(master, config, **keywords)

            self._master = master

            # Define basic layout.
            self.grid(padx=_PADDING, pady=_PADDING)
            # self.grid_columnconfigure(1, weight=1)
            self.grid_rowconfigure(_VALIDATION_REPORT_ROW, weight=1)

            # Choose CID.
            self._cid_label = Label(self, text='CID:')
            self._cid_label.grid(row=_CID_ROW, column=0, sticky=E)
            self._cid_path_entry = Entry(self, width=55)
            self._cid_path_entry.grid(row=_CID_ROW, column=1, sticky=E + W)
            self._choose_cid_button = Button(self, command=self.choose_cid, text='Choose...')
            self._choose_cid_button.grid(row=_CID_ROW, column=2)
            self.cid_path = cid_path

            # Choose data.
            self._data_label = Label(self, text='Data:')
            self._data_label.grid(row=_DATA_ROW, column=0, sticky=E)
            self._data_path_entry = Entry(self, width=55)
            self._data_path_entry.grid(row=_DATA_ROW, column=1, sticky=E + W)
            self._choose_data_button = Button(self, command=self.choose_data, text='Choose...')
            self._choose_data_button.grid(row=_DATA_ROW, column=2)
            self.data_path = data_path

            # Validate.
            self._validate_button = Button(self, command=self.validate, text='Validate')
            self._validate_button.grid(row=_VALIDATE_BUTTON_ROW, column=0, padx=_PADDING, pady=_PADDING)

            # Validation status text.
            self._validation_status_text = StringVar()
            validation_status_label = Label(self, textvariable=self._validation_status_text)
            validation_status_label.grid(row=_VALIDATE_BUTTON_ROW, column=1)

            # Validation result.
            validation_report_frame = LabelFrame(self, text='Validation report')
            validation_report_frame.grid(row=_VALIDATION_REPORT_ROW, columnspan=3, sticky=E + N + S + W)
            validation_report_frame.grid_columnconfigure(0, weight=1)
            validation_report_frame.grid_rowconfigure(0, weight=1)
            self._validation_report_text = Text(validation_report_frame)
            self._validation_report_text.grid(column=0, row=0, sticky=E + N + S)
            _validation_report_scrollbar = Scrollbar(validation_report_frame)
            _validation_report_scrollbar.grid(column=1, row=0, sticky=N + S + W)
            _validation_report_scrollbar.config(command=self._validation_report_text.yview)
            self._validation_report_text.config(yscrollcommand=_validation_report_scrollbar.set)

            # Set up file dialogs.
            self._choose_cid_dialog = Open(
                initialfile=self.cid_path,
                title='Choose CID',
            )
            self._choose_data_dialog = Open(
                initialfile=self.data_path,
                title='Choose data',
            )
            self._save_log_as_dialog = SaveAs(
                defaultextension='.log',
                initialfile='cutplace.log',
                title='Save validation result',
            )

            menubar = Menu(master)
            master.config(menu=menubar)
            self._file_menu = Menu(menubar, tearoff=False)
            self._file_menu.add_command(command=self.choose_cid, label='Choose CID...')
            self._file_menu.add_command(command=self.choose_data, label='Choose data...')
            self._file_menu.add_command(command=self.save_validation_report_as, label='Save validation report as...')
            self._file_menu.add_command(command=self.quit, label='Quit')
            menubar.add_cascade(label='File', menu=self._file_menu)
            help_menu = Menu(menubar, tearoff=False)
            help_menu.add_command(command=self.show_about, label='About')
            menubar.add_cascade(label='Help', menu=help_menu)

            self._enable_usable_widgets()

        def _enable_usable_widgets(self):
            def state_for(possibly_empty_text):
                if (possibly_empty_text is not None) and (possibly_empty_text.rstrip() != ''):
                    result = 'normal'
                else:
                    result = 'disabled'
                return result

            def set_state(widget_to_set_state_for, possibly_empty_text):
                widget_to_set_state_for.config(state=state_for(possibly_empty_text))

            set_state(self._validate_button, self.cid_path)
            set_state(self._validation_report_text, self.validation_report)
            set_state(self._data_path_entry, self.cid_path)
            set_state(self._choose_data_button, self.cid_path)

            cid_path_state = state_for(self.cid_path)
            self._file_menu.entryconfig(_CHOOSE_DATA_PATH_MENU_INDEX, state=cid_path_state)
            self._file_menu.entryconfig(_SAVE_VALIDATION_REPORT_AS_MENU_INDEX, state=state_for(self.validation_report))

        def choose_cid(self):
            """
            Open a dialog to set the CID path.
            """
            cid_path = self._choose_cid_dialog.show()
            if cid_path != '':
                self.cid_path = cid_path
                self._enable_usable_widgets()

        def choose_data(self):
            """
            Open a dialog to set the data path.
            """
            data_path = self._choose_data_dialog.show()
            if data_path != '':
                self.data_path = data_path
                self._enable_usable_widgets()

        def save_validation_report_as(self):
            """
            Open a dialog to set specify where the validation results should be
            stored and write to this file.
            """
            validation_report_path = self._save_log_as_dialog.show()
            if validation_report_path != '':
                try:
                    with io.open(validation_report_path, 'w', encoding='utf-8') as validation_result_file:
                        validation_result_file.write(self._validation_report_text.get(1.0, END))
                except Exception as error:
                    showerror('Cutplace error', 'Cannot save validation results:\n%s' % error)

        def quit(self):
            self._master.destroy()

        def show_about(self):
            showinfo('Cutplace', 'Version ' + __version__)

        def clear_validation_report_text(self):
            """
            Clear the text area containing the validation results.
            """
            self._validation_report_text.configure(state='normal')
            self._validation_report_text.delete(1.0, END)
            self._validation_report_text.see(END)
            self._enable_usable_widgets()

        def _cid_path(self):
            return self._cid_path_entry.get()

        def _set_cid_path(self, value):
            self._cid_path_entry.delete(0, END)
            if value is not None:
                self._cid_path_entry.insert(0, value)

        cid_path = property(_cid_path, _set_cid_path, None, 'Path of the CID to use for validation')

        def _data_path(self):
            return self._data_path_entry.get()

        def _set_data_path(self, value):
            self._data_path_entry.delete(0, END)
            if value is not None:
                self._data_path_entry.insert(0, value)

        data_path = property(_data_path, _set_data_path, None, 'Path of the data to validate')

        @property
        def validation_report(self):
            return self._validation_report_text.get(0.0, END)

        def validate(self):
            """
            Validate the CID and (if specified) data file and update the
            :py:attr:`validation_result`. Show any errors unrelated to data in a
            dialog.
            """
            assert self.cid_path != ''

            def add_log_line(line):
                self._validation_report_text.config(state=NORMAL)
                try:
                    self._validation_report_text.insert(END, line + '\n')
                    self._validation_report_text.see(END)
                finally:
                    self._validation_report_text.config(state=DISABLED)

            def add_log_error_line(line_or_error):
                add_log_line('ERROR: %s' % line_or_error)

            def show_status_line(line):
                self._validation_status_text.set(line)
                self.master.update()

            assert self.cid_path != ''

            cid_name = os.path.basename(self.cid_path)
            self.clear_validation_report_text()
            add_log_line('%s: validating' % cid_name)
            self._enable_usable_widgets()
            cid = None
            try:
                cid = interface.Cid(self.cid_path)
                add_log_line('%s: ok' % cid_name)
            except errors.InterfaceError as error:
                add_log_error_line(error)
            except Exception as error:
                add_log_error_line('cannot read CID: %s' % error)

            if (cid is not None) and (self.data_path != ''):
                try:
                    data_name = os.path.basename(self.data_path)
                    add_log_line('%s: validating' % data_name)
                    validator = validio.Reader(cid, self.data_path, on_error='yield')
                    show_status_line('Validation started')
                    last_update_time = time.time()
                    for row_or_error in validator.rows():
                        now = time.time()
                        if (now - last_update_time) >= 3:
                            last_update_time = now
                            show_status_line(
                                '%d rows validated' % (validator.accepted_rows_count + validator.rejected_rows_count))
                        if isinstance(row_or_error, errors.DataError):
                            add_log_error_line(row_or_error)
                    show_status_line(
                        '%d rows validated - finished' % (validator.accepted_rows_count + validator.rejected_rows_count))
                    add_log_line(
                        '%s: %d rows accepted, %d rows rejected'
                        % (data_name, validator.accepted_rows_count, validator.rejected_rows_count))
                except Exception as error:
                    add_log_error_line('cannot validate data: %s' % error)
 def my_asksaveasfilename(self):   
     if not self.saveDialog:
        self.saveDialog = SaveAs(initialdir=self.startfiledir,
                                 filetypes=self.ftypes)
     return self.saveDialog.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()
class TextEditor:                        
    startfiledir = '.'                
    editwindows  = []                    

    if __name__ == '__main__':
        from textConfig import (              
            opensAskUser, opensEncoding,
            savesUseKnownEncoding, savesAskUser, savesEncoding)
    else:
        from .textConfig import (              
            opensAskUser, opensEncoding,
            savesUseKnownEncoding, savesAskUser, savesEncoding)

    ftypes = [('All files',     '*'),             
              ('Text files',   '.txt'),              
              ('Python files', '.py')]                

    colors = [{'fg':'black',      'bg':'white'},    
              {'fg':'yellow',     'bg':'black'},     
              {'fg':'white',      'bg':'blue'},      
              {'fg':'black',      'bg':'beige'},    
              {'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'),
              ('courier',   12+FontScale, 'normal'),  
              ('courier',   10+FontScale, 'bold'),    
              ('courier',   10+FontScale, 'italic'),  
              ('times',     10+FontScale, 'normal'),  
              ('helvetica', 10+FontScale, 'normal'),
              ('ariel',     10+FontScale, 'normal'),
              ('system',    10+FontScale, 'normal'),
              ('courier',   20+FontScale, 'normal')]

    def __init__(self, loadFirst='', loadEncode=''):
        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.knownEncoding = None                
        self.text.focus()                          
        if loadFirst:
            self.update()                        
            self.onOpen(loadFirst, loadEncode)

    def start(self):                               
        self.menuBar = [                           
            ('File', 0,                           
                 [('Open...',    0, self.onOpen),   
                  ('Save',       0, self.onSave),   
                  ('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),
                  ('Grep...',    3, self.onGrep)]
            ),
            ('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):                          
        name = Label(self, bg='black', fg='white')  
        name.pack(side=TOP, fill=X)                 
                                                    
        vbar  = Scrollbar(self)
        hbar  = Scrollbar(self, orient='horizontal')
        text  = Text(self, padx=5, wrap='none')       
        text.config(undo=1, autoseparators=1)          

        vbar.pack(side=RIGHT,  fill=Y)
        hbar.pack(side=BOTTOM, fill=X)               
        text.pack(side=TOP,    fill=BOTH, expand=YES) 

        text.config(yscrollcommand=vbar.set)    
        text.config(xscrollcommand=hbar.set)
        vbar.config(command=text.yview)        
        hbar.config(command=text.xview)         
        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

    def my_askopenfilename(self):      
        if not self.openDialog:
           self.openDialog = Open(initialdir=self.startfiledir,
                                  filetypes=self.ftypes)
        return self.openDialog.show()

    def my_asksaveasfilename(self):   
        if not self.saveDialog:
           self.saveDialog = SaveAs(initialdir=self.startfiledir,
                                    filetypes=self.ftypes)
        return self.saveDialog.show()

    def onOpen(self, loadFirst='', loadEncode=''):

        if self.text_edit_modified(): 
            if not askyesno('SimpleEditor', 'Text has changed: discard changes?'):
                return

        file = loadFirst or self.my_askopenfilename()
        if not file: 
            return
        
        if not os.path.isfile(file):
            showerror('SimpleEditor', 'Could not open file ' + file)
            return
        text = None   
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):    
                pass

        # try user input, prefill with next choice as default
        if text == None and self.opensAskUser:
            self.update()  
            askuser = askstring('SimpleEditor', 'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding or 
                                              sys.getdefaultencoding() or ''))
            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass
        if text == None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass

        if text == None:
            try:
                text = open(file, 'r', encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass
        if text == None:
            try:
                text = open(file, 'rb').read()         
                text = text.replace(b'\r\n', b'\n')    # for display, saves
                self.knownEncoding = None
            except IOError:
                pass

        if text == None:
            showerror('SimpleEditor', 'Could not decode and open file ' + file)
        else:
            self.setAllText(text)
            self.setFileName(file)
            self.text.edit_reset()            
            self.text.edit_modified(0)       

    def onSave(self):
        self.onSaveAs(self.currfile)  # may be None

    def onSaveAs(self, forcefile=None):

        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return

        text = self.getAllText()     
        encpick = None                # even if read/inserted as bytes 

        # try known encoding at latest Open or Save, if any
        if self.knownEncoding and (                                  # enc known?
           (forcefile     and self.savesUseKnownEncoding >= 1) or    # on Save?
           (not forcefile and self.savesUseKnownEncoding >= 2)):     # on SaveAs?
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass

        if not encpick and self.savesAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('SimpleEditor', 'Enter Unicode encoding for save',
                                initialvalue=(self.knownEncoding or 
                                              self.savesEncoding or 
                                              sys.getdefaultencoding() or ''))
            if askuser:
                try:
                    text.encode(askuser)
                    encpick = askuser
                except (UnicodeError, LookupError):    # LookupError:  bad name 
                    pass                               
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass

        # try platform default (utf8 on windows)
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass
        if not encpick:
            showerror('SimpleEditor', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('SimpleEditor', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)          
                self.text.edit_modified(0)       
                self.knownEncoding = encpick        
                                                    
    def onNew(self):
        if self.text_edit_modified():    
            if not askyesno('SimpleEditor', 'Text has changed: discard changes?'):
                return
        self.setFileName(None)
        self.clearAllText()
        self.text.edit_reset()                
        self.text.edit_modified(0)             
        self.knownEncoding = None              #

    def onQuit(self):
        assert False, 'onQuit must be defined in window-specific sublass' 

    def text_edit_modified(self):
        return self.text.edit_modified()

    def onUndo(self):                         
        try:                                   
            self.text.edit_undo()               # exception if stacks empty
        except TclError:                        # menu tear-offs for quick undo
            showinfo('SimpleEditor', 'Nothing to undo')

    def onRedo(self):                           
        try:
            self.text.edit_redo()
        except TclError:
            showinfo('SimpleEditor', 'Nothing to redo')

    def onCopy(self):                           # get text selected by mouse, etc.
        if not self.text.tag_ranges(SEL):      
            showerror('SimpleEditor', '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('SimpleEditor', 'No text selected')
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', '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('SimpleEditor', '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

    def onGoto(self, forceline=None):
        line = forceline or askinteger('SimpleEditor', '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('SimpleEditor', 'Bad line number')

    def onFind(self, lastkey=None):
        key = lastkey or askstring('SimpleEditor', 'Enter search string')
        self.text.update()
        self.text.focus()
        self.lastfind = key
        if key:                                                   
            nocase = configs.get('caseinsens', True)           
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:                                      
                showerror('SimpleEditor', 'String not found')
            else:
                pastkey = where + '+%dc' % len(key)           
                self.text.tag_remove(SEL, '1.0', END)        
                self.text.tag_add(SEL, where, pastkey)       
                self.text.mark_set(INSERT, pastkey)          
                self.text.see(where)                         

    def onRefind(self):
        self.onFind(self.lastfind)

    def onChange(self):
 
        new = Toplevel(self)
        new.title('SimpleEditor - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0, column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1, column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():                         # use my entry in enclosing scope   
            self.onFind(entry1.get())         # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find',  command=onFind ).grid(row=0, column=2, sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1, column=2, sticky=EW)
        new.columnconfigure(1, weight=1)      # expandable entries

    def onDoChange(self, findtext, changeto):
        if self.text.tag_ranges(SEL):                     
            self.text.delete(SEL_FIRST, SEL_LAST)          
            self.text.insert(INSERT, changeto)           
            self.text.see(INSERT)
            self.onFind(findtext)                          
            self.text.update()                           

    def onGrep(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel()
        popup.title('SimpleEditor - grep')
        var1 = makeFormRow(popup, label='Directory root',   width=18, browse=False)
        var2 = makeFormRow(popup, label='Filename pattern', width=18, browse=False)
        var3 = makeFormRow(popup, label='Search string',    width=18, browse=False)
        var1.set('.')      
        var2.set('*.py')  
        Button(popup, text='Go',
           command=lambda: self.onDoGrep(var1.get(), var2.get(), var3.get())).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey):
        import threading, queue
        mypopup = Tk()
        mypopup.title('SimpleEditor - grepping')
        status = Label(mypopup, text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, myqueue)
        threading.Thread(target=self.grepThreadProducer, args=threadargs).start()
        self.grepThreadConsumer(grepkey, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, myqueue):
        from PP4E.Tools.find import find
        matches = []
        for filepath in find(pattern=filenamepatt, startdir=dirname):
            try:
                for (linenum, linestr) in enumerate(open(filepath)):
                    if grepkey in linestr:
                        message = '%s@%d  [%s]' % (filepath, linenum + 1, linestr)
                        matches.append(message)
            except UnicodeDecodeError:
                print('Unicode error in:', filepath)
        myqueue.put(matches)


    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 x 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 SimpleEditor', helptext % ((Version,)*2))
Example #26
0
 def Save_As():
     global sgpas
     path.set(
         SaveAs(title="Save File",
                initialdir="C:/",
                filetypes=(("Excel Workbook", "*.xlsx"),
                           ("All Files", "*.*"))) +
         ".xlsx")
     if (path.get() != ""):
         XL_File = Workbook("%s" % (path.get()))
         keys = list(sgpas.keys())
         s1 = XL_File.add_worksheet("%s to %s" %
                                    (keys[0], keys[-1]))
         s1.write("A1", "Roll Num")
         s1.write("B1", "Result")
         roll_num, res = [], []
         for i in keys:
             roll_num.append(i)
             x = XL_File.add_worksheet("%s" % (i))
             x.write("C1", i)
             x.write("A2", "Subject Code")
             x.write("B2", "Subject Name")
             x.write("C2", "Grade / Status")
             x.write("D2", "Credits")
             sgpa, fails = 0, 0
             gra, crs = [], []
             for j in range(len(sgpas[i])):
                 l = ["A", "B", "C", "D"]
                 for k in sgpas[i][j]:
                     ind = l[sgpas[i][j].index(k)]
                     x.write("%s%i" % (ind, j + 3),
                             "%s" % (k))
                     if (l.index(ind) == 2):
                         if (k == "O"): gra.append(10)
                         elif (k == "S"): gra.append(9)
                         elif (k == "A"): gra.append(8)
                         elif (k == "B"): gra.append(7)
                         elif (k == "C"): gra.append(6)
                         elif (k == "D"): gra.append(5)
                         elif (k == "F"): fails += 1
                         else: gra.append(0)
                     if (l.index(ind) == 3):
                         crs.append(int(k))
             for z in range(len(gra)):
                 sgpa += gra[z] * crs[z]
             try:
                 if (fails <= 0):
                     Y = sgpa / sum(crs)
                     res.append(Y)
                     x.write("C%i" % (j + 4), "SGPA:")
                     x.write("D%i" % (j + 4),
                             "%.3f" % (Y))
                 else:
                     x.write("D%i" % (j + 4), "Failed!")
                     res.append("Failed!")
             except Exception:
                 pass
             for i in range(len(roll_num)):
                 s1.write("A%i" % (i + 2), roll_num[i])
                 s1.write("B%i" % (i + 2), res[i])
         XL_File.close()
         mb.showinfo("Success :)",
                     "File Saved Successfully...")
     else:
         path.set("Please Try Again...")
Example #27
0
class TextEditor:  # mix with menu/toolbar Frame class
    startfiledir = '.'  # for dialogs
    editwindows = []  # for process-wide quit check

    # Unicode configurations
    # imported in class to allow overrides in subclass or self
    if __name__ == '__main__':

        pass
    else:

        pass

    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='', loadEncode=''):
        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.knownEncoding = None  # 2.1 Unicode: till Open or Save
        self.text.focus()  # else must click in text
        if loadFirst:
            self.update()  # 2.1: else @ line 2; see book
            self.onOpen(loadFirst, loadEncode)

    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),
                           ('Replace...', 0, self.onChange),
                           ('Grep...', 3, self.onGrep)]),
            ('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
        # GuiMaker frame packs itself
        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='none')  # disable line wrapping
        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='', loadEncode=''):
        """
        tests if file is okay ahead of time to try to avoid opens;
        we could also load and manually decode bytes to str to avoid
        multiple open attempts, but this is unlikely to try all cases;

        encoding behavior is configurable in the local textConfig.py:
        1) tries known type first if passed in by client (email_self charsets)
        2) if opensAskUser True, try user input next (prefill wih defaults)
        3) if opensEncoding nonempty, try this encoding next: 'latin-1', etc.
        4) tries sys.getdefaultencoding() platform default next
        5) uses binary mode bytes and Tk policy as the last resort
        """

        if self.text_edit_modified():  # 2.0
            if not askyesno('PyEdit', 'Text has changed: discard changes?'):
                return

        file = loadFirst or self.my_askopenfilename()
        if not file:
            return

        if not os.path.isfile(file):
            showerror('PyEdit', 'Could not open file ' + file)
            return

        # try known encoding if passed and accurate (e.g., email_self)
        text = None  # empty file = '' = False: test for None!
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):  # lookup: bad name
                pass

        # try user input, prefill with next choice as default
        if text is None and self.opensAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('PyEdit',
                                'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            self.text.focus()  # else must click
            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass

        # try config file (or before ask user?)
        if text is None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass

        # try platform default (utf-8 on windows; try utf8 always?)
        if text is None:
            try:
                text = open(file, 'r',
                            encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass

        # last resort: use binary bytes and rely on Tk to decode
        if text is None:
            try:
                text = open(file, 'rb').read()  # bytes for Unicode
                text = text.replace(b'\r\n', b'\n')  # for display, saves
                self.knownEncoding = None
            except IOError:
                pass

        if text is None:
            showerror('PyEdit', 'Could not decode and 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):
        """
        retains successful encoding name here for next save, because this
        may be the first Save after New or a manual text insertion;  Save
        and SaveAs may both use last known encoding, per config file (it
        probably should be used for Save, but SaveAs usage is unclear);
        gui prompts are prefilled with the known encoding if there is one;

        does manual text.encode() to avoid creating file; text mode files
        perform platform specific end-line conversion: Windows \r dropped
        if present on open by text mode (auto) and binary mode (manually);
        if manual content inserts, must delete \r else duplicates here;
        knownEncoding=None before first Open or Save, after New, if binary Open;

        encoding behavior is configurable in the local textConfig.py:
        1) if savesUseKnownEncoding > 0, try encoding from last open or save
        2) if savesAskUser True, try user input next (prefill with known?)
        3) if savesEncoding nonempty, try this encoding next: 'utf-8', etc
        4) tries sys.getdefaultencoding() as a last resort
        """

        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return

        text = self.getAllText()  # 2.1: a str string, with \n eolns,
        encpick = None  # even if read/inserted as bytes

        # try known encoding at latest Open or Save, if any
        if self.knownEncoding and (  # enc known?
            (forcefile and self.savesUseKnownEncoding >= 1) or  # on Save?
            (not forcefile and self.savesUseKnownEncoding >= 2)):  # on SaveAs?
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass

        # try user input, prefill with known type, else next choice
        if not encpick and self.savesAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('PyEdit',
                                'Enter Unicode encoding for save',
                                initialvalue=(self.knownEncoding
                                              or self.savesEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            self.text.focus()  # else must click
            if askuser:
                try:
                    text.encode(askuser)
                    encpick = askuser
                except (UnicodeError, LookupError):  # LookupError:  bad name
                    pass  # UnicodeError: can't encode

        # try config file
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass

        # try platform default (utf8 on windows)
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass

        # open in text mode for endlines + encoding
        if not encpick:
            showerror('PyEdit', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('PyEdit', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)  # may be newly created
                self.text.edit_modified(0)  # 2.0: clear modified flag
                self.knownEncoding = encpick  # 2.1: keep enc for next save
                # don't clear undo/redo stks!

    def onNew(self):
        """
        start editing a new file from scratch in current window;
        see onClone to pop-up a new independent edit window instead;
        """
        if self.text_edit_modified():  # 2.0
            if not askyesno('PyEdit', 'Text has changed: discard changes?'):
                return
        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
        self.knownEncoding = None  # 2.1: Unicode type unknown

    def onQuit(self):
        """
        on Quit menu/toolbar select and wm border X button in toplevel windows;
        2.1: don't exit app if others changed;  2.0: don't ask if self unchanged;
        moved to the top-level window classes at the end since may vary per usage:
        a Quit in GUI might quit() to exit, destroy() just one Toplevel, Tk, or
        edit frame, or not be provided at all when run as an attached component;
        check self for changes, and if might quit(), main windows should check
        other windows in the process-wide list to see if they have changed too;
        """
        assert False, 'onQuit must be defined in window-specific sublass'

    def text_edit_modified(self):
        """
        2.1: this now works! seems to have been a bool result type issue in tkinter;
        2.0: self.text.edit_modified() broken in Python 2.4: do manually for now;
        """
        return self.text.edit_modified()
        # 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', True)  # 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):
        """
        non-modal find/change dialog
        2.1: pass per-dialog inputs to callbacks, may be > 1 change dialog open
        """
        new = Toplevel(self)
        new.title('PyEdit - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0,
                                                                   column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1,
                                                                   column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():  # use my entry in enclosing scope
            self.onFind(entry1.get())  # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0,
                                                      column=2,
                                                      sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1,
                                                        column=2,
                                                        sticky=EW)
        new.columnconfigure(1, weight=1)  # expandable entries

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):  # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)  # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)  # goto next appear
            self.text.update()  # force refresh

    def onGrep(self):
        """
        TBD: better to issue an error if any file fails to decode?
        but utf-16 2-bytes/char format created in Notepad may decode
        without error per utf-8, and search strings won't be found;
        TBD: could allow input of multiple encoding names, split on
        comma, try each one for every file, without open loadEncode?
        """
        from minghu6.gui.formrows import makeFormRow

        # nonmodal dialog: get dirnname, filenamepatt, grepkey
        popup = Toplevel()
        popup.title('PyEdit - grep')
        var1 = makeFormRow(popup,
                           label='Directory root',
                           width=18,
                           browse=False)
        var2 = makeFormRow(popup,
                           label='Filename pattern',
                           width=18,
                           browse=False)
        var3 = makeFormRow(popup,
                           label='Search string',
                           width=18,
                           browse=False)
        var4 = makeFormRow(popup,
                           label='Content encoding',
                           width=18,
                           browse=False)
        var1.set('.')  # current dir
        var2.set('*.py')  # initial values
        var4.set(sys.getdefaultencoding())  # for file content, not filenames
        cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(),
                                   var4.get())
        Button(popup, text='Go', command=cb).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey, encoding):
        """
        on Go in grep dialog: populate scrolled list with matches
        tbd: should producer thread be daemon so it dies with app?
        """
        import threading, queue

        # make non-modal un-closeable dialog
        mypopup = Tk()
        mypopup.title('PyEdit - grepping')
        status = Label(mypopup,
                       text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close

        # start producer thread, consumer loop
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue)
        threading.Thread(target=self.grepThreadProducer,
                         args=threadargs).start()
        self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding,
                           myqueue):
        """
        in a non-GUI parallel thread: queue find.find results list;
        could also queue matches as found, but need to keep window;
        file content and file names may both fail to decode here;

        TBD: could pass encoded bytes to find() to avoid filename
        decoding excs in os.walk/listdir, but which encoding to use:
        sys.getfilesystemencoding() if not None?  see also Chapter6
        footnote issue: 3.1 fnmatch always converts bytes per Latin-1;
        """
        from minghu6.etc.find import find
        matches = []
        try:
            for filepath in find(pattern=filenamepatt, startdir=dirname):
                try:
                    textfile = open(filepath, encoding=encoding)
                    for (linenum, linestr) in enumerate(textfile):
                        if grepkey in linestr:
                            msg = '%s@%d  [%s]' % (filepath, linenum + 1,
                                                   linestr)
                            matches.append(msg)
                except UnicodeError as X:
                    print('Unicode error in:', filepath, X)  # eg: decode, bom
                except IOError as X:
                    print('IO error in:', filepath, X)  # eg: permission
        finally:
            myqueue.put(matches)  # stop consumer loop on find excs: filenames?

    def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup):
        """
        in the main GUI thread: watch queue for results or [];
        there may be multiple active grep threads/loops/queues;
        there may be other types of threads/checkers in process,
        especially when PyEdit is attached component (PyMailGUI);
        """
        import queue
        try:
            matches = myqueue.get(block=False)
        except queue.Empty:
            myargs = (grepkey, encoding, myqueue, mypopup)
            self.after(250, self.grepThreadConsumer, *myargs)
        else:
            mypopup.destroy()  # close status
            self.update()  # erase it now
            if not matches:
                showinfo('PyEdit', 'Grep found no matches for: %r' % grepkey)
            else:
                self.grepMatchesList(matches, grepkey, encoding)

    def grepMatchesList(self, matches, grepkey, encoding):
        """
        populate list after successful matches;
        we already know Unicode encoding from the search: use
        it here when filename clicked, so open doesn't ask user;
        """
        from minghu6.gui.scrolledlist import ScrolledList
        print('Matches for %s: %s' % (grepkey, len(matches)))

        # catch list double-click
        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):
                file, line = selection.split('  [', 1)[0].split('@')
                editor = TextEditorMainPopup(loadFirst=file,
                                             winTitle=' grep match',
                                             loadEncode=encoding)
                editor.onGoto(int(line))
                editor.text.focus_force()  # no, really

        # new non-modal widnow
        popup = Tk()
        popup.title('PyEdit - grep matches: %r (%s)' % (grepkey, encoding))
        ScrolledFilenames(parent=popup, options=matches)

    ############################################################################
    # 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):
        """
        pop-up dialog giving text statistics and cursor location;
        caveat : Tk insert position column counts a tab as one
        character: translate to next multiple of 8 to match visual?
        """
        text = self.getAllText()  # added on 5/3/00 in 15 mins
        bytes = len(text)  # words uses a simple guess:
        lines = len(text.split('\n'))  # any separated by whitespace
        words = len(text.split())  # 3.x: bytes is really chars
        index = self.text.index(INSERT)  # str is unicode code points
        where = tuple(index.split('.'))
        showinfo(
            'PyEdit Information',
            'Current location:\n\n' + 'line:\t%s\ncolumn:\t%s\n\n' % where +
            'File text statistics:\n\n' +
            'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self, makewindow=True):
        """
        open a new edit window without changing one already open (onNew);
        inherits quit and other behavior of the window that it clones;
        """
        if not makewindow:
            new = None  # assume class makes its own window
        else:
            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 PP4E 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 may be PyEdit's window;
        subprocess or multiprocessing modules may work here too;

        sometimes does not appear in rare cases;
        """
        def askcmdargs():
            return askstring('PyEdit', 'Commandline arguments?') or ''

        from launchmods import System, Start, StartArgs, Fork
        filemode = False
        thefile = str(self.getFileName())
        if os.path.exists(thefile):
            filemode = askyesno('PyEdit', 'Run from file?')
            self.update()  # 2.1: run update()
        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() + '\n', namespace)  # exceptions ignored
        elif self.text_edit_modified():  # 2.0: changed test
            showerror('PyEdit', 'Text changed: you must save before run')
        else:
            cmdargs = askcmdargs()
            mycwd = os.getcwd()  # cwd may be root
            dirname, filename = os.path.split(thefile)  # get dir, base
            os.chdir(dirname or mycwd)  # cd for filenames
            thecmd = filename + ' ' + cmdargs  # 2.1: not theFile
            if not parallelmode:  # run as file
                System(thecmd, thecmd)()  # block editor
            else:
                if sys.platform[:3] == 'win':  # spawn in parallel
                    run = StartArgs if cmdargs else Start  # 2.1: support args
                    run(thecmd, thecmd)()  # or always Spawn
                else:
                    Fork(thecmd, thecmd)()  # spawn in parallel
            os.chdir(mycwd)  # go back to my dir

    def onPickFont(self):
        """
        """
        from minghu6.gui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('PyEdit - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var1.set('courier')
        var2.set('14')  # suggested vals
        var3.set('bold italic')  # see pick list for valid inputs
        Button(popup,
               text='Apply',
               command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        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 str string

    def setAllText(self, text):
        """
        caller: call self.update() first if just packed, else the
        initial position may be at line 2, not line 1 (2.1; Tk bug?)
        """
        self.text.delete('1.0', END)  # store text string in widget
        self.text.insert(END, text)  # or '1.0'; text=bytes or str
        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):  # see also: onGoto(linenum)
        self.currfile = name  # for save
        self.filelabel.config(text=str(name))

    def setKnownEncoding(self, encoding='utf-8'):  # for saves if inserted
        self.knownEncoding = encoding  # else saves use config, ask?

    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 x 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)
Example #28
0
class TextEditor:
    startfiledir = '.'
    editwindows = []
    iconpatt = '*.ico'
    iconmine = 'pad.ico'
    helpButton = False

    # Unicode configurations
    # imported in class to allow overrides in subclass or self

    if __name__ == "__main__":
        from textConfig import (openAskUser, openEncoding, savesAskUser,
                                savesEncoding, savesUseKnownEncoding)

    else:
        from p_python.TextEditor.textConfig import (openEncoding, openEncoding,
                                                    savesUseKnownEncoding,
                                                    savesEncoding,
                                                    savesAskUser)

    ftypes = [('All files', '*'), ('Text Documents (*.txt)', '*.txt'),
              ('Python files (*.py)', '.py')]

    colours = [{
        'fg': 'black',
        'bg': 'white'
    }, {
        'fg': 'yellow',
        'bg': 'black'
    }, {
        'fg': 'white',
        'bg': 'blue'
    }, {
        'fg': 'black',
        'bg': 'beige'
    }, {
        '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'),
             ('courier', 12 + FontScale, 'normal'),
             ('courier', 10 + FontScale, 'bold'),
             ('courier', 10 + FontScale, 'italic'),
             ('times', 10 + FontScale, 'normal'),
             ('helvetica', 10 + FontScale, 'normal'),
             ('ariel', 10 + FontScale, 'normal'),
             ('system', 10 + FontScale, 'normal'),
             ('courier', 20 + FontScale, 'normal')]

    def __init__(self, loadFirst='', loadEncode=''):
        if not isinstance(self, GuiMaker):
            raise TypeError('TextEditor needs a GuiMaker mixin')
        self.setFileName(None)
        #self.currfile = None
        self.lastFind = None
        self.openDialog = None
        self.saveDialog = None
        self.knownEncoding = None
        self.text.focus()

        if loadFirst:
            self.update()
            self.onOpen(loadFirst, loadEncode)

    def start(self):
        self.menuBar = [
            ('File', 0,
             [('New                                Ctrl+N', 0, self.onNew),
              ('New Window      Ctrl+Shift+N', 1, self.onClone),
              ('Open...                           Ctrl+O', 0, self.onOpen),
              ('Save                                Ctrl+S', 0, self.onSave),
              ('Save As...               Ctrl+Shif+S', 5, self.onSaveAs),
              'separator', ('Page Setup...        ', 2, self.notDone),
              ('Print...                            Ctrl+P', 0, self.onPrint),
              ('Exit', 0, self.onQuit)]),
            ('Edit', 0,
             [('Undo                         Ctrl+Z', 0, self.onUndo),
              'separator',
              ('Cut                            Ctrl+X', 0, self.onCut),
              ('Copy                         Ctrl+C', 0, self.onCopy),
              ('Paste                         Ctrl+V', 0, self.onPaste),
              ('Delete                           Del', 0, self.onDelete),
              'separator', ('Search with Bing...  Ctrl+E', 1, self.notDone),
              ('Find...                        Ctrl+F', 0, self.onFind),
              ('Find Next                        F3', 1, self.notDone),
              ('Find Previous        Shift+F3', 1, self.notDone),
              ('Replace...                 Ctrl+H', 0, self.onReplace),
              ('Go To...                     Ctrl+G', 0, self.onGoto),
              'separator',
              ('Select All                   Ctrl+A', 0, self.onSelectAll),
              ('Time/Date                        F5', 0, self.onTime)]),
            ('Format', 0, [('Word wrap            ', 0, self.notDone),
                           ('Font...', 0, self.onFont),
                           ('Pick Bg...', 0, self.onBg),
                           ('Pick Fg...', 0, self.onFg)]),
            ('View', 0, [('Zoom', 0, [
                ('Zoom In                          Ctrl+Plus', 1,
                 self.notDone),
                ('Zoom Out                   Ctrl+Minus', 1, self.notDone),
                ('Restore Default Zoom       Ctrl+0', 1, self.notDone)
            ]), ('Status Bar', 0, self.notDone)]),
            ('Help', 0, [('View Help', 0, self.notDone),
                         ('Send Feedback', 0, self.notDone), 'separator',
                         ('About PyNote', 0, self.help)])
        ]

    def makeWidgets(self):
        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='none')
        text.config(undo=1, autoseparators=1)

        vbar.pack(side=RIGHT, fill=Y)
        hbar.pack(side=BOTTOM, fill=X)

        text.pack(side=TOP, fill=BOTH, expand=YES)

        text.config(yscrollcommand=vbar.set)
        text.config(xscrollcommand=hbar.set)

        vbar.config(command=text.yview)
        hbar.config(command=text.xview)

        # apply user configurations or default
        startfont = configs.get('font', self.fonts[0])
        startbg = configs.get('bg', self.colours[0]['bg'])
        startfg = configs.get('fg', self.colours[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.text.bind('<Control-s>', self.onSave)
        self.text.bind('<Control-p>', self.onPrint)
        self.text.bind('<Control-f>', self.onFind)
        self.text.bind('<Control-g>', self.onGoto)
        self.text.bind('<Control-h>', self.onReplace)

    # File menu commands
    '''def my_askopenfilename(self):
        if not self.openDialog:
            self.openDialog = Open(initialdir=self.startfiledir,
                                    filetypes=self.ftypes)
            return self.openDialog.show()'''

    def my_asksaveasfilename(self):
        if not self.saveDialog:
            self.saveDialog = SaveAs(initialdir=self.startfiledir,
                                     filetypes=self.ftypes)
            return self.saveDialog.show()

    def onOpen(self, loadFirst='', loadEncode=''):
        """
        Open file from system
        """
        def Open():
            global file
            file = loadFirst or askopenfilename(initialdir=self.startfiledir,
                                                filetypes=self.ftypes)
            if not file:
                return
            if not os.path.isfile(file):
                showerror('PyNote', 'Could not open file' + file)
                return

        if self.text.edit_modified():
            if not askyesno('PyNote', 'Save changes to file?'):
                Open()
            else:
                self.onSave()
                self.text.edit_modified(0)

        if not self.text.edit_modified():
            Open()

        # try known encoding if passed and acurate
        text = None
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):
                pass

        # try user input, prefill with next choice as default
        if text == None and self.openAskUser:
            self.update()
            askuser = askstring('PyNote',
                                'Enter Unicode encoding for open',
                                initialvalue=(self.openEncoding
                                              or sys.getdefaultencoding()
                                              or ''))

            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass

        #try config file
        if text == None and self.openEncoding:
            try:
                text = open(file, 'r', encoding=self.openEncoding).read()
                self.knownEncoding = self.openEncoding
            except (UnicodeError, LookupError, IOError):
                pass

        #try platform default
        if text == None:
            try:
                text = open(file, 'r',
                            encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass

        # last resort: use binary bytes and rely on Tk to decode
        if text == None:
            try:
                text = open(file, 'rb').read()
                text.replace(b'\r\n', b'\n')
                self.knownEncoding = None
            except IOError:
                pass

        if text == None:
            showerror('PyNote', 'Could not decode and open file' + file)
        else:
            self.setAllText(text)
            self.setFileName(file)
            self.text.edit_reset()
            self.text.edit_modified(0)

    def onSave(self, event):
        """
        save file to system
        """
        filename = self.currfile or self.my_asksaveasfilename()
        #print(filename)

        if filename == None:
            filename = asksaveasfilename(initialdir=self.startfiledir,
                                         filetypes=self.ftypes)
            #print(str(filename)+'1')

        text = self.getAllText()
        enpick = None

        if filename:
            # try known encoding at latest Open or Save, if any
            if self.knownEncoding and (
                (self.currfile and self.savesUseKnownEncoding >= 1) or
                (not self.currfile and self.savesUseKnownEncoding >= 2)):

                try:
                    text.encode(self.knownEncoding)
                    enpick = self.knownEncoding
                except UnicodeError:
                    pass

            #try user input, prefill with known type, else next choice
            if not enpick and self.savesAskUser:
                self.update()
                askuser = askstring('PyNote',
                                    'Enter unicode encoding for save',
                                    initialvalue=(self.knownEncoding
                                                  or self.savesEncoding
                                                  or sys.getdefaultencoding()
                                                  or ''))
                if askuser:
                    try:
                        text.encode(askuser)
                        enpick = askuser
                    except (UnicodeError, LookupError):
                        pass

            #try config file
            if not enpick and self.savesEncoding:
                try:
                    text.encode(self.savesEncoding)
                    enpick = self.savesEncoding
                except (UnicodeError, LookupError):
                    pass

            #try platform default
            if not enpick:
                try:
                    text.encode(sys.getdefaultencoding())
                    enpick = sys.getdefaultencoding
                except (UnicodeError, LookupError):
                    pass

            # open in text mode for endlines + encoding
            if not enpick:
                showerror('PyNote', 'Could not encode for file' + filename)

            else:
                try:
                    file = open(filename, 'w', encoding=enpick)
                    file.write(text)
                    file.close()
                except:
                    showerror('PyNote', 'Could not save file' + filename)
                else:
                    self.setFileName(filename)
                    self.text.edit_modified(0)
                    self.knownEncoding = enpick

    def onSaveAs(self):
        """
        save file to system
        """
        filename = asksaveasfilename()

        if not filename:
            return

        text = self.getAllText()
        enpick = None

        # try known encoding at latest Open or Save, if any
        if self.knownEncoding and ((self.savesUseKnownEncoding >= 1) or
                                   (self.savesUseKnownEncoding >= 2)):

            try:
                text.encode(self.knownEncoding)
                enpick = self.knownEncoding
            except UnicodeError:
                pass

        #try user input, prefill with known type, else next choice
        if not enpick and self.savesAskUser:
            self.update()
            askuser = askstring('PyNote',
                                'Enter unicode encoding for save',
                                initialvalue=(self.knownEncoding
                                              or self.savesEncoding
                                              or sys.getdefaultencoding()
                                              or ''))
            if askuser:
                try:
                    text.encode(askuser)
                    enpick = askuser
                except (UnicodeError, LookupError):
                    pass

        #try config file
        if not enpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                enpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass

        #try platform default
        if not enpick:
            try:
                text.encode(sys.getdefaultencoding())
                enpick = sys.getdefaultencoding
            except (UnicodeError, LookupError):
                pass

        # open in text mode for endlines + encoding
        if not enpick:
            showerror('PyNote', 'Could not encode for file' + filename)

        else:
            try:
                file = open(filename, 'w', encoding=enpick)
                file.write(text)
                file.close()
            except:
                showerror('PyNote', 'Could not save file' + filename)
            else:
                self.setFileName(filename)
                self.text.edit_modified(0)
                self.knownEncoding = enpick

    def onNew(self):
        """
        start editing a new file from scratch in current window;
        """
        if self.text.edit_modified():
            if not askyesno('PyNote', 'Discard changes made to file?'):
                return
        self.setFileName(None)
        self.clearAllText()
        self.text.edit_reset()
        self.text.edit_modified(0)
        self.knownEncoding = None

    def onClone(self):
        """
        open a new edit window
        """
        from p_python.launchmodes import PortableLauncher
        PortableLauncher('PyNote', 'PyNote.py')()

    def onQuit(self):
        assert False, 'onQuit must be defined in window specific subclass'

    def onPrint(self, event):
        import win32api, time
        import win32print
        import tempfile
        from p_python.GUI.Tour.scrolledlist import ScrolledList

        printText = TextEditor.getAllText(self)
        print(str(printText))

        class Print(ScrolledList):
            def __init__(self, options, parent=None):
                Label(parent, text='Select Printer').pack(side=TOP,
                                                          fill=BOTH,
                                                          expand=YES)
                ScrolledList.__init__(self, options, parent)
                Button(parent,
                       text='Print',
                       command=(
                           lambda: self.runCommand(self.selection()))).pack()
                self.focus_get()
                #self.grab_set()
                self.wait_window()

            def runCommand(self, selection):
                filename = tempfile.mkstemp('.txt')[1]
                print(filename)
                open(filename, 'w').write(str(printText))
                win32api.ShellExecute(0, 'printto', filename,
                                      '"%s"' % win32print.GetDefaultPrinter(),
                                      '.', 0)
                '''time.sleep(5)
                os.remove(filename)'''

        master = Toplevel()
        master.title('Print')
        master.config(height=40, width=70)
        options = [i[2] for i in list(win32print.EnumPrinters(2))]
        #print(options)

        Print(options, master)

    # Edit menu commands
    def onUndo(self):
        try:
            self.text.edit_undo()
        except TclError:
            pass

    def onRedo(self):
        try:
            self.text.edit_redo()
        except TclError:
            pass

    def onCopy(self):
        if not self.text.tag_ranges(SEL):
            pass
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

    def onDelete(self):
        if not self.text.tag_ranges(SEL):
            pass
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            pass
        else:
            self.onCopy()
            self.onDelete()

    def onPaste(self):
        try:
            text = self.selection_get(selection='CLIPBOARD')
        except:
            pass
        self.text.insert(INSERT, text)
        self.text.tag_remove(SEL, '1.0', END)
        self.text.tag_add(SEL, INSERT + '%dc' % len(text), INSERT)
        self.text.see(INSERT)

    def onSelectAll(self):
        self.text.tag_add(SEL, '1.0', END + '-1c')
        self.text.mark_set(INSERT, '1.0')
        self.text.see(INSERT)

    def onGoto(self, event, forceline=None):
        """
        goes to a passes in line number
        """
        line = forceline or askinteger('PyNote', '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('PyNote',
                          'line number is beyond total numbers of lines')

    def onFind(self, event, lastkey=None):
        """
        Finds particular passed word in text
        """
        key = lastkey or askstring('PyNote', 'Enter search string')
        self.text.update()
        self.text.focus()
        self.lastFind = key
        if key:
            nocase = configs.get('caseinsens', True)
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:
                showerror('PyNote', 'word not found')
            else:
                #pastkey =
                #self.text.tag_remove(SEL, '1.0', END)
                self.text.see(where)

    def onReplace(self, event):
        """
        non-modal find/replace dialog
        """
        new = Toplevel()
        new.title('PyNote-Replace')
        Label(new, text='Find', width=15).grid(row=0, column=0)
        Label(new, text='Replace', width=15).grid(row=1, column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():
            self.onFind(entry1.get())

        def onApply():
            self.onDoReplace(entry1.get(), entry2.get())

        ttk.Button(new, text='Find', command=onFind).grid(row=0,
                                                          column=2,
                                                          sticky=EW)
        ttk.Button(new, text='Replace', command=onApply).grid(row=1,
                                                              column=2,
                                                              sticky=EW)
        new.columnconfigure(1, weight=1)

    def onDoReplace(self, findtext, replace):
        # replace and find next
        if self.text.tag_ranges(SEL):
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, replace)
            self.text.see(INSERT)
            self.onFind(findtext)
            self.text.update()

    def onTime(self):
        try:
            import time
            text = time.asctime()
        except:
            pass
        else:
            self.text.insert(END, text)

    # format menu command
    def onFont(self):
        """
        non-modal font input dialog
        """
        from p_python.GUI.shellgui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('PyNote-font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var1.set('courier')
        var2.set('12')
        var3.set('bold italic')
        b = Button(
            popup,
            text='apply',
            command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get()))
        b.pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        except:
            showerror('PyNote', 'Font is not registered.')

    def onFg(self):
        self.pickColour('fg')

    def onBg(self):
        self.pickColour('bg')

    def pickColour(self, part):
        (triple, hexstr) = askcolor()
        if hexstr:
            self.text.config(**{part: hexstr})

    def isEmpty(self):
        return not self.getAllText()

    def getAllText(self):
        return self.text.get('1.0', END + '-1c')

    def setAllText(self, text):
        self.text.delete('1.0', END)
        self.text.insert(END, text)
        self.text.mark_set(INSERT, '1.0')
        self.text.see(INSERT)

    def clearAllText(self):
        self.text.delete('1.0', END)

    def getFileName(self):
        return self.currfile

    def setFileName(self, name):
        self.currfile = name
        #self.filelabel.config

    def setKnownEncoding(self, encoding='utf-8'):
        self.knownEncoding = encoding

    def setBg(self, colour):
        self.text.config(bg=colour)

    def setFg(self, colour):
        self.text.config(fg=colour)

    def setFont(self, font):
        self.text.config(font=font)

    def setHeight(self, height):
        self.text.config(height=height)

    def setWidth(self, width):
        self.text.config(width=width)

    def clearModified(self):
        self.text.edit_modified(0)  # clear modified tag

    def isModified(self):
        return self.text.edit_modified()

    def help(self):
        showinfo('About sPyNote', helptext)

    def notDone(self):
        showerror('PyNote', 'Button not available')
Example #29
0
class TextEditor:  # mix with menu/toolbar Frame class
    startfiledir = '.'  # for dialogs
    editwindows = []  # for process-wide quit check

    # Unicode configurations
    # imported in class to allow overrides in subclass or self
    if __name__ == '__main__':

        pass
    else:

        pass

    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='', loadEncode=''):
        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.knownEncoding = None  # 2.1 Unicode: till Open or Save
        self.text.focus()  # else must click in text
        if loadFirst:
            self.update()  # 2.1: else @ line 2; see book
            self.onOpen(loadFirst, loadEncode)

    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),
              ('Replace...', 0, self.onChange),
              ('Grep...', 3, self.onGrep)]
             ),
            ('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
        # GuiMaker frame packs itself
        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='none')  # disable line wrapping
        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='', loadEncode=''):
        """
        tests if file is okay ahead of time to try to avoid opens;
        we could also load and manually decode bytes to str to avoid
        multiple open attempts, but this is unlikely to try all cases;

        encoding behavior is configurable in the local textConfig.py:
        1) tries known type first if passed in by client (email_self charsets)
        2) if opensAskUser True, try user input next (prefill wih defaults)
        3) if opensEncoding nonempty, try this encoding next: 'latin-1', etc.
        4) tries sys.getdefaultencoding() platform default next
        5) uses binary mode bytes and Tk policy as the last resort
        """

        if self.text_edit_modified():  # 2.0
            if not askyesno('PyEdit', 'Text has changed: discard changes?'):
                return

        file = loadFirst or self.my_askopenfilename()
        if not file:
            return

        if not os.path.isfile(file):
            showerror('PyEdit', 'Could not open file ' + file)
            return

        # try known encoding if passed and accurate (e.g., email_self)
        text = None  # empty file = '' = False: test for None!
        if loadEncode:
            try:
                text = open(file, 'r', encoding=loadEncode).read()
                self.knownEncoding = loadEncode
            except (UnicodeError, LookupError, IOError):  # lookup: bad name
                pass

        # try user input, prefill with next choice as default
        if text is None and self.opensAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('PyEdit', 'Enter Unicode encoding for open',
                                initialvalue=(self.opensEncoding or
                                              sys.getdefaultencoding() or ''))
            self.text.focus()  # else must click
            if askuser:
                try:
                    text = open(file, 'r', encoding=askuser).read()
                    self.knownEncoding = askuser
                except (UnicodeError, LookupError, IOError):
                    pass

        # try config file (or before ask user?)
        if text is None and self.opensEncoding:
            try:
                text = open(file, 'r', encoding=self.opensEncoding).read()
                self.knownEncoding = self.opensEncoding
            except (UnicodeError, LookupError, IOError):
                pass

        # try platform default (utf-8 on windows; try utf8 always?)
        if text is None:
            try:
                text = open(file, 'r', encoding=sys.getdefaultencoding()).read()
                self.knownEncoding = sys.getdefaultencoding()
            except (UnicodeError, LookupError, IOError):
                pass

        # last resort: use binary bytes and rely on Tk to decode
        if text is None:
            try:
                text = open(file, 'rb').read()  # bytes for Unicode
                text = text.replace(b'\r\n', b'\n')  # for display, saves
                self.knownEncoding = None
            except IOError:
                pass

        if text is None:
            showerror('PyEdit', 'Could not decode and 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):
        """
        retains successful encoding name here for next save, because this
        may be the first Save after New or a manual text insertion;  Save
        and SaveAs may both use last known encoding, per config file (it
        probably should be used for Save, but SaveAs usage is unclear);
        gui prompts are prefilled with the known encoding if there is one;

        does manual text.encode() to avoid creating file; text mode files
        perform platform specific end-line conversion: Windows \r dropped
        if present on open by text mode (auto) and binary mode (manually);
        if manual content inserts, must delete \r else duplicates here;
        knownEncoding=None before first Open or Save, after New, if binary Open;

        encoding behavior is configurable in the local textConfig.py:
        1) if savesUseKnownEncoding > 0, try encoding from last open or save
        2) if savesAskUser True, try user input next (prefill with known?)
        3) if savesEncoding nonempty, try this encoding next: 'utf-8', etc
        4) tries sys.getdefaultencoding() as a last resort
        """

        filename = forcefile or self.my_asksaveasfilename()
        if not filename:
            return

        text = self.getAllText()  # 2.1: a str string, with \n eolns,
        encpick = None  # even if read/inserted as bytes

        # try known encoding at latest Open or Save, if any
        if self.knownEncoding and (  # enc known?
                    (forcefile and self.savesUseKnownEncoding >= 1) or  # on Save?
                    (not forcefile and self.savesUseKnownEncoding >= 2)):  # on SaveAs?
            try:
                text.encode(self.knownEncoding)
                encpick = self.knownEncoding
            except UnicodeError:
                pass

        # try user input, prefill with known type, else next choice
        if not encpick and self.savesAskUser:
            self.update()  # else dialog doesn't appear in rare cases
            askuser = askstring('PyEdit', 'Enter Unicode encoding for save',
                                initialvalue=(self.knownEncoding or
                                              self.savesEncoding or
                                              sys.getdefaultencoding() or ''))
            self.text.focus()  # else must click
            if askuser:
                try:
                    text.encode(askuser)
                    encpick = askuser
                except (UnicodeError, LookupError):  # LookupError:  bad name
                    pass  # UnicodeError: can't encode

        # try config file
        if not encpick and self.savesEncoding:
            try:
                text.encode(self.savesEncoding)
                encpick = self.savesEncoding
            except (UnicodeError, LookupError):
                pass

        # try platform default (utf8 on windows)
        if not encpick:
            try:
                text.encode(sys.getdefaultencoding())
                encpick = sys.getdefaultencoding()
            except (UnicodeError, LookupError):
                pass

        # open in text mode for endlines + encoding
        if not encpick:
            showerror('PyEdit', 'Could not encode for file ' + filename)
        else:
            try:
                file = open(filename, 'w', encoding=encpick)
                file.write(text)
                file.close()
            except:
                showerror('PyEdit', 'Could not write file ' + filename)
            else:
                self.setFileName(filename)  # may be newly created
                self.text.edit_modified(0)  # 2.0: clear modified flag
                self.knownEncoding = encpick  # 2.1: keep enc for next save
                # don't clear undo/redo stks!

    def onNew(self):
        """
        start editing a new file from scratch in current window;
        see onClone to pop-up a new independent edit window instead;
        """
        if self.text_edit_modified():  # 2.0
            if not askyesno('PyEdit', 'Text has changed: discard changes?'):
                return
        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
        self.knownEncoding = None  # 2.1: Unicode type unknown

    def onQuit(self):
        """
        on Quit menu/toolbar select and wm border X button in toplevel windows;
        2.1: don't exit app if others changed;  2.0: don't ask if self unchanged;
        moved to the top-level window classes at the end since may vary per usage:
        a Quit in GUI might quit() to exit, destroy() just one Toplevel, Tk, or
        edit frame, or not be provided at all when run as an attached component;
        check self for changes, and if might quit(), main windows should check
        other windows in the process-wide list to see if they have changed too;
        """
        assert False, 'onQuit must be defined in window-specific sublass'

    def text_edit_modified(self):
        """
        2.1: this now works! seems to have been a bool result type issue in tkinter;
        2.0: self.text.edit_modified() broken in Python 2.4: do manually for now;
        """
        return self.text.edit_modified()
        # 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', True)  # 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):
        """
        non-modal find/change dialog
        2.1: pass per-dialog inputs to callbacks, may be > 1 change dialog open
        """
        new = Toplevel(self)
        new.title('PyEdit - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0, column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1, column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():  # use my entry in enclosing scope
            self.onFind(entry1.get())  # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0, column=2, sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1, column=2, sticky=EW)
        new.columnconfigure(1, weight=1)  # expandable entries

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):  # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)  # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)  # goto next appear
            self.text.update()  # force refresh

    def onGrep(self):
        """
        TBD: better to issue an error if any file fails to decode?
        but utf-16 2-bytes/char format created in Notepad may decode
        without error per utf-8, and search strings won't be found;
        TBD: could allow input of multiple encoding names, split on
        comma, try each one for every file, without open loadEncode?
        """
        from minghu6.gui.formrows import makeFormRow

        # nonmodal dialog: get dirnname, filenamepatt, grepkey
        popup = Toplevel()
        popup.title('PyEdit - grep')
        var1 = makeFormRow(popup, label='Directory root', width=18, browse=False)
        var2 = makeFormRow(popup, label='Filename pattern', width=18, browse=False)
        var3 = makeFormRow(popup, label='Search string', width=18, browse=False)
        var4 = makeFormRow(popup, label='Content encoding', width=18, browse=False)
        var1.set('.')  # current dir
        var2.set('*.py')  # initial values
        var4.set(sys.getdefaultencoding())  # for file content, not filenames
        cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(), var4.get())
        Button(popup, text='Go', command=cb).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey, encoding):
        """
        on Go in grep dialog: populate scrolled list with matches
        tbd: should producer thread be daemon so it dies with app?
        """
        import threading, queue

        # make non-modal un-closeable dialog
        mypopup = Tk()
        mypopup.title('PyEdit - grepping')
        status = Label(mypopup, text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close

        # start producer thread, consumer loop
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue)
        threading.Thread(target=self.grepThreadProducer, args=threadargs).start()
        self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding, myqueue):
        """
        in a non-GUI parallel thread: queue find.find results list;
        could also queue matches as found, but need to keep window;
        file content and file names may both fail to decode here;

        TBD: could pass encoded bytes to find() to avoid filename
        decoding excs in os.walk/listdir, but which encoding to use:
        sys.getfilesystemencoding() if not None?  see also Chapter6
        footnote issue: 3.1 fnmatch always converts bytes per Latin-1;
        """
        from minghu6.etc.find import find
        matches = []
        try:
            for filepath in find(pattern=filenamepatt, startdir=dirname):
                try:
                    textfile = open(filepath, encoding=encoding)
                    for (linenum, linestr) in enumerate(textfile):
                        if grepkey in linestr:
                            msg = '%s@%d  [%s]' % (filepath, linenum + 1, linestr)
                            matches.append(msg)
                except UnicodeError as X:
                    print('Unicode error in:', filepath, X)  # eg: decode, bom
                except IOError as X:
                    print('IO error in:', filepath, X)  # eg: permission
        finally:
            myqueue.put(matches)  # stop consumer loop on find excs: filenames?

    def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup):
        """
        in the main GUI thread: watch queue for results or [];
        there may be multiple active grep threads/loops/queues;
        there may be other types of threads/checkers in process,
        especially when PyEdit is attached component (PyMailGUI);
        """
        import queue
        try:
            matches = myqueue.get(block=False)
        except queue.Empty:
            myargs = (grepkey, encoding, myqueue, mypopup)
            self.after(250, self.grepThreadConsumer, *myargs)
        else:
            mypopup.destroy()  # close status
            self.update()  # erase it now
            if not matches:
                showinfo('PyEdit', 'Grep found no matches for: %r' % grepkey)
            else:
                self.grepMatchesList(matches, grepkey, encoding)

    def grepMatchesList(self, matches, grepkey, encoding):
        """
        populate list after successful matches;
        we already know Unicode encoding from the search: use
        it here when filename clicked, so open doesn't ask user;
        """
        from minghu6.gui.scrolledlist import ScrolledList
        print('Matches for %s: %s' % (grepkey, len(matches)))

        # catch list double-click
        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):
                file, line = selection.split('  [', 1)[0].split('@')
                editor = TextEditorMainPopup(
                    loadFirst=file, winTitle=' grep match', loadEncode=encoding)
                editor.onGoto(int(line))
                editor.text.focus_force()  # no, really

        # new non-modal widnow
        popup = Tk()
        popup.title('PyEdit - grep matches: %r (%s)' % (grepkey, encoding))
        ScrolledFilenames(parent=popup, options=matches)

    ############################################################################
    # 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):
        """
        pop-up dialog giving text statistics and cursor location;
        caveat : Tk insert position column counts a tab as one
        character: translate to next multiple of 8 to match visual?
        """
        text = self.getAllText()  # added on 5/3/00 in 15 mins
        bytes = len(text)  # words uses a simple guess:
        lines = len(text.split('\n'))  # any separated by whitespace
        words = len(text.split())  # 3.x: bytes is really chars
        index = self.text.index(INSERT)  # str is unicode code points
        where = tuple(index.split('.'))
        showinfo('PyEdit Information',
                 'Current location:\n\n' +
                 'line:\t%s\ncolumn:\t%s\n\n' % where +
                 'File text statistics:\n\n' +
                 'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self, makewindow=True):
        """
        open a new edit window without changing one already open (onNew);
        inherits quit and other behavior of the window that it clones;
        """
        if not makewindow:
            new = None  # assume class makes its own window
        else:
            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 PP4E 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 may be PyEdit's window;
        subprocess or multiprocessing modules may work here too;

        sometimes does not appear in rare cases;
        """

        def askcmdargs():
            return askstring('PyEdit', 'Commandline arguments?') or ''

        from launchmods import System, Start, StartArgs, Fork
        filemode = False
        thefile = str(self.getFileName())
        if os.path.exists(thefile):
            filemode = askyesno('PyEdit', 'Run from file?')
            self.update()  # 2.1: run update()
        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() + '\n', namespace)  # exceptions ignored
        elif self.text_edit_modified():  # 2.0: changed test
            showerror('PyEdit', 'Text changed: you must save before run')
        else:
            cmdargs = askcmdargs()
            mycwd = os.getcwd()  # cwd may be root
            dirname, filename = os.path.split(thefile)  # get dir, base
            os.chdir(dirname or mycwd)  # cd for filenames
            thecmd = filename + ' ' + cmdargs  # 2.1: not theFile
            if not parallelmode:  # run as file
                System(thecmd, thecmd)()  # block editor
            else:
                if sys.platform[:3] == 'win':  # spawn in parallel
                    run = StartArgs if cmdargs else Start  # 2.1: support args
                    run(thecmd, thecmd)()  # or always Spawn
                else:
                    Fork(thecmd, thecmd)()  # spawn in parallel
            os.chdir(mycwd)  # go back to my dir

    def onPickFont(self):
        """
        """
        from minghu6.gui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('PyEdit - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var1.set('courier')
        var2.set('14')  # suggested vals
        var3.set('bold italic')  # see pick list for valid inputs
        Button(popup, text='Apply', command=
        lambda: self.onDoFont(var1.get(), var2.get(), var3.get())).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        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 str string

    def setAllText(self, text):
        """
        caller: call self.update() first if just packed, else the
        initial position may be at line 2, not line 1 (2.1; Tk bug?)
        """
        self.text.delete('1.0', END)  # store text string in widget
        self.text.insert(END, text)  # or '1.0'; text=bytes or str
        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):  # see also: onGoto(linenum)
        self.currfile = name  # for save
        self.filelabel.config(text=str(name))

    def setKnownEncoding(self, encoding='utf-8'):  # for saves if inserted
        self.knownEncoding = encoding  # else saves use config, ask?

    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 x 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)
Example #30
0
# e.g. 11-5
import sys, math, os
from tkinter import *
from tkinter.filedialog import SaveAs, Directory

from PIL import Image
from PIL.ImageTk import PhotoImage
from viewer_thumbs import makeThumbs


saveDialog = SaveAs(title='Save As (filename gives image type)')
openDialog = Directory(title='Select Image Directory To Open')

trace = print
appname = 'PyPhoto 1.1'


class ScrolledCanvas(Canvas):
    def __init__(self, container):
        Canvas.__init__(self, container)
        self.config(borderwidth=0)

        vbar = Scrollbar(container)
        hbar = Scrollbar(container, orient=HORIZONTAL)

        vbar.pack(side=RIGHT, fill=Y)
        hbar.pack(side=BOTTOM, fill=X)
        self.pack(side=TOP, fill=BOTH, expand=YES)

        vbar.config(command=self.yview)
        hbar.config(command=self.xview)
class TextEditor:  # mix with menu/toolbar Frame class
    startfiledir = '.'  # for dialogs
    editwindows = []  # for process-wide quit check

    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()
        if loadFirst:
            self.update()
            self.onOpen(loadFirst)

    def start(self):
        self.menuBar = [('File', 0, [('Open...', 0, self.onOpen),
                                     ('Save', 0, self.onSave),
                                     ('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),
                                       ('Grep...', 3, self.onGrep)]),
                        ('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):
        name = Label(self, bg='black', fg='white')
        name.pack(side=TOP, fill=X)

        vbar = Scrollbar(self)
        hbar = Scrollbar(self, orient='horizontal')
        text = Text(self, padx=5, wrap='none')
        text.config(undo=1, autoseparators=1)

        vbar.pack(side=RIGHT, fill=Y)
        hbar.pack(side=BOTTOM, fill=X)
        text.pack(side=TOP, fill=BOTH, expand=YES)

        text.config(yscrollcommand=vbar.set)
        text.config(xscrollcommand=hbar.set)
        vbar.config(command=text.yview)
        hbar.config(command=text.xview)
        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

    def my_askopenfilename(self):
        if not self.openDialog:
            self.openDialog = Open(initialdir=self.startfiledir,
                                   filetypes=self.ftypes)
        return self.openDialog.show()

    def my_asksaveasfilename(self):
        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 askyesno(
            'SimpleEditor', 'Text has changed: discard changes?'))
        if doit:
            file = loadFirst or self.my_askopenfilename()
            if file:
                try:
                    text = open(file, 'r').read()
                except:
                    showerror('SimpleEditor', 'Could not open file ' + file)
                else:
                    self.setAllText(text)
                    self.setFileName(file)
                    self.text.edit_reset()
                    self.text.edit_modified(0)

    def onSave(self):
        self.onSaveAs(self.currfile)

    def onSaveAs(self, forcefile=None):
        file = forcefile or self.my_asksaveasfilename()
        if file:
            text = self.getAllText()
            try:
                open(file, 'w').write(text)
            except:
                showerror('SimpleEditor', 'Could not write file ' + file)
            else:
                self.setFileName(file)  # may be newly created
                self.text.edit_modified(0)

    def onNew(self):
        doit = (not self.text_edit_modified() or askyesno(
            'SimpleEditor', 'Text has changed: discard changes?'))
        if doit:
            self.setFileName(None)
            self.clearAllText()
            self.text.edit_reset()
            self.text.edit_modified(0)

    def onQuit(self):
        assert False, 'onQuit must be defined in window-specific sublass'

    def text_edit_modified(self):
        return self.text.edit_modified()

    def onUndo(self):
        try:
            self.text.edit_undo()
        except TclError:
            showinfo('SimpleEditor', 'Nothing to undo')

    def onRedo(self):
        try:
            self.text.edit_redo()
        except TclError:
            showinfo('SimpleEditor', 'Nothing to redo')

    def onCopy(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', 'No text selected')
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

    def onDelete(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', 'No text selected')
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            showerror('SimpleEditor', 'No text selected')
        else:
            self.onCopy()
            self.onDelete()

    def onPaste(self):
        try:
            text = self.selection_get(selection='CLIPBOARD')
        except TclError:
            showerror('SimpleEditor', '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')
        self.text.mark_set(INSERT, '1.0')
        self.text.see(INSERT)

    def onGoto(self, forceline=None):
        line = forceline or askinteger('SimpleEditor', '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)
                self.text.tag_remove(SEL, '1.0', END)
                self.text.tag_add(SEL, INSERT, 'insert + 1l')
                self.text.see(INSERT)
            else:
                showerror('SimpleEditor', 'Bad line number')

    def onFind(self, lastkey=None):
        key = lastkey or askstring('SimpleEditor', 'Enter search string')
        self.text.update()
        self.text.focus()
        self.lastfind = key
        if key:
            nocase = configs.get('caseinsens', True)
            where = self.text.search(key, INSERT, END, nocase=nocase)
            if not where:
                showerror('SimpleEditor', 'String not found')
            else:
                pastkey = where + '+%dc' % len(key)
                self.text.tag_remove(SEL, '1.0', END)
                self.text.tag_add(SEL, where, pastkey)
                self.text.mark_set(INSERT, pastkey)
                self.text.see(where)

    def onRefind(self):
        self.onFind(self.lastfind)

    def onChange(self):
        new = Toplevel(self)
        new.title('SimpleEditor - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0,
                                                                   column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1,
                                                                   column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():
            self.onFind(entry1.get())

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find', command=onFind).grid(row=0,
                                                      column=2,
                                                      sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1,
                                                        column=2,
                                                        sticky=EW)
        new.columnconfigure(1, weight=1)

    def onDoChange(self, findtext, changeto):
        if self.text.tag_ranges(SEL):
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(INSERT, changeto)
            self.text.see(INSERT)
            self.onFind(findtext)
            self.text.update()

    def onGrep(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel()
        popup.title('SimpleEditor - grep')
        var1 = makeFormRow(popup,
                           label='Directory root',
                           width=18,
                           browse=False)
        var2 = makeFormRow(popup,
                           label='Filename pattern',
                           width=18,
                           browse=False)
        var3 = makeFormRow(popup,
                           label='Search string',
                           width=18,
                           browse=False)
        var1.set('.')
        var2.set('*.py')
        Button(popup,
               text='Go',
               command=lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey):
        from PP4E.Tools.find import find
        from PP4E.Gui.Tour.scrolledlist import ScrolledList

        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):
                file, line = selection.split('  [', 1)[0].split('@')
                editor = TextEditorMainPopup(loadFirst=file,
                                             winTitle=' grep match')
                editor.onGoto(int(line))
                editor.text.focus_force()

        showinfo('SimpleEditor Wait',
                 'Ready to search files (a pause may follow)...')
        matches = []
        for filepath in find(pattern=filenamepatt, startdir=dirname):
            try:
                for (linenum, linestr) in enumerate(open(filepath)):
                    if grepkey in linestr:
                        matches.append('%s@%d  [%s]' %
                                       (filepath, linenum + 1, linestr))
            except:
                print('Failed:', filepath)  # Unicode errors, probably

        if not matches:
            showinfo('SimpleEditor', 'No matches found')
        else:
            popup = Tk()
            popup.title('SimpleEditor - grep matches: %r' % grepkey)
            ScrolledFilenames(parent=popup, options=matches)

    def onFontList(self):
        self.fonts.append(self.fonts[0])
        del self.fonts[0]
        self.text.config(font=self.fonts[0])

    def onColorList(self):
        self.colors.append(self.colors[0])
        del self.colors[0]
        self.text.config(fg=self.colors[0]['fg'], bg=self.colors[0]['bg'])

    def onPickFg(self):
        self.pickColor('fg')

    def onPickBg(self):  # select arbitrary color
        self.pickColor('bg')  # in standard color dialog

    def pickColor(self, part):
        (triple, hexstr) = askcolor()
        if hexstr:
            self.text.config(**{part: hexstr})

    def onInfo(self):
        text = self.getAllText()
        bytes = len(text)
        lines = len(text.split('\n'))  # any separated by whitespace
        words = len(text.split())
        index = self.text.index(INSERT)  # str is unicode code points
        where = tuple(index.split('.'))
        showinfo(
            'SimpleEditor Information',
            'Current location:\n\n' + 'line:\t%s\ncolumn:\t%s\n\n' % where +
            'File text statistics:\n\n' +
            'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self):
        """
        open a new edit window without changing one already open
        inherits quit and other behavior of window that it clones
        """
        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):
        def askcmdargs():
            return askstring('SimpleEditor', 'Commandline arguments?') or ''

        from PP4E.launchmodes import System, Start, StartArgs, Fork
        filemode = False
        thefile = str(self.getFileName())
        if os.path.exists(thefile):
            filemode = askyesno('SimpleEditor', 'Run from file?')
        if not filemode:
            cmdargs = askcmdargs()
            namespace = {'__name__': '__main__'}
            sys.argv = [thefile] + cmdargs.split()
            exec(self.getAllText() + '\n', namespace)
        elif self.text_edit_modified():
            showerror('SimpleEditor', 'Text changed: save before run')
        else:
            cmdargs = askcmdargs()
            mycwd = os.getcwd()  # cwd may be root
            dirname, filename = os.path.split(thefile)  # get dir, base
            os.chdir(dirname or mycwd)
            thecmd = filename + ' ' + cmdargs
            if not parallelmode:  # run as file
                System(thecmd, thecmd)()  # block editor
            else:
                if sys.platform[:3] == 'win':  # spawn in parallel
                    run = StartArgs if cmdargs else Start
                    run(thecmd, thecmd)()  # or always Spawn
                else:
                    Fork(thecmd, thecmd)()  # spawn in parallel
            os.chdir(mycwd)

    def onPickFont(self):
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('SimpleEditor - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size', browse=False)
        var3 = makeFormRow(popup, label='Style', browse=False)
        var1.set('courier')
        var2.set('12')  # suggested vals
        var3.set('bold italic')  # see pick list for valid inputs
        Button(popup,
               text='Apply',
               command=lambda: self.onDoFont(var1.get(), var2.get(), var3.get(
               ))).pack()

    def onDoFont(self, family, size, style):
        try:
            self.text.config(font=(family, int(size), style))
        except:
            showerror('SimpleEditor', 'Bad font specification')

    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 x 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 ', helptext % ((Version, ) * 2))
Example #32
0
        def __init__(self,
                     master,
                     cid_path=None,
                     data_path=None,
                     config=dict(),
                     **keywords):
            """
            Set up a frame with widgets to validate ``id_path`` and ``data_path``.

            :param master: Tk master or root in which the frame should show up
            :param cid_path: optional preset for :guilabel:`CID` widget
            :type cid_path: str or None
            :param data_path: optional preset for :guilabel:`Data` widget
            :type data_path: str or None
            :param config: Tk configuration
            :param keywords: Tk keywords
            """
            assert has_tk
            assert master is not None

            if six.PY2:
                # In Python 2, Frame is an old style class.
                Frame.__init__(self, master, config, **keywords)
            else:
                super().__init__(master, config, **keywords)

            self._master = master

            # Define basic layout.
            self.grid(padx=_PADDING, pady=_PADDING)
            # self.grid_columnconfigure(1, weight=1)
            self.grid_rowconfigure(_VALIDATION_REPORT_ROW, weight=1)

            # Choose CID.
            self._cid_label = Label(self, text='CID:')
            self._cid_label.grid(row=_CID_ROW, column=0, sticky=E)
            self._cid_path_entry = Entry(self, width=55)
            self._cid_path_entry.grid(row=_CID_ROW, column=1, sticky=E + W)
            self._choose_cid_button = Button(self,
                                             command=self.choose_cid,
                                             text='Choose...')
            self._choose_cid_button.grid(row=_CID_ROW, column=2)
            self.cid_path = cid_path

            # Choose data.
            self._data_label = Label(self, text='Data:')
            self._data_label.grid(row=_DATA_ROW, column=0, sticky=E)
            self._data_path_entry = Entry(self, width=55)
            self._data_path_entry.grid(row=_DATA_ROW, column=1, sticky=E + W)
            self._choose_data_button = Button(self,
                                              command=self.choose_data,
                                              text='Choose...')
            self._choose_data_button.grid(row=_DATA_ROW, column=2)
            self.data_path = data_path

            # Validate.
            self._validate_button = Button(self,
                                           command=self.validate,
                                           text='Validate')
            self._validate_button.grid(row=_VALIDATE_BUTTON_ROW,
                                       column=0,
                                       padx=_PADDING,
                                       pady=_PADDING)

            # Validation status text.
            self._validation_status_text = StringVar()
            validation_status_label = Label(
                self, textvariable=self._validation_status_text)
            validation_status_label.grid(row=_VALIDATE_BUTTON_ROW, column=1)

            # Validation result.
            validation_report_frame = LabelFrame(self,
                                                 text='Validation report')
            validation_report_frame.grid(row=_VALIDATION_REPORT_ROW,
                                         columnspan=3,
                                         sticky=E + N + S + W)
            validation_report_frame.grid_columnconfigure(0, weight=1)
            validation_report_frame.grid_rowconfigure(0, weight=1)
            self._validation_report_text = Text(validation_report_frame)
            self._validation_report_text.grid(column=0,
                                              row=0,
                                              sticky=E + N + S)
            _validation_report_scrollbar = Scrollbar(validation_report_frame)
            _validation_report_scrollbar.grid(column=1,
                                              row=0,
                                              sticky=N + S + W)
            _validation_report_scrollbar.config(
                command=self._validation_report_text.yview)
            self._validation_report_text.config(
                yscrollcommand=_validation_report_scrollbar.set)

            # Set up file dialogs.
            self._choose_cid_dialog = Open(
                initialfile=self.cid_path,
                title='Choose CID',
            )
            self._choose_data_dialog = Open(
                initialfile=self.data_path,
                title='Choose data',
            )
            self._save_log_as_dialog = SaveAs(
                defaultextension='.log',
                initialfile='cutplace.log',
                title='Save validation result',
            )

            menubar = Menu(master)
            master.config(menu=menubar)
            self._file_menu = Menu(menubar, tearoff=False)
            self._file_menu.add_command(command=self.choose_cid,
                                        label='Choose CID...')
            self._file_menu.add_command(command=self.choose_data,
                                        label='Choose data...')
            self._file_menu.add_command(command=self.save_validation_report_as,
                                        label='Save validation report as...')
            self._file_menu.add_command(command=self.quit, label='Quit')
            menubar.add_cascade(label='File', menu=self._file_menu)
            help_menu = Menu(menubar, tearoff=False)
            help_menu.add_command(command=self.show_about, label='About')
            menubar.add_cascade(label='Help', menu=help_menu)

            self._enable_usable_widgets()
class TextEditor:                          # mix with menu/toolbar Frame class
    startfiledir = '.'   # for dialogs
    editwindows = []     # for process-wide quit check

    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.update()                           # 2.1: else @ line 2; see book
            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),
                  ('Grep...',    3, self.onGrep)]
            ),
            ('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
                                                    # GuiMaker frame packs itself
        vbar  = Scrollbar(self)
        hbar  = Scrollbar(self, orient='horizontal')
        text  = Text(self, padx=5, wrap='none')        # disable line wrapping
        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):
        """
        on Quit menu/toolbar select and wm border X button in toplevel windows;
        2.1: don't exit app if others changed;  2.0: don't ask if self unchanged;
        moved to the top-level window classes at the end since may vary per usage:
        a Quit in GUI might quit() to exit, destroy() just one Toplevel, Tk, or 
        edit frame, or not be provided at all when run as an attached component;
        check self for changes, and if might quit(), main windows should check
        other windows in the process-wide list to see if they have changed too; 
        """
        assert False, 'onQuit must be defined in window-specific sublass' 

    def text_edit_modified(self):
        """
        2.1: this now works! seems to have been a bool result type issue in tkinter;
        2.0: self.text.edit_modified() broken in Python 2.4: do manually for now; 
        """
        return self.text.edit_modified()
       #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', True)               # 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):
        """
        non-modal find/change dialog 
        2.1: pass per-dialog inputs to callbacks, may be > 1 change dialog open
        """
        new = Toplevel(self)
        new.title('PyEdit - change')
        Label(new, text='Find text?', relief=RIDGE, width=15).grid(row=0, column=0)
        Label(new, text='Change to?', relief=RIDGE, width=15).grid(row=1, column=0)
        entry1 = Entry(new)
        entry2 = Entry(new)
        entry1.grid(row=0, column=1, sticky=EW)
        entry2.grid(row=1, column=1, sticky=EW)

        def onFind():                         # use my entry in enclosing scope   
            self.onFind(entry1.get())         # runs normal find dialog callback

        def onApply():
            self.onDoChange(entry1.get(), entry2.get())

        Button(new, text='Find',  command=onFind ).grid(row=0, column=2, sticky=EW)
        Button(new, text='Apply', command=onApply).grid(row=1, column=2, sticky=EW)
        new.columnconfigure(1, weight=1)      # expandable entries

    def onDoChange(self, findtext, changeto):
        # on Apply in change dialog: change and refind
        if self.text.tag_ranges(SEL):                      # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)          
            self.text.insert(INSERT, changeto)             # deletes if empty
            self.text.see(INSERT)
            self.onFind(findtext)                          # goto next appear
            self.text.update()                             # force refresh

    def onGrep(self):
        """
        new in version 2.1: threaded external file search;
        search matched filenames in directory tree for string;
        listbox clicks open matched file at line of occurrence;
        search is threaded so the GUI remains active and is not
        blocked, and to allow multiple greps to overlap in time;
        could use threadtools, but avoid loop in no active grep;
        """
        from PP4E.Gui.ShellGui.formrows import makeFormRow

        # nonmodal dialog: get dirnname, filenamepatt, grepkey
        popup = Toplevel()
        popup.title('PyEdit - grep')
        var1 = makeFormRow(popup, label='Directory root',   width=18, browse=False)
        var2 = makeFormRow(popup, label='Filename pattern', width=18, browse=False)
        var3 = makeFormRow(popup, label='Search string',    width=18, browse=False)
        var1.set('.')      # current dir
        var2.set('*.py')   # initial values
        Button(popup, text='Go',
           command=lambda: self.onDoGrep(var1.get(), var2.get(), var3.get())).pack()

    def onDoGrep(self, dirname, filenamepatt, grepkey):
        # on Go in grep dialog: populate scrolled list with matches
        # tbd: should producer thread be daemon so dies with app?
        import threading, queue

        # make non-modal un-closeable dialog
        mypopup = Tk()
        mypopup.title('PyEdit - grepping')
        status = Label(mypopup, text='Grep thread searching for: %r...' % grepkey)
        status.pack(padx=20, pady=20)
        mypopup.protocol('WM_DELETE_WINDOW', lambda: None)  # ignore X close

        # start producer thread, consumer loop
        myqueue = queue.Queue()
        threadargs = (filenamepatt, dirname, grepkey, myqueue)
        threading.Thread(target=self.grepThreadProducer, args=threadargs).start()
        self.grepThreadConsumer(grepkey, myqueue, mypopup)

    def grepThreadProducer(self, filenamepatt, dirname, grepkey, myqueue):
        """
        in a non-GUI parallel thread: queue find.find results list;
        could also queue matches as found, but need to keep window;
        """
        from PP4E.Tools.find import find
        matches = []
        for filepath in find(pattern=filenamepatt, startdir=dirname):
            try:
                for (linenum, linestr) in enumerate(open(filepath)):
                    if grepkey in linestr:
                        message = '%s@%d  [%s]' % (filepath, linenum + 1, linestr)
                        matches.append(message)
            except UnicodeDecodeError:
                print('Unicode error in:', filepath)
        myqueue.put(matches)

    def grepThreadConsumer(self, grepkey, myqueue, mypopup):
        """
        in the main GUI thread: watch queue for results or [];
        there may be multiple active grep threads/loops/queues;
        there may be other types of threads/checkers in process,
        especially when PyEdit is attached component (PyMailGUI);
        """
        import queue
        try:
            matches = myqueue.get(block=False)
        except queue.Empty:
            self.after(250, self.grepThreadConsumer, grepkey, myqueue, mypopup)
        else:
            mypopup.destroy()     # close status
            self.update()         # erase it now
            if not matches:
                showinfo('PyEdit', 'Grep found no matches for: %r' % grepkey)
            else:
                self.grepMatchesList(matches, grepkey)

    def grepMatchesList(self, matches, grepkey):
        # populate list after successful matches
        from PP4E.Gui.Tour.scrolledlist import ScrolledList
        print('Matches for %s: %s' % (grepkey, len(matches)))

        # catch list double-click
        class ScrolledFilenames(ScrolledList):
            def runCommand(self, selection):  
                file, line = selection.split('  [', 1)[0].split('@')
                editor = TextEditorMainPopup(loadFirst=file, winTitle=' grep match')
                editor.onGoto(int(line))
                editor.text.focus_force()   # no, really

        # new non-modal widnow
        popup = Tk()
        popup.title('PyEdit - grep matches: %r' % grepkey)
        ScrolledFilenames(parent=popup, options=matches)


    ############################################################################
    # 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('\n'))              # any separated by whitespace
        words = len(text.split())                  # 3.x: bytes is really chars
        index = self.text.index(INSERT)            # str is unicode code points
        where = tuple(index.split('.'))
        showinfo('PyEdit Information',
                 'Current location:\n\n' +
                 'line:\t%s\ncolumn:\t%s\n\n' % where +
                 'File text statistics:\n\n' +
                 'chars:\t%d\nlines:\t%d\nwords:\t%d\n' % (bytes, lines, words))

    def onClone(self):                  
        """
        open a new edit window without changing one already open
        inherits quit and other behavior of window that it clones
        """
        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 PP4E 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 may be PyEdit's window;
        subprocess or multiprocessing modules may work here too;
        2.1: fixed to use base file name after chdir, not path;
        2.1: use StartArs to allow args in file mode on Windows;
        """
        def askcmdargs():
            return askstring('PyEdit', 'Commandline arguments?') or ''

        from PP4E.launchmodes import System, Start, StartArgs, 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() + '\n', 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
            dirname, filename = os.path.split(thefile)      # get dir, base
            os.chdir(dirname or mycwd)                      # cd for filenames
            thecmd  = filename + ' ' + cmdargs              # 2.1: not theFile
            if not parallelmode:                            # run as file
                System(thecmd, thecmd)()                    # block editor
            else:
                if sys.platform[:3] == 'win':               # spawn in parallel
                    run = StartArgs if cmdargs else Start   # 2.1: support args
                    run(thecmd, thecmd)()                   # or always Spawn
                else:
                    Fork(thecmd, thecmd)()                  # spawn in parallel
            os.chdir(mycwd)                                 # go back to my dir

    def onPickFont(self):
        """
        2.0 non-modal font spec dialog
        2.1: pass per-dialog inputs to callback, may be > 1 font dialog open
        """
        from PP4E.Gui.ShellGui.formrows import makeFormRow
        popup = Toplevel(self)
        popup.title('PyEdit - font')
        var1 = makeFormRow(popup, label='Family', browse=False)
        var2 = makeFormRow(popup, label='Size',   browse=False)
        var3 = makeFormRow(popup, label='Style',  browse=False)
        var1.set('courier')
        var2.set('12')              # suggested vals
        var3.set('bold italic')     # see pick list for valid inputs
        Button(popup, text='Apply', command=
               lambda: self.onDoFont(var1.get(), var2.get(), var3.get())).pack()

    def onDoFont(self, family, size, style):
        try:  
            self.text.config(font=(family, int(size), style))
        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 x 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))
 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()
Example #35
0
class CutplaceFrame(Frame):
    def __init__(self,
                 master,
                 cid_path=None,
                 data_path=None,
                 config=dict(),
                 **keywords):
        assert has_tk
        if six.PY2:
            Frame.__init__(self, master, config, **keywords)
        else:
            super().__init__(master, config, **keywords)

        # Define basic layout.
        self.grid(padx=_PADDING, pady=_PADDING)
        # self.grid_columnconfigure(1, weight=1)
        self.grid_rowconfigure(_VALIDATION_RESULT_ROW, weight=1)

        # Choose CID.
        self._cid_label = Label(self, text='CID:')
        self._cid_label.grid(row=_CID_ROW, column=0, sticky=E)
        self._cid_path_entry = Entry(self, width=55)
        self._cid_path_entry.grid(row=_CID_ROW, column=1, sticky=E + W)
        self._choose_cid_button = Button(self,
                                         command=self.choose_cid,
                                         text='Choose...')
        self._choose_cid_button.grid(row=_CID_ROW, column=2)
        self.cid_path = cid_path

        # Choose data.
        self._data_label = Label(self, text='Data:')
        self._data_label.grid(row=_DATA_ROW, column=0, sticky=E)
        self._data_path_entry = Entry(self, width=55)
        self._data_path_entry.grid(row=_DATA_ROW, column=1, sticky=E + W)
        self._choose_data_button = Button(self,
                                          command=self.choose_data,
                                          text='Choose...')
        self._choose_data_button.grid(row=_DATA_ROW, column=2)
        self.data_path = data_path

        # Validate.
        self._validate_button = Button(self,
                                       command=self.validate,
                                       text='Validate')
        self._validate_button.grid(row=_VALIDATE_BUTTON_ROW,
                                   column=0,
                                   padx=_PADDING,
                                   pady=_PADDING)

        # Validation status text.
        self._validation_status_text = StringVar()
        validation_status_label = Label(
            self, textvariable=self._validation_status_text)
        validation_status_label.grid(row=_VALIDATE_BUTTON_ROW, column=1)

        # Validation result.
        validation_result_frame = LabelFrame(self, text='Validation result')
        validation_result_frame.grid(row=_VALIDATION_RESULT_ROW,
                                     columnspan=3,
                                     sticky=E + N + S + W)
        validation_result_frame.grid_columnconfigure(0, weight=1)
        validation_result_frame.grid_rowconfigure(0, weight=1)
        self._validation_result_text = Text(validation_result_frame)
        self._validation_result_text.grid(column=0, row=0, sticky=E + N + S)
        _validation_result_scrollbar = Scrollbar(validation_result_frame)
        _validation_result_scrollbar.grid(column=1, row=0, sticky=N + S + W)
        _validation_result_scrollbar.config(
            command=self._validation_result_text.yview)
        self._validation_result_text.config(
            yscrollcommand=_validation_result_scrollbar.set)

        # "Save validation result as" button.
        self._save_log_button = Button(self,
                                       command=self.save_log_as,
                                       text='Save validation result as...')
        self._save_log_button.grid(row=_SAVE_ROW,
                                   column=1,
                                   columnspan=2,
                                   sticky=E + S)

        # Set up file dialogs.
        self._choose_cid_dialog = Open(
            initialfile=self.cid_path,
            title='Choose CID',
        )
        self._choose_data_dialog = Open(
            initialfile=self.data_path,
            title='Choose data',
        )
        self._save_log_as_dialog = SaveAs(
            defaultextension='.log',
            initialfile='cutplace.log',
            title='Save validation result',
        )

        self.enable_usable_widgets()

    def enable_usable_widgets(self):
        def set_state(widget_to_set_state_for, possibly_empty_text):
            if (possibly_empty_text
                    is not None) and (possibly_empty_text.rstrip() != ''):
                state = 'normal'
            else:
                state = 'disabled'
            widget_to_set_state_for.config(state=state)

        set_state(self._validate_button, self.cid_path)
        set_state(self._validation_result_text, self.validation_result)
        set_state(self._save_log_button, self.validation_result)

    def choose_cid(self):
        cid_path = self._choose_cid_dialog.show()
        if cid_path != '':
            self.cid_path = cid_path
            self.enable_usable_widgets()

    def choose_data(self):
        data_path = self._choose_data_dialog.show()
        if data_path != '':
            self.data_path = data_path
            self.enable_usable_widgets()

    def save_log_as(self):
        validation_result_path = self._save_log_as_dialog.show()
        if validation_result_path != '':
            try:
                with io.open(validation_result_path, 'w',
                             encoding='utf-8') as validation_result_file:
                    validation_result_file.write(
                        self._validation_result_text.get(1.0, END))
            except Exception as error:
                showerror('Cutplace error',
                          'Cannot save validation results:\n%s' % error)

    def clear_validation_result_text(self):
        self._validation_result_text.configure(state='normal')
        self._validation_result_text.delete(1.0, END)
        self._validation_result_text.see(END)
        self._validation_result_text.configure(state='disabled')

    def _cid_path(self):
        return self._cid_path_entry.get()

    def _set_cid_path(self, value):
        self._cid_path_entry.delete(0, END)
        if value is not None:
            self._cid_path_entry.insert(0, value)

    cid_path = property(_cid_path, _set_cid_path, None,
                        'Path of the CID to use for validation')

    def _data_path(self):
        return self._data_path_entry.get()

    def _set_data_path(self, value):
        self._data_path_entry.delete(0, END)
        if value is not None:
            self._data_path_entry.insert(0, value)

    data_path = property(_data_path, _set_data_path, None,
                         'Path of the data to validate')

    @property
    def validation_result(self):
        return self._validation_result_text.get(0.0, END)

    def validate(self):
        def add_log_line(line):
            self._validation_result_text.config(state=NORMAL)
            try:
                self._validation_result_text.insert(END, line + '\n')
                self._validation_result_text.see(END)
            finally:
                self._validation_result_text.config(state=DISABLED)

        def add_log_error_line(line):
            add_log_line('ERROR: %s' % line)

        def show_status_line(line):
            self._validation_status_text.set(line)
            self.master.update()

        assert self.cid_path != ''

        cid_name = os.path.basename(self.cid_path)
        self.clear_validation_result_text()
        add_log_line('%s: validating' % cid_name)
        self.enable_usable_widgets()
        cid = None
        try:
            cid = interface.Cid(self.cid_path)
            add_log_line('%s: ok' % cid_name)
        except errors.InterfaceError as error:
            add_log_error_line(error)
        except Exception as error:
            add_log_error_line('cannot read CID: %s' % error)

        if (cid is not None) and (self.data_path != ''):
            try:
                data_name = os.path.basename(self.data_path)
                add_log_line('%s: validating' % data_name)
                validator = validio.Reader(cid,
                                           self.data_path,
                                           on_error='yield')
                show_status_line('Validation started')
                last_update_time = time.time()
                for row_or_error in validator.rows():
                    now = time.time()
                    if (now - last_update_time) > 3:
                        last_update_time = now
                        show_status_line('%d rows validated' %
                                         (validator.accepted_rows_count +
                                          validator.rejected_rows_count))
                    if isinstance(row_or_error, errors.CutplaceError):
                        add_log_error_line(row_or_error)
                show_status_line('%d rows validated - finished' %
                                 (validator.accepted_rows_count +
                                  validator.rejected_rows_count))
                add_log_line('%s: %d rows accepted, %d rows rejected' %
                             (data_name, validator.accepted_rows_count,
                              validator.rejected_rows_count))
            except Exception as error:
                add_log_error_line('cannot validate data: %s' % error)