PyRoomEditor

A complete (well almost) editor in a single file. Based on Tkinter and inspired by Fredrik Lundh's Vroom!

Features:

Drawbacks:

   1 __version__ = 0.1
   2 __date__    = "2009-07-27"
   3 __author__  = "robert@pytrash.co.uk"
   4 __licence__ = "Public Domain"
   5 
   6 
   7 __changelog__ = (
   8 
   9 ('2009-07-27', '0.1', 'PyTrash',
  10 
  11 """Initial version."""),
  12  
  13 )
  14 
  15 
  16 from Tkinter import *
  17 
  18 import os
  19 
  20 TITLE = 'pyRoomEditor'
  21 
  22 TICK_PERIOD = 100 #ms
  23 
  24 FONTSIZE = 12
  25 FONT = "{Lucida Sans Typewriter} %d" % FONTSIZE
  26 
  27 BACKGROUND = 'black'
  28 FOREGROUND = 'lightgreen'
  29 
  30 SELECT_BACKGROUND = "#008000"
  31 SELECT_FOREGROUND = 'green'
  32 
  33 LINE_NUMBER_BACKGROUND = 'black'
  34 LINE_NUMBER_FOREGROUND = 'green'
  35 LINE_NUMBER_WIDTH = 2
  36 
  37 INSERT_BACKGROUND = 'white'
  38 
  39 FILETYPES = [
  40     ("Text files", "*.txt"), ("All files", "*")
  41 ]
  42 
  43 DEFAULT_TEXT_BINDINGS = (
  44     ("<Control-Alt-n>", "fileNew"),
  45     ("<Control-n>", "fileNewWindow"),
  46     ("<Control-o>", "fileOpen"),
  47     ("<Control-s>", "fileSave"),
  48     ("<Control-Shift-S>", "fileSaveAs"),
  49     ("<Control-Alt-s>" , "fileSaveCopyAs"),
  50     ("<Control-q>", "fileQuit"),
  51     ("<Control-v>", "editPaste"),
  52     ("<Control-l>", "optionsToggleLineNumbers"),
  53 )
  54 
  55 class Cancel(Exception):
  56     pass
  57 
  58 import tkFileDialog
  59 import tkMessageBox
  60 
  61 class pyRoomEditor(object):
  62 
  63     _editorList = []
  64     _tickerId = None
  65 
  66     def __init__(self, master):
  67 
  68         # A frame to hold the components of the widget.
  69         self.frame = Frame(master, bd=0, relief=FLAT, background=BACKGROUND)
  70 
  71 
  72         # The Text widget holding the line numbers.
  73         self.lnText = Text(self.frame,
  74             borderwidth=0,
  75             font=FONT,
  76             foreground = LINE_NUMBER_FOREGROUND,
  77             background = LINE_NUMBER_BACKGROUND,
  78             insertbackground='black',
  79             selectforeground=LINE_NUMBER_FOREGROUND,
  80             selectbackground=LINE_NUMBER_BACKGROUND,
  81             wrap=None,
  82             undo=False,              
  83             width = LINE_NUMBER_WIDTH,
  84             padx = 4,
  85             highlightthickness = 0,
  86             takefocus = 0,
  87             state='disabled'
  88         )
  89 
  90         # The Main Text Widget
  91         self.text = Text(self.frame,
  92             borderwidth=0,
  93             font=FONT,
  94             foreground=FOREGROUND,
  95             background=BACKGROUND,
  96             insertbackground=INSERT_BACKGROUND,
  97             selectforeground=SELECT_FOREGROUND,
  98             selectbackground=SELECT_BACKGROUND,
  99             wrap=WORD,
 100             undo=True,
 101             width=64,
 102         )
 103         self.text.pack(side=LEFT, fill=BOTH, expand=1)
 104 
 105         self._title = None
 106         self.filename = None
 107         self.lineNumbers = ''
 108         self.showLineNumbers = False
 109         self.editorList.append(self)
 110         self.lineNumberWidth = 2
 111 
 112         self.startTicker()
 113 
 114 
 115     def getTop(self):
 116         return self.text.winfo_toplevel()
 117     top = property(getTop)
 118 
 119         
 120     ## TICKER INTERFACE
 121 
 122     @classmethod
 123     def startTicker(cls):
 124         if cls._tickerId is None:
 125             cls.nextTick()
 126             
 127     @classmethod
 128     def nextTick(cls):
 129         cls._tickerId = root.after(TICK_PERIOD, cls.tickNotify)
 130 
 131     @classmethod
 132     def tickNotify(cls):
 133 
 134         for ed in cls._editorList:
 135             ed.doTick()
 136             
 137         cls.nextTick()
 138 
 139     def doTick(self):
 140         
 141         if self.edit_modified():
 142             if not self.modIndicator:
 143                 self.setTitle()
 144         else:
 145             if self.modIndicator:
 146                 self.setTitle()
 147 
 148         if self.showLineNumbers:
 149             self.updateLineNumbers()
 150 
 151     ## LINE NUMBER METHODS
 152 
 153     def getLineNumbers(self):
 154         
 155         x = 0
 156         line = '0'
 157         col= ''
 158         ln = []
 159         
 160         # assume each line is at least 6 pixels high
 161         step = 6
 162         
 163         nl = ''
 164         lineMask = '%s\n'
 165         indexMask = '@0,%d'
 166         
 167         
 168         for i in range(0, self.text.winfo_height(), step):
 169             
 170             ll, cc = self.text.index( indexMask % i).split('.')
 171 
 172             if line == ll:
 173                 if col != cc:
 174                     col = cc
 175                     ln.append(nl)
 176             else:
 177                 line, col = ll, cc
 178                 ln.append(line)
 179 
 180             maxWidth = max(len(ll) for ll in ln)
 181             if self.lineNumberWidth < maxWidth:
 182                 self.lineNumberWidth = maxWidth
 183                 self.lnText.configure(width=maxWidth)
 184 
 185         maxWidth = self.lineNumberWidth
 186             
 187         lineMask = (' ' * maxWidth) + '%s'
 188         ln = '\n'.join( [(lineMask%ll)[-maxWidth:] for ll in ln])
 189 
 190         return ln
 191 
 192     def updateLineNumbers(self):
 193 
 194         tt = self.lnText
 195         ln = self.getLineNumbers()
 196         if self.lineNumbers != ln:
 197             self.lineNumbers = ln
 198             tt.config(state='normal')
 199             tt.delete('1.0', END)
 200             tt.insert('1.0', self.lineNumbers)
 201             tt.config(state='disabled')
 202         
 203 
 204     def toggleLineNumbers(self):
 205         
 206         if not self.showLineNumbers:
 207             self.lnText.pack(side=LEFT, before=self.text, fill=Y)
 208             self.showLineNumbers = True
 209         else:
 210             self.lnText.pack_forget()
 211             self.showLineNumbers = False
 212         
 213     
 214     def getEditorList(self):
 215         return self.__class__._editorList
 216 
 217     editorList = property(getEditorList)
 218 
 219 
 220     def getFilename(self):
 221         return self._filename
 222 
 223     def setFilename(self, filename):
 224         self._filename = filename
 225         self.title = os.path.basename(filename or "(new document)")
 226         
 227     filename = property(getFilename, setFilename)
 228 
 229 
 230     def getTitle(self):
 231         return self._title
 232 
 233     def setTitle(self, title=None):
 234 
 235         title = title or self._title
 236 
 237         self._title = title
 238 
 239         self.modIndicator = mod = self.edit_modified()
 240         mod = mod and '*' or ''
 241 
 242         self.top.title(mod + title + " - " + TITLE)
 243         
 244     title = property(getTitle, setTitle)
 245 
 246 
 247     def edit_modified(self, value=None):
 248         # Python 2.5's implementation is broken
 249         return self.text.tk.call(self.text, "edit", "modified", value)
 250 
 251     modified = property(edit_modified, edit_modified)
 252 
 253     def clear(self, filename=None, text=''):
 254 
 255         tt = self.text
 256         tt.delete(1.0, END)
 257 
 258         tt.insert(END, text)
 259         tt.edit_reset()
 260         tt.mark_set(INSERT, 1.0)
 261 
 262         self.edit_modified(False)
 263         self.filename = filename
 264         
 265         
 266     def loadFile(self, filename=None):
 267 
 268         text = ''
 269 
 270         if filename is None:
 271             filename = self.filename
 272 
 273         if filename is not None:
 274 
 275             try:
 276                 text = open(filename, 'rb').read()
 277                 try:
 278                     text= text.decode('utf-8')
 279                 except Exception:
 280                     text = text.decode('latin-1')
 281             except Exception:
 282                 print 'failed to open file', filename
 283                 return
 284 
 285         self.clear(filename, text)
 286         
 287         
 288     def saveFile(self, filename=None):
 289 
 290         filename = filename or self.filename
 291 
 292         try:
 293             fp = open(filename, "w")
 294             s = self.text.get(1.0, END)
 295             fp.write(s.rstrip().encode('utf-8'))
 296             fp.write("\n")
 297         finally:
 298             try:
 299                 fp.close()
 300             except Exception():
 301                 pass
 302             
 303         self.edit_modified(False)
 304 
 305         
 306     def openAs(self):
 307 
 308         f = tkFileDialog.askopenfilename(
 309             parent=root, filetypes=FILETYPES)
 310 
 311         if not f:
 312             raise Cancel
 313         try:
 314             self.loadFile(f)
 315         except IOError:
 316             tkMessageBox.showwarning("Open", "Cannot open the file.")
 317             raise Cancel
 318 
 319 
 320     def saveAs(self, copy=False):
 321 
 322         title = 'Save ' + (copy and 'Copy ' or '') + 'As'
 323        
 324         f = tkFileDialog.asksaveasfilename(
 325             parent=root, title = title, defaultextension=".txt")
 326 
 327         if not f: raise Cancel
 328 
 329         try:
 330             self.saveFile(f)    
 331         except IOError:
 332 
 333             tkMessageBox.showwarning(title , "Cannot save the file.")
 334             raise Cancel
 335 
 336         if not copy:
 337             self.filename = f
 338 
 339 
 340     def save(self):
 341         
 342         if self.filename:
 343             try:
 344                 self.saveFile()
 345             except IOError:
 346                 tkMessageBox.showwarning("Save", "Cannot save the file.")
 347                 raise Cancel
 348         else:
 349             self.saveAs()
 350 
 351 
 352     def saveIfModified(self):
 353         if self.edit_modified() and askyesnocancel(
 354                 TITLE, "Document modified. Save changes?"
 355         ):
 356             self.save()
 357 
 358 
 359     def setDefaultTextBindings(self):
 360 
 361         for b, t in DEFAULT_TEXT_BINDINGS:
 362             self.text.bind(b, getattr(self, t))
 363 
 364 
 365     @classmethod
 366     def newWindow(cls, filename=None):
 367         
 368         top = Toplevel(root)
 369         top.config(background="black")
 370 
 371         #top.wm_state("normal")
 372 
 373         editor = cls(top)
 374         editor.setDefaultTextBindings()
 375         
 376         top.protocol("WM_DELETE_WINDOW", editor.fileQuit)
 377         top.wm_iconify()
 378         top.wm_deiconify()
 379 
 380         editor.loadFile(filename)
 381         
 382         editor.frame.pack(fill=BOTH, expand=1, pady=0)
 383         editor.text.focus_set()
 384             
 385 
 386 
 387     ## COMMANDS
 388 
 389     # FILE MENU
 390     
 391     def fileNew(self, event=None):
 392         
 393         try:
 394             self.saveIfModified()
 395             self.filename = None
 396             self.clear()
 397         except Cancel:
 398             pass
 399 
 400         return "break"
 401 
 402     def fileNewWindow(self, event=None):
 403 
 404         try:
 405             self.newWindow()
 406         except Cancel:
 407             pass
 408 
 409         return "break"
 410     
 411     def fileOpen(self, event=None):
 412 
 413         try:
 414             self.saveIfModified()
 415             self.openAs()
 416         except Cancel:
 417             pass
 418 
 419         return "break"
 420 
 421     def fileSave(self, event=None):
 422         
 423         try:
 424             self.save()
 425         except Cancel:
 426             pass
 427 
 428         return "break"
 429 
 430     def fileSaveAs(self, event=None, copy=False):
 431         
 432         try:
 433             self.saveAs(copy)
 434         except Cancel:
 435             pass
 436 
 437         return "break"
 438 
 439     def fileSaveCopyAs(self, event=None):
 440 
 441         return self.fileSaveAs(event, copy=True)
 442 
 443     def fileQuit(self, event=None):
 444 
 445         try:
 446             self.saveIfModified()
 447         except Cancel:
 448             return "break"
 449 
 450         lst = self.editorList
 451         lst.remove(self)
 452         self.text.winfo_toplevel().destroy()
 453         if len(lst) == 0:
 454             root.destroy()
 455             
 456 
 457     # EDIT MENU
 458 
 459     def editPaste(self, event=None):
 460 
 461         tt = self.text
 462         try:
 463             s = tt.selection_get(selection="CLIPBOARD")
 464         except Exception:
 465             s = None
 466 
 467         if s is not None:
 468             sel = tt.tag_ranges("sel")
 469             if len(sel) == 2:
 470                 start,end = sel
 471                 if tt.compare(start,"!=",end):
 472                     tt.delete(start,end)
 473                     tt.insert(start,s)
 474             else:
 475                 tt.insert(INSERT, s)
 476         
 477         return "break"
 478 
 479 
 480     # OPTION MENU
 481 
 482     def optionsToggleLineNumbers(self, event=None):
 483         try:
 484             self.toggleLineNumbers()
 485         except Exception:
 486             pass
 487 
 488         return "break"
 489 
 490 def askyesnocancel(
 491         title=None,
 492         message=None,
 493         icon=tkMessageBox.QUESTION,
 494         type=tkMessageBox.YESNOCANCEL,
 495         **options
 496 ):
 497     
 498     s = tkMessageBox.Message(
 499         title=title, message=message, icon=icon, type=type, **options
 500     ).show()
 501     
 502     if isinstance(s, bool):
 503         return s
 504 
 505     if str(s) == "cancel":
 506         raise Cancel
 507     
 508     return str(s) == "yes"
 509 
 510 
 511 if __name__ == '__main__':
 512 
 513     root = Tk()
 514     root.withdraw()
 515 
 516     try:
 517         pyRoomEditor.newWindow(sys.argv[1])
 518     except (IndexError, IOError):
 519         pyRoomEditor.newWindow()
 520         
 521     mainloop()

tkinter: PyRoomEditor (last edited 2010-07-26 11:59:12 by localhost)