New release.
[rox-edit.git] / EditWindow.py
blobf3b66998ed736e6c217d4c35503d19ed756d2583
1 import rox
2 from rox import g, filer, app_options
3 import sys
4 from rox.loading import XDSLoader
5 from rox.options import Option
6 from buffer import Buffer
7 from rox.saving import Saveable
9 FALSE = g.FALSE
10 TRUE = g.TRUE
12 from rox.Menu import Menu, set_save_name
14 default_font = Option('default_font', 'serif')
16 background_colour = Option('background', '#fff')
17 foreground_colour = Option('foreground', '#000')
19 set_save_name('Edit')
21 menu = Menu('main', [
22 ('/File', '', '<Branch>'),
23 ('/File/Save', 'save', ''),
24 ('/File/Open Parent', 'up', ''),
25 ('/File/Close', 'close', ''),
26 ('/File/', '', '<Separator>'),
27 ('/File/New', 'new', ''),
28 ('/Edit', '', '<Branch>'),
29 ('/Edit/Undo', 'undo', ''),
30 ('/Edit/Redo', 'redo', ''),
31 ('/Edit/', '', '<Separator>'),
32 ('/Edit/Search...', 'search', ''),
33 ('/Edit/Goto line...', 'goto', ''),
34 ('/Edit/', '', '<Separator>'),
35 ('/Edit/Process...', 'process', ''),
36 ('/Options', 'show_options', ''),
37 ('/Help', 'help', '', 'F1'),
40 known_codecs = (
41 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
42 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
43 "iso8859_13", "iso8859_14", "iso8859_15",
44 "ascii", "base64_codec", "charmap",
45 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
46 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
47 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
48 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
49 "cp869", "cp874", "cp875", "hex_codec",
50 "koi8_r",
51 "latin_1",
52 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
53 "mbcs", "quopri_codec", "raw_unicode_escape",
54 "rot_13",
55 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
56 "zlib_codec"
59 class Abort(Exception):
60 pass
62 class Minibuffer:
63 def setup(self):
64 """Called when the minibuffer is opened."""
66 def key_press(self, kev):
67 """A keypress event in the minibuffer text entry."""
69 def changed(self):
70 """The minibuffer text has changed."""
72 def activate(self):
73 """Return or Enter pressed."""
75 info = 'Press Escape to close the minibuffer.'
77 class EditWindow(g.Window, XDSLoader, Saveable):
78 def __init__(self, filename = None):
79 g.Window.__init__(self)
80 XDSLoader.__init__(self, ['text/plain', 'UTF8_STRING'])
81 self.set_default_size(g.gdk.screen_width() * 2 / 3,
82 g.gdk.screen_height() / 2)
84 self.savebox = None
85 self.info_box = None
87 app_options.add_notify(self.update_styles)
89 self.buffer = Buffer()
91 scrollbar = g.VScrollbar()
92 self.v_adj = scrollbar.get_adjustment()
93 self.text = g.TextView()
94 self.text.set_property('left-margin', 4)
95 self.text.set_property('right-margin', 4)
96 self.text.set_buffer(self.buffer)
97 self.text.set_scroll_adjustments(None, self.v_adj)
98 self.text.set_size_request(10, 10)
99 self.xds_proxy_for(self.text)
100 self.text.set_wrap_mode(g.WRAP_WORD)
101 self.update_styles()
103 self.insert_mark = self.buffer.get_mark('insert')
104 self.selection_bound_mark = self.buffer.get_mark('selection_bound')
105 start = self.buffer.get_start_iter()
106 self.mark_start = self.buffer.create_mark('mark_start', start, TRUE)
107 self.mark_end = self.buffer.create_mark('mark_end', start, FALSE)
108 tag = self.buffer.create_tag('marked')
109 tag.set_property('background', 'green')
110 self.marked = 0
112 # When searching, this is where the cursor was when the minibuffer
113 # was opened.
114 start = self.buffer.get_start_iter()
115 self.search_base = self.buffer.create_mark('search_base', start, TRUE)
117 vbox = g.VBox(FALSE)
118 self.add(vbox)
120 tools = g.Toolbar()
121 tools.set_style(g.TOOLBAR_ICONS)
122 vbox.pack_start(tools, FALSE, TRUE, 0)
123 tools.show()
125 tools.insert_stock(g.STOCK_HELP, 'Help', None, self.help, None, 0)
126 tools.insert_stock(g.STOCK_REDO, 'Redo', None, self.redo, None, 0)
127 tools.insert_stock(g.STOCK_UNDO, 'Undo', None, self.undo, None, 0)
128 tools.insert_stock(g.STOCK_FIND, 'Search', None, self.search, None, 0)
129 tools.insert_stock(g.STOCK_SAVE, 'Save', None, self.save, None, 0)
130 tools.insert_stock(g.STOCK_GO_UP, 'Up', None, self.up, None, 0)
131 tools.insert_stock(g.STOCK_CLOSE, 'Close', None, self.close, None, 0)
133 hbox = g.HBox(FALSE) # View + Minibuffer + Scrollbar
134 vbox.pack_start(hbox, TRUE, TRUE)
136 inner_vbox = g.VBox(FALSE) # View + Minibuffer
137 hbox.pack_start(inner_vbox, TRUE, TRUE)
138 inner_vbox.pack_start(self.text, TRUE, TRUE)
139 hbox.pack_start(scrollbar, FALSE, TRUE)
141 self.show_all()
143 # Create the minibuffer
144 self.mini_hbox = g.HBox(FALSE)
145 info = g.Button()
146 info.set_relief(g.RELIEF_NONE)
147 info.unset_flags(g.CAN_FOCUS)
148 image = g.Image()
149 image.set_from_stock(g.STOCK_DIALOG_INFO, size = g.ICON_SIZE_SMALL_TOOLBAR)
150 info.add(image)
151 info.show_all()
152 info.connect('clicked', self.mini_show_info)
154 self.mini_hbox.pack_start(info, FALSE, TRUE, 0)
155 self.mini_label = g.Label('')
156 self.mini_hbox.pack_start(self.mini_label, FALSE, TRUE, 0)
157 self.mini_entry = g.Entry()
158 self.mini_hbox.pack_start(self.mini_entry, TRUE, TRUE, 0)
159 inner_vbox.pack_start(self.mini_hbox, FALSE, TRUE)
160 self.mini_entry.connect('key-press-event', self.mini_key_press)
161 self.mini_entry.connect('changed', self.mini_changed)
163 rox.toplevel_ref()
164 self.connect('destroy', self.destroyed)
166 self.connect('delete-event', self.delete_event)
167 self.text.grab_focus()
169 if filename:
170 import os.path
171 self.uri = os.path.abspath(filename)
172 else:
173 self.uri = None
174 self.update_title()
176 # Loading might take a while, so get something on the screen
177 # now...
178 g.gdk.flush()
180 if filename:
181 try:
182 self.load_file(filename)
183 if filename != '-':
184 self.save_mode = os.stat(filename).st_mode
185 except Abort:
186 self.destroy()
187 raise
189 self.buffer.connect('modified-changed', self.update_title)
190 self.buffer.set_modified(FALSE)
192 self.text.connect('button-press-event', self.button_press)
193 self.text.connect('scroll-event', self.scroll_event)
195 menu.attach(self, self)
196 self.buffer.place_cursor(self.buffer.get_start_iter())
197 self.buffer.start_undo_history()
199 def destroyed(self, widget):
200 app_options.remove_notify(self.update_styles)
201 rox.toplevel_unref()
203 def update_styles(self):
204 try:
205 import pango
206 font = pango.FontDescription(default_font.value)
207 bg = g.gdk.color_parse(background_colour.value)
208 fg = g.gdk.color_parse(foreground_colour.value)
209 except:
210 rox.report_exception()
211 else:
212 self.text.modify_font(font)
213 self.text.modify_base(g.STATE_NORMAL, bg)
214 self.text.modify_text(g.STATE_NORMAL, fg)
216 def button_press(self, text, event):
217 if event.button != 3:
218 return 0
219 menu.popup(self, event)
220 return 1
222 def scroll_event(self, text, event):
223 dir = event.direction
224 new = self.v_adj.value
225 if dir == g.gdk.SCROLL_UP:
226 new -= 32
227 elif dir == g.gdk.SCROLL_DOWN:
228 new += 32
229 self.v_adj.set_value(new)
231 def delete_event(self, window, event):
232 if self.buffer.get_modified():
233 self.save(discard = 1)
234 return 1
235 return 0
237 def update_title(self, *unused):
238 title = self.uri or '<Untitled>'
239 if self.buffer.get_modified():
240 title = title + " *"
241 self.set_title(title)
243 def xds_load_from_stream(self, name, t, stream):
244 if t == 'UTF8_STRING':
245 return # Gtk will handle it
246 try:
247 self.insert_data(stream.read())
248 except Abort:
249 pass
251 def get_encoding(self, message):
252 "Returns (encoding, errors), or raises Abort to cancel."
253 import codecs
255 box = g.MessageDialog(self, 0, g.MESSAGE_QUESTION, g.BUTTONS_CANCEL, message)
256 box.set_has_separator(FALSE)
258 frame = g.Frame()
259 box.vbox.pack_start(frame, TRUE, TRUE)
260 frame.set_border_width(6)
262 hbox = g.HBox(FALSE, 4)
263 hbox.set_border_width(6)
265 hbox.pack_start(g.Label('Encoding:'), FALSE, TRUE, 0)
266 combo = g.Combo()
267 combo.disable_activate()
268 combo.entry.connect('activate', lambda w: box.activate_default())
269 combo.set_popdown_strings(known_codecs)
270 hbox.pack_start(combo, TRUE, TRUE, 0)
271 ignore_errors = g.CheckButton('Ignore errors')
272 hbox.pack_start(ignore_errors, FALSE, TRUE)
274 frame.add(hbox)
276 box.vbox.show_all()
277 box.add_button(g.STOCK_CONVERT, g.RESPONSE_YES)
278 box.set_default_response(g.RESPONSE_YES)
280 while 1:
281 combo.entry.grab_focus()
283 resp = box.run()
284 if resp != g.RESPONSE_YES:
285 box.destroy()
286 raise Abort
288 if ignore_errors.get_active():
289 errors = 'replace'
290 else:
291 errors = 'strict'
292 encoding = combo.entry.get_text()
293 try:
294 codecs.getdecoder(encoding)
295 break
296 except:
297 rox.alert("Unknown encoding '%s'" % encoding)
299 box.destroy()
301 return encoding, errors
303 def insert_data(self, data):
304 import codecs
305 errors = 'strict'
306 encoding = 'utf-8'
307 while 1:
308 decoder = codecs.getdecoder(encoding)
309 try:
310 data = decoder(data, errors)[0]
311 assert '\0' not in data
312 break
313 except:
314 pass
316 encoding, errors = self.get_encoding(
317 "Data is not valid %s. Please select the file's encoding."
318 "Turn on 'ignore errors' to try and load it anyway." % encoding)
320 try:
321 self.buffer.insert_at_cursor(data)
322 except TypeError:
323 self.buffer.insert_at_cursor(data, -1)
324 return 1
326 def load_file(self, path):
327 try:
328 if path == '-':
329 file = sys.stdin
330 else:
331 file = open(path, 'r')
332 contents = file.read()
333 if path != '-':
334 file.close()
335 self.insert_data(contents)
336 except Abort:
337 raise
338 except:
339 rox.report_exception()
340 raise Abort
342 def close(self, button = None):
343 if self.buffer.get_modified():
344 self.save(discard = 1)
345 else:
346 self.destroy()
348 def discard(self):
349 self.destroy()
351 def up(self, button = None):
352 if self.uri:
353 filer.show_file(self.uri)
354 else:
355 rox.alert("File is not saved to disk yet")
357 def has_selection(self):
358 s, e = self.get_selection_range()
359 return not e.equal(s)
361 def get_marked_range(self):
362 s = self.buffer.get_iter_at_mark(self.mark_start)
363 e = self.buffer.get_iter_at_mark(self.mark_end)
364 return s, e
366 def get_selection_range(self):
367 s = self.buffer.get_iter_at_mark(self.insert_mark)
368 e = self.buffer.get_iter_at_mark(self.selection_bound_mark)
369 return s, e
371 def save(self, widget = None, discard = 0):
372 from rox.saving import SaveBox
374 if self.savebox:
375 self.savebox.destroy()
377 if self.has_selection() and not discard:
378 saver = SelectionSaver(self)
379 self.savebox = SaveBox(saver, 'Selection', 'text/plain')
380 self.savebox.connect('destroy', lambda w: saver.destroy())
381 else:
382 uri = self.uri or 'TextFile'
383 self.savebox = SaveBox(self, uri, 'text/plain', discard)
384 self.savebox.show()
386 def help(self, button = None):
387 filer.open_dir(rox.app_dir + '/Help')
389 def save_to_stream(self, stream):
390 s = self.buffer.get_start_iter()
391 e = self.buffer.get_end_iter()
392 stream.write(self.buffer.get_text(s, e, TRUE))
394 def set_uri(self, uri):
395 self.uri = uri
396 self.buffer.set_modified(FALSE)
397 self.update_title()
399 def new(self):
400 EditWindow()
402 def change_font(self):
403 style = self.text.get_style().copy()
404 style.font = load_font(options.get('edit_font'))
405 self.text.set_style(style)
407 def show_options(self):
408 rox.edit_options()
410 def set_marked(self, start = None, end = None):
411 "Set the marked region (from the selection if no region is given)."
412 self.clear_marked()
413 assert not self.marked
415 buffer = self.buffer
416 if start:
417 assert end
418 else:
419 assert not end
420 start, end = self.get_selection_range()
421 buffer.move_mark(self.mark_start, start)
422 buffer.move_mark(self.mark_end, end)
423 buffer.apply_tag_by_name('marked',
424 buffer.get_iter_at_mark(self.mark_start),
425 buffer.get_iter_at_mark(self.mark_end))
426 self.marked = 1
428 def clear_marked(self):
429 if not self.marked:
430 return
431 self.marked = 0
432 buffer = self.buffer
433 buffer.remove_tag_by_name('marked',
434 buffer.get_iter_at_mark(self.mark_start),
435 buffer.get_iter_at_mark(self.mark_end))
437 def undo(self, widget = None):
438 self.buffer.undo()
440 def redo(self, widget = None):
441 self.buffer.redo()
443 def goto(self, widget = None):
444 from goto import Goto
445 self.set_minibuffer(Goto())
447 def search(self, widget = None):
448 from search import Search
449 self.set_minibuffer(Search())
451 def process(self, widget = None):
452 from process import Process
453 self.set_minibuffer(Process())
455 def set_mini_label(self, label):
456 self.mini_label.set_text(label)
458 def set_minibuffer(self, minibuffer):
459 assert minibuffer is None or isinstance(minibuffer, Minibuffer)
461 self.minibuffer = None
463 if minibuffer:
464 self.mini_entry.set_text('')
465 self.minibuffer = minibuffer
466 minibuffer.setup(self)
467 self.mini_entry.grab_focus()
468 self.mini_hbox.show_all()
469 else:
470 self.mini_hbox.hide()
471 self.text.grab_focus()
473 def mini_key_press(self, entry, kev):
474 if kev.keyval == g.keysyms.Escape:
475 self.set_minibuffer(None)
476 return 1
477 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
478 self.minibuffer.activate()
479 return 1
481 return self.minibuffer.key_press(kev)
483 def mini_changed(self, entry):
484 if not self.minibuffer:
485 return
486 self.minibuffer.changed()
488 def mini_show_info(self, *unused):
489 assert self.minibuffer
490 if self.info_box:
491 self.info_box.destroy()
492 self.info_box = g.MessageDialog(self, 0, g.MESSAGE_INFO, g.BUTTONS_OK,
493 self.minibuffer.info)
494 self.info_box.set_title('Minibuffer help')
495 def destroy(box):
496 self.info_box = None
497 self.info_box.connect('destroy', destroy)
498 self.info_box.show()
499 self.info_box.connect('response', lambda w, r: w.destroy())
501 class SelectionSaver(Saveable):
502 def __init__(self, window):
503 self.window = window
504 window.set_marked()
506 def save_to_stream(self, stream):
507 s, e = self.window.get_marked_range()
508 stream.write(self.window.buffer.get_text(s, e, TRUE))
510 def destroy(self):
511 # Called when savebox is remove. Get rid of the selection marker
512 self.window.clear_marked()