Added French translation (Arnaud Calvo).
[rox-edit.git] / EditWindow.py
blob14420306e2f687169714218088bec06647270b3e
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
8 import os
9 import diff
10 import codecs
12 to_utf8 = codecs.getencoder('utf-8')
14 FALSE = g.FALSE
15 TRUE = g.TRUE
17 from rox.Menu import Menu, set_save_name
19 default_font = Option('default_font', 'serif')
21 background_colour = Option('background', '#fff')
22 foreground_colour = Option('foreground', '#000')
24 auto_indent = Option('autoindent', '1')
25 word_wrap = Option('wordwrap', '1')
27 layout_left_margin = Option('layout_left_margin', 2)
28 layout_right_margin = Option('layout_right_margin', 4)
30 layout_before_para = Option('layout_before_para', 0)
31 layout_after_para = Option('layout_after_para', 0)
32 layout_inside_para = Option('layout_inside_para', 0)
33 layout_indent_para = Option('layout_indent_para', 2)
35 set_save_name('Edit')
37 menu = Menu('main', [
38 (_('/File'), '', '<Branch>'),
39 (_('/File/') + _('Save'), 'save', '<StockItem>', '<Ctrl>S', g.STOCK_SAVE),
40 (_('/File/') + _('Open Parent'), 'up', '<StockItem>', '', g.STOCK_GO_UP),
41 (_('/File/') + _('Show Changes'), 'diff', '<StockItem>', '', 'rox-diff'),
42 (_('/File/') + _('Close'), 'close', '<StockItem>', '', g.STOCK_CLOSE),
43 (_('/File/'), '', '<Separator>'),
44 (_('/File/') + _('New'), 'new', '<StockItem>', '', g.STOCK_NEW),
46 (_('/Edit'), '', '<Branch>'),
47 (_('/Edit/') + _('Cut'), 'cut', '<StockItem>', '<Ctrl>X', g.STOCK_CUT),
48 (_('/Edit/') + _('Copy'), 'copy', '<StockItem>', '<Ctrl>C', g.STOCK_COPY),
49 (_('/Edit/') + _('Paste'), 'paste', '<StockItem>', '<Ctrl>V', g.STOCK_PASTE),
50 (_('/Edit/'), '', '<Separator>'),
51 (_('/Edit/') + _('Undo'), 'undo', '<StockItem>', '<Ctrl>Z', g.STOCK_UNDO),
52 (_('/Edit/') + _('Redo'), 'redo', '<StockItem>', '<Ctrl>Y', g.STOCK_REDO),
53 (_('/Edit/'), '', '<Separator>'),
54 (_('/Edit/') + _('Search...'), 'search', '<StockItem>', 'F4', g.STOCK_FIND),
55 (_('/Edit/') + _('Search and Replace....'), 'search_replace',
56 '<StockItem>', '<Ctrl>F4', g.STOCK_FIND_AND_REPLACE),
57 (_('/Edit/') + _('Goto line...'), 'goto', '<StockItem>', 'F5', g.STOCK_JUMP_TO),
59 (_('/Options'), 'show_options', '<StockItem>', '', g.STOCK_PROPERTIES),
60 (_('/Help'), 'help', '<StockItem>', 'F1', g.STOCK_HELP),
63 known_codecs = (
64 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
65 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
66 "iso8859_13", "iso8859_14", "iso8859_15",
67 "ascii", "base64_codec", "charmap",
68 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
69 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
70 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
71 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
72 "cp869", "cp874", "cp875", "hex_codec",
73 "koi8_r",
74 "latin_1",
75 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
76 "mbcs", "quopri_codec", "raw_unicode_escape",
77 "rot_13",
78 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
79 "zlib_codec"
82 class Abort(Exception):
83 pass
85 class Minibuffer:
86 def setup(self):
87 """Called when the minibuffer is opened."""
89 def key_press(self, kev):
90 """A keypress event in the minibuffer text entry."""
92 def changed(self):
93 """The minibuffer text has changed."""
95 def activate(self):
96 """Return or Enter pressed."""
98 info = 'Press Escape to close the minibuffer.'
100 class DiffLoader(XDSLoader):
101 def __init__(self, window):
102 XDSLoader.__init__(self, ['text/plain'])
103 self.window = window
105 def xds_load_from_file(self, path):
106 self.window.diff(path = path)
108 def xds_load_from_stream(self, name, type, stream):
109 tmp = diff.Tmp(suffix = '-' + (name or 'tmp'))
110 import shutil
111 shutil.copyfileobj(stream, tmp)
112 tmp.seek(0)
113 self.window.diff(path = tmp.name)
115 class EditWindow(rox.Window, XDSLoader, Saveable):
116 def __init__(self, filename = None):
117 rox.Window.__init__(self)
118 XDSLoader.__init__(self, ['text/plain', 'UTF8_STRING'])
119 self.set_default_size(g.gdk.screen_width() * 2 / 3,
120 g.gdk.screen_height() / 2)
122 self.savebox = None
123 self.info_box = None
125 app_options.add_notify(self.update_styles)
127 self.buffer = Buffer()
129 self.text = g.TextView()
130 self.text.set_buffer(self.buffer)
131 self.text.set_size_request(10, 10)
132 self.xds_proxy_for(self.text)
133 self.update_styles()
135 self.insert_mark = self.buffer.get_mark('insert')
136 self.selection_bound_mark = self.buffer.get_mark('selection_bound')
137 start = self.buffer.get_start_iter()
138 self.mark_start = self.buffer.create_mark('mark_start', start, TRUE)
139 self.mark_end = self.buffer.create_mark('mark_end', start, FALSE)
140 self.mark_tmp = self.buffer.create_mark('mark_tmp', start, FALSE)
141 tag = self.buffer.create_tag('marked')
142 tag.set_property('background', 'green')
143 self.marked = 0
145 # When searching, this is where the cursor was when the minibuffer
146 # was opened.
147 start = self.buffer.get_start_iter()
148 self.search_base = self.buffer.create_mark('search_base', start, TRUE)
150 vbox = g.VBox(FALSE)
151 self.add(vbox)
153 tools = g.Toolbar()
154 tools.set_style(g.TOOLBAR_ICONS)
155 vbox.pack_start(tools, FALSE, TRUE, 0)
156 tools.show()
158 self.status_label = g.Label('')
159 tools.append_widget(self.status_label, None, None)
160 tools.insert_stock(g.STOCK_HELP, _('Help'), None, self.help, None, 0)
161 diff = tools.insert_stock('rox-diff', _('Show changes from saved copy.\n'
162 'Or, drop a backup file onto this button to see changes from that.'),
163 None, self.diff, None, 0)
164 DiffLoader(self).xds_proxy_for(diff)
165 tools.insert_stock(g.STOCK_REDO, _('Redo'), None, self.redo, None, 0)
166 tools.insert_stock(g.STOCK_UNDO, _('Undo'), None, self.undo, None, 0)
167 tools.insert_stock(g.STOCK_FIND_AND_REPLACE, _('Replace'), None, self.search_replace, None, 0)
168 tools.insert_stock(g.STOCK_FIND, _('Search'), None, self.search, None, 0)
169 tools.insert_stock(g.STOCK_SAVE, _('Save'), None, self.save, None, 0)
170 tools.insert_stock(g.STOCK_GO_UP, _('Up'), None, self.up, None, 0)
171 tools.insert_stock(g.STOCK_CLOSE, _('Close'), None, self.close, None, 0)
172 # Set minimum size to ignore the label
173 tools.set_size_request(tools.size_request()[0], -1)
175 swin = g.ScrolledWindow()
176 swin.set_policy(g.POLICY_AUTOMATIC, g.POLICY_AUTOMATIC)
177 vbox.pack_start(swin, True, True)
179 swin.add(self.text)
181 self.show_all()
183 # Create the minibuffer
184 self.mini_hbox = g.HBox(FALSE)
185 info = g.Button()
186 info.set_relief(g.RELIEF_NONE)
187 info.unset_flags(g.CAN_FOCUS)
188 image = g.Image()
189 image.set_from_stock(g.STOCK_DIALOG_INFO, size = g.ICON_SIZE_SMALL_TOOLBAR)
190 info.add(image)
191 info.show_all()
192 info.connect('clicked', self.mini_show_info)
194 self.mini_hbox.pack_start(info, FALSE, TRUE, 0)
195 self.mini_label = g.Label('')
196 self.mini_hbox.pack_start(self.mini_label, FALSE, TRUE, 0)
197 self.mini_entry = g.Entry()
198 self.mini_hbox.pack_start(self.mini_entry, TRUE, TRUE, 0)
199 vbox.pack_start(self.mini_hbox, FALSE, TRUE)
200 self.mini_entry.connect('key-press-event', self.mini_key_press)
201 self.mini_entry.connect('changed', self.mini_changed)
203 self.connect('destroy', self.destroyed)
205 self.connect('delete-event', self.delete_event)
206 self.text.grab_focus()
207 self.text.connect('key-press-event', self.key_press)
209 def update_current_line(*unused):
210 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
211 bound = self.buffer.get_iter_at_mark(self.selection_bound_mark)
212 if cursor.compare(bound) == 0:
213 n_lines = self.buffer.get_line_count()
214 self.status_label.set_text(_('Line %s of %d') % (cursor.get_line() + 1, n_lines))
215 else:
216 n_lines = abs(cursor.get_line() - bound.get_line()) + 1
217 if n_lines == 1:
218 n_chars = abs(cursor.get_line_offset() - bound.get_line_offset())
219 if n_chars == 1:
220 bytes = to_utf8(self.buffer.get_text(cursor, bound, False))[0]
221 self.status_label.set_text(_('One character selected (%s)') %
222 ' '.join(map(lambda x: '0x%2x' % ord(x), bytes)))
223 else:
224 self.status_label.set_text(_('%d characters selected') % n_chars)
225 else:
226 self.status_label.set_text(_('%d lines selected') % n_lines)
227 self.buffer.connect('mark-set', update_current_line)
228 self.buffer.connect('changed', update_current_line)
230 if filename:
231 import os.path
232 self.uri = os.path.abspath(filename)
233 else:
234 self.uri = None
235 self.update_title()
237 # Loading might take a while, so get something on the screen
238 # now...
239 g.gdk.flush()
241 if filename:
242 try:
243 self.load_file(filename)
244 if filename != '-':
245 self.save_mode = os.stat(filename).st_mode
246 except Abort:
247 self.destroy()
248 raise
250 self.buffer.connect('modified-changed', self.update_title)
251 self.buffer.set_modified(FALSE)
253 def button_press(text, event):
254 if event.button != 3:
255 return False
256 menu.popup(self, event)
257 return True
258 self.text.connect('button-press-event', button_press)
259 self.text.connect('popup-menu', lambda text: menu.popup(self, None))
261 menu.attach(self, self)
262 self.buffer.place_cursor(self.buffer.get_start_iter())
263 self.buffer.start_undo_history()
265 def key_press(self, text, kev):
266 if kev.keyval != g.keysyms.Return and kev.keyval != g.keysyms.KP_Enter:
267 return
268 if not auto_indent.int_value:
269 return
270 start = self.buffer.get_iter_at_mark(self.insert_mark)
271 end = start.copy()
272 start.set_line_offset(0)
273 end.forward_to_line_end()
274 line = self.buffer.get_text(start, end, False)
275 indent = ''
276 for x in line:
277 if x in ' \t':
278 indent += x
279 else:
280 break
281 self.buffer.begin_user_action()
282 self.buffer.insert_at_cursor('\n' + indent)
283 self.buffer.end_user_action()
284 return True
286 def destroyed(self, widget):
287 app_options.remove_notify(self.update_styles)
289 def update_styles(self):
290 try:
291 import pango
292 font = pango.FontDescription(default_font.value)
293 bg = g.gdk.color_parse(background_colour.value)
294 fg = g.gdk.color_parse(foreground_colour.value)
296 self.text.set_left_margin(layout_left_margin.int_value)
297 self.text.set_right_margin(layout_right_margin.int_value)
299 self.text.set_pixels_above_lines(layout_before_para.int_value)
300 self.text.set_pixels_below_lines(layout_after_para.int_value)
301 self.text.set_pixels_inside_wrap(layout_inside_para.int_value)
302 self.text.set_indent(layout_indent_para.int_value)
304 if word_wrap.int_value == 1:
305 self.text.set_wrap_mode(g.WRAP_WORD)
306 else:
307 self.text.set_wrap_mode(g.WRAP_NONE)
308 except:
309 rox.report_exception()
310 else:
311 self.text.modify_font(font)
312 self.text.modify_base(g.STATE_NORMAL, bg)
313 self.text.modify_text(g.STATE_NORMAL, fg)
315 def cut(self): self.text.emit('cut_clipboard')
316 def copy(self): self.text.emit('copy_clipboard')
317 def paste(self): self.text.emit('paste_clipboard')
319 def delete_event(self, window, event):
320 if self.buffer.get_modified():
321 self.save(discard = 1)
322 return 1
323 return 0
325 def update_title(self, *unused):
326 title = self.uri or '<Untitled>'
327 if self.buffer.get_modified():
328 title = title + " *"
329 self.set_title(title)
331 def xds_load_from_stream(self, name, t, stream):
332 if t == 'UTF8_STRING':
333 return # Gtk will handle it
334 try:
335 self.insert_data(stream.read())
336 except Abort:
337 pass
339 def get_encoding(self, message):
340 "Returns (encoding, errors), or raises Abort to cancel."
341 box = g.MessageDialog(self, 0, g.MESSAGE_QUESTION, g.BUTTONS_CANCEL, message)
342 box.set_has_separator(FALSE)
344 frame = g.Frame()
345 box.vbox.pack_start(frame, TRUE, TRUE)
346 frame.set_border_width(6)
348 hbox = g.HBox(FALSE, 4)
349 hbox.set_border_width(6)
351 hbox.pack_start(g.Label('Encoding:'), FALSE, TRUE, 0)
352 combo = g.Combo()
353 combo.disable_activate()
354 combo.entry.connect('activate', lambda w: box.activate_default())
355 combo.set_popdown_strings(known_codecs)
356 hbox.pack_start(combo, TRUE, TRUE, 0)
357 ignore_errors = g.CheckButton('Ignore errors')
358 hbox.pack_start(ignore_errors, FALSE, TRUE)
360 frame.add(hbox)
362 box.vbox.show_all()
363 box.add_button(g.STOCK_CONVERT, g.RESPONSE_YES)
364 box.set_default_response(g.RESPONSE_YES)
366 while 1:
367 combo.entry.grab_focus()
369 resp = box.run()
370 if resp != g.RESPONSE_YES:
371 box.destroy()
372 raise Abort
374 if ignore_errors.get_active():
375 errors = 'replace'
376 else:
377 errors = 'strict'
378 encoding = combo.entry.get_text()
379 try:
380 codecs.getdecoder(encoding)
381 break
382 except:
383 rox.alert(_("Unknown encoding '%s'") % encoding)
385 box.destroy()
387 return encoding, errors
389 def insert_data(self, data):
390 import codecs
391 errors = 'strict'
392 encoding = 'utf-8'
393 while 1:
394 decoder = codecs.getdecoder(encoding)
395 try:
396 data = decoder(data, errors)[0]
397 if errors == 'strict':
398 assert '\0' not in data
399 else:
400 if '\0' in data:
401 data = data.replace('\0', '\\0')
402 break
403 except:
404 pass
406 encoding, errors = self.get_encoding(
407 _("Data is not valid %s. Please select the file's encoding. "
408 "Turn on 'ignore errors' to try and load it anyway.")
409 % encoding)
411 self.buffer.begin_user_action()
412 self.buffer.insert_at_cursor(data)
413 self.buffer.end_user_action()
414 return 1
416 def load_file(self, path):
417 try:
418 if path == '-':
419 file = sys.stdin
420 else:
421 file = open(path, 'r')
422 contents = file.read()
423 if path != '-':
424 file.close()
425 self.insert_data(contents)
426 except Abort:
427 raise
428 except:
429 rox.report_exception()
430 raise Abort
432 def close(self, button = None):
433 if self.buffer.get_modified():
434 self.save(discard = 1)
435 else:
436 self.destroy()
438 def discard(self):
439 self.destroy()
441 def up(self, button = None):
442 if self.uri:
443 filer.show_file(self.uri)
444 else:
445 rox.alert(_('File is not saved to disk yet'))
447 def diff(self, button = None, path = None):
448 path = path or self.uri
449 if not path:
450 rox.alert(_('This file has never been saved; nothing to compare it to!\n'
451 'Note: you can drop a file onto the toolbar button to see '
452 'the changes from that file.'))
453 return
454 diff.show_diff(path, self.save_to_stream)
456 def has_selection(self):
457 s, e = self.get_selection_range()
458 return not e.equal(s)
460 def get_marked_range(self):
461 s = self.buffer.get_iter_at_mark(self.mark_start)
462 e = self.buffer.get_iter_at_mark(self.mark_end)
463 if s.compare(e) > 0:
464 return e, s
465 return s, e
467 def get_selection_range(self):
468 s = self.buffer.get_iter_at_mark(self.insert_mark)
469 e = self.buffer.get_iter_at_mark(self.selection_bound_mark)
470 if s.compare(e) > 0:
471 return e, s
472 return s, e
474 def save(self, widget = None, discard = 0):
475 from rox.saving import SaveBox
477 if self.savebox:
478 self.savebox.destroy()
480 if self.has_selection() and not discard:
481 saver = SelectionSaver(self)
482 self.savebox = SaveBox(saver, 'Selection', 'text/plain')
483 self.savebox.connect('destroy', lambda w: saver.destroy())
484 else:
485 uri = self.uri or 'TextFile'
486 self.savebox = SaveBox(self, uri, 'text/plain', discard)
487 self.savebox.show()
489 def help(self, button = None):
490 filer.open_dir(os.path.join(rox.app_dir, 'Help'))
492 def save_to_stream(self, stream):
493 s = self.buffer.get_start_iter()
494 e = self.buffer.get_end_iter()
495 stream.write(self.buffer.get_text(s, e, TRUE))
497 def set_uri(self, uri):
498 self.uri = uri
499 self.buffer.set_modified(FALSE)
500 self.update_title()
502 def new(self):
503 EditWindow()
505 def change_font(self):
506 style = self.text.get_style().copy()
507 style.font = load_font(options.get('edit_font'))
508 self.text.set_style(style)
510 def show_options(self):
511 rox.edit_options()
513 def set_marked(self, start = None, end = None):
514 "Set the marked region (from the selection if no region is given)."
515 self.clear_marked()
516 assert not self.marked
518 buffer = self.buffer
519 if start:
520 assert end
521 else:
522 assert not end
523 start, end = self.get_selection_range()
524 buffer.move_mark(self.mark_start, start)
525 buffer.move_mark(self.mark_end, end)
526 buffer.apply_tag_by_name('marked',
527 buffer.get_iter_at_mark(self.mark_start),
528 buffer.get_iter_at_mark(self.mark_end))
529 self.marked = 1
531 def clear_marked(self):
532 if not self.marked:
533 return
534 self.marked = 0
535 buffer = self.buffer
536 buffer.remove_tag_by_name('marked',
537 buffer.get_iter_at_mark(self.mark_start),
538 buffer.get_iter_at_mark(self.mark_end))
540 def undo(self, widget = None):
541 self.buffer.undo()
543 def redo(self, widget = None):
544 self.buffer.redo()
546 def goto(self, widget = None):
547 from goto import Goto
548 self.set_minibuffer(Goto())
550 def search(self, widget = None):
551 from search import Search
552 self.set_minibuffer(Search())
554 def search_replace(self, widget = None):
555 from search import Replace
556 Replace(self).show()
558 def set_mini_label(self, label):
559 self.mini_label.set_text(label)
561 def set_minibuffer(self, minibuffer):
562 assert minibuffer is None or isinstance(minibuffer, Minibuffer)
564 self.minibuffer = None
566 if minibuffer:
567 self.mini_entry.set_text('')
568 self.minibuffer = minibuffer
569 minibuffer.setup(self)
570 self.mini_entry.grab_focus()
571 self.mini_hbox.show_all()
572 else:
573 self.mini_hbox.hide()
574 self.text.grab_focus()
576 def mini_key_press(self, entry, kev):
577 if kev.keyval == g.keysyms.Escape:
578 self.set_minibuffer(None)
579 return 1
580 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
581 self.minibuffer.activate()
582 return 1
584 return self.minibuffer.key_press(kev)
586 def mini_changed(self, entry):
587 if not self.minibuffer:
588 return
589 self.minibuffer.changed()
591 def mini_show_info(self, *unused):
592 assert self.minibuffer
593 if self.info_box:
594 self.info_box.destroy()
595 self.info_box = g.MessageDialog(self, 0, g.MESSAGE_INFO, g.BUTTONS_OK,
596 self.minibuffer.info)
597 self.info_box.set_title(_('Minibuffer help'))
598 def destroy(box):
599 self.info_box = None
600 self.info_box.connect('destroy', destroy)
601 self.info_box.show()
602 self.info_box.connect('response', lambda w, r: w.destroy())
604 def process_selected(self, process):
605 """Calls process(line) on each line in the selection, or each line in the file
606 if there is no selection. If the result is not None, the text is replaced."""
607 self.buffer.begin_user_action()
608 self._process_selected(process)
609 self.buffer.end_user_action()
611 def _process_selected(self, process):
612 if self.has_selection():
613 def get_end():
614 start, end = self.get_selection_range()
615 if start.compare(end) > 0:
616 return start
617 return end
618 start, end = self.get_selection_range()
619 if start.compare(end) > 0:
620 start = end
621 else:
622 def get_end():
623 return self.buffer.get_end_iter()
624 start = self.buffer.get_start_iter()
625 end = get_end()
627 while start.compare(end) <= 0:
628 line_end = start.copy()
629 line_end.forward_to_line_end()
630 if line_end.compare(end) >= 0:
631 line_end = end
632 line = self.buffer.get_text(start, line_end, False)
633 new = process(line)
634 if new is not None:
635 self.buffer.move_mark(self.mark_tmp, start)
636 self.buffer.insert(line_end, new)
637 start = self.buffer.get_iter_at_mark(self.mark_tmp)
638 line_end = start.copy()
639 line_end.forward_chars(len(line))
640 self.buffer.delete(start, line_end)
642 start = self.buffer.get_iter_at_mark(self.mark_tmp)
643 end = get_end()
644 if not start.forward_line(): break
646 class SelectionSaver(Saveable):
647 def __init__(self, window):
648 self.window = window
649 window.set_marked()
651 def save_to_stream(self, stream):
652 s, e = self.window.get_marked_range()
653 stream.write(self.window.buffer.get_text(s, e, TRUE))
655 def destroy(self):
656 # Called when savebox is remove. Get rid of the selection marker
657 self.window.clear_marked()