PyRoomEditor
A complete (well almost) editor in a single file. Based on Tkinter and inspired by Fredrik Lundh's Vroom!
Features:
- - No menus, no scroll bar, no toolbar! - Optional line numbers. - Multiple windows allowed. - Unicode editing.
Drawbacks:
- - No Find/Replace (yet). - Need a more sophisticated save/backup facillity.
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()
522