Fix for untested sourceview patch (Guido Schimmels).
[rox-edit.git] / EditWindow.py
blob37983a924e7cc0098c0a6f24104a09e94522cb4b
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 rox.saving import Saveable
7 import os
8 import diff
9 import codecs
11 # WARNING: This is a temporary hack, until we write a way choose between
12 # the two ways of doing toolbars or we abandon the old method entirely
13 import warnings
14 warnings.filterwarnings('ignore', category=DeprecationWarning,
15 module='EditWindow')
16 # End temporary hack
18 to_utf8 = codecs.getencoder('utf-8')
20 from buffer import Buffer, have_sourceview
21 from rox.Menu import Menu, set_save_name, SubMenu, Separator, Action, ToggleItem
23 default_font = Option('default_font', 'serif')
25 background_colour = Option('background', '#fff')
26 foreground_colour = Option('foreground', '#000')
28 auto_indent = Option('autoindent', '1')
29 word_wrap = Option('wordwrap', '1')
31 layout_left_margin = Option('layout_left_margin', 2)
32 layout_right_margin = Option('layout_right_margin', 4)
34 layout_before_para = Option('layout_before_para', 0)
35 layout_after_para = Option('layout_after_para', 0)
36 layout_inside_para = Option('layout_inside_para', 0)
37 layout_indent_para = Option('layout_indent_para', 2)
39 show_toolbar = Option('show_toolbar', 1)
41 set_save_name('Edit')
43 menu = Menu('main', [
44 SubMenu(_('File'), [
45 Action(_('Save As...'), 'save_as', '<Ctrl>S', g.STOCK_SAVE),
46 Action(_('Open Parent'), 'up', '', g.STOCK_GO_UP),
47 Action(_('Show Changes'), 'diff', '', 'rox-diff'),
48 ToggleItem(_('Word Wrap'), 'word_wrap'),
49 Action(_('Close'), 'close', '', g.STOCK_CLOSE),
50 Separator(),
51 Action(_('New'), 'new', '', g.STOCK_NEW)]),
53 SubMenu(_('Edit'), [
54 Action(_('Cut'), 'cut', '<Ctrl>X', g.STOCK_CUT),
55 Action(_('Copy'), 'copy', '<Ctrl>C', g.STOCK_COPY),
56 Action(_('Paste'), 'paste', '<Ctrl>V', g.STOCK_PASTE),
57 Separator(),
58 Action(_('Undo'), 'undo', '<Ctrl>Z', g.STOCK_UNDO),
59 Action(_('Redo'), 'redo', '<Ctrl>Y', g.STOCK_REDO),
60 Separator(),
61 Action(_('Search...'), 'search', 'F4', g.STOCK_FIND),
62 Action(_('Search and Replace....'), 'search_replace',
63 '<Ctrl>F4', g.STOCK_FIND_AND_REPLACE),
64 Action(_('Goto line...'), 'goto', 'F5', g.STOCK_JUMP_TO)]),
66 Action(_('Options'), 'show_options', '', g.STOCK_PROPERTIES),
67 Action(_('Help'), 'help', 'F1', g.STOCK_HELP),
70 known_codecs = (
71 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
72 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
73 "iso8859_13", "iso8859_14", "iso8859_15",
74 "ascii", "base64_codec", "charmap",
75 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
76 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
77 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
78 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
79 "cp869", "cp874", "cp875", "hex_codec",
80 "koi8_r",
81 "latin_1",
82 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
83 "mbcs", "quopri_codec", "raw_unicode_escape",
84 "rot_13",
85 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
86 "zlib_codec"
89 class Abort(Exception):
90 pass
92 class Minibuffer:
93 def setup(self):
94 """Called when the minibuffer is opened."""
96 def key_press(self, kev):
97 """A keypress event in the minibuffer text entry."""
99 def changed(self):
100 """The minibuffer text has changed."""
102 def activate(self):
103 """Return or Enter pressed."""
105 info = 'Press Escape to close the minibuffer.'
107 class DiffLoader(XDSLoader):
108 def __init__(self, window):
109 XDSLoader.__init__(self, ['text/plain'])
110 self.window = window
112 def xds_load_from_file(self, path):
113 self.window.diff(path = path)
115 def xds_load_from_stream(self, name, type, stream):
116 tmp = diff.Tmp(suffix = '-' + (name or 'tmp'))
117 import shutil
118 shutil.copyfileobj(stream, tmp)
119 tmp.seek(0)
120 self.window.diff(path = tmp.name)
122 class EditWindow(rox.Window, XDSLoader, Saveable):
123 _word_wrap = False
124 wrap_button = None
126 def __init__(self, filename = None, show = True):
127 rox.Window.__init__(self)
128 XDSLoader.__init__(self, ['text/plain', 'UTF8_STRING'])
129 self.set_default_size(g.gdk.screen_width() * 2 / 3,
130 g.gdk.screen_height() / 2)
132 self.savebox = None
133 self.info_box = None
135 app_options.add_notify(self.update_styles)
137 if filename:
138 import os.path
139 self.uri = os.path.abspath(filename)
140 else:
141 self.uri = None
143 self.buffer = Buffer()
145 if have_sourceview:
146 import gtksourceview
147 self.text = gtksourceview.SourceView(self.buffer)
148 self.text.set_show_line_numbers(True)
149 self.text.set_show_line_markers(True)
150 self.text.set_auto_indent(True)
151 self.text.set_smart_home_end(True)
152 if self.uri:
153 from rox import mime
154 self.buffer.set_type(mime.get_type(self.uri, 1))
155 else:
156 self.text = g.TextView()
157 self.text.set_buffer(self.buffer)
159 self.update_title()
161 self.text.set_size_request(10, 10)
162 self.xds_proxy_for(self.text)
164 self.insert_mark = self.buffer.get_mark('insert')
165 self.selection_bound_mark = self.buffer.get_mark('selection_bound')
166 start = self.buffer.get_start_iter()
167 self.mark_start = self.buffer.create_mark('mark_start', start, True)
168 self.mark_end = self.buffer.create_mark('mark_end', start, False)
169 self.mark_tmp = self.buffer.create_mark('mark_tmp', start, False)
170 tag = self.buffer.create_tag('marked')
171 tag.set_property('background', 'green')
172 self.marked = 0
174 # When searching, this is where the cursor was when the minibuffer
175 # was opened.
176 start = self.buffer.get_start_iter()
177 self.search_base = self.buffer.create_mark('search_base', start, True)
179 vbox = g.VBox(False)
180 self.add(vbox)
182 tools = g.Toolbar()
183 tools.set_style(g.TOOLBAR_ICONS)
184 vbox.pack_start(tools, False, True, 0)
186 self.status_label = g.Label('')
187 tools.append_widget(self.status_label, None, None)
188 tools.insert_stock(g.STOCK_HELP, _('Help'), None, self.help, None, 0)
189 diff = tools.insert_stock('rox-diff', _('Show changes from saved copy.\n'
190 'Or, drop a backup file onto this button to see changes from that.'),
191 None, self.diff, None, 0)
192 DiffLoader(self).xds_proxy_for(diff)
194 image_wrap = g.Image()
195 image_wrap.set_from_file(rox.app_dir + '/images/rox-word-wrap.png')
196 self.wrap_button = tools.insert_element(g.TOOLBAR_CHILD_TOGGLEBUTTON,
197 None, _("Word Wrap"), _("Word Wrap"), None,
198 image_wrap,
199 lambda button: self.set_word_wrap(button.get_active()),
200 None, 0)
201 tools.insert_stock(g.STOCK_REDO, _('Redo'), None, self.redo, None, 0)
202 tools.insert_stock(g.STOCK_UNDO, _('Undo'), None, self.undo, None, 0)
203 tools.insert_stock(g.STOCK_FIND_AND_REPLACE, _('Replace'), None, self.search_replace, None, 0)
204 tools.insert_stock(g.STOCK_FIND, _('Search'), None, self.search, None, 0)
205 tools.insert_stock(g.STOCK_SAVE, _('Save'), None, self.save_as, None, 0)
206 tools.insert_stock(g.STOCK_GO_UP, _('Up'), None, self.up, None, 0)
207 tools.insert_stock(g.STOCK_CLOSE, _('Close'), None, self.close, None, 0)
208 # Set minimum size to ignore the label
209 tools.set_size_request(tools.size_request()[0], -1)
211 self.tools = tools
213 swin = g.ScrolledWindow()
214 swin.set_policy(g.POLICY_AUTOMATIC, g.POLICY_AUTOMATIC)
215 vbox.pack_start(swin, True, True)
217 swin.add(self.text)
219 if show:
220 self.show_all()
221 self.update_styles()
223 # Create the minibuffer
224 self.mini_hbox = g.HBox(False)
225 info = g.Button()
226 info.set_relief(g.RELIEF_NONE)
227 info.unset_flags(g.CAN_FOCUS)
228 image = g.Image()
229 image.set_from_stock(g.STOCK_DIALOG_INFO, size = g.ICON_SIZE_SMALL_TOOLBAR)
230 info.add(image)
231 info.show_all()
232 info.connect('clicked', self.mini_show_info)
234 self.mini_hbox.pack_start(info, False, True, 0)
235 self.mini_label = g.Label('')
236 self.mini_hbox.pack_start(self.mini_label, False, True, 0)
237 self.mini_entry = g.Entry()
238 self.mini_hbox.pack_start(self.mini_entry, True, True, 0)
239 vbox.pack_start(self.mini_hbox, False, True)
240 self.mini_entry.connect('key-press-event', self.mini_key_press)
241 self.mini_entry.connect('changed', self.mini_changed)
243 self.connect('destroy', self.destroyed)
245 self.connect('delete-event', self.delete_event)
246 self.text.grab_focus()
247 self.text.connect('key-press-event', self.key_press)
249 def update_current_line(*unused):
250 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
251 bound = self.buffer.get_iter_at_mark(self.selection_bound_mark)
252 if cursor.compare(bound) == 0:
253 n_lines = self.buffer.get_line_count()
254 self.status_label.set_text(_('Line %s of %d') % (cursor.get_line() + 1, n_lines))
255 else:
256 n_lines = abs(cursor.get_line() - bound.get_line()) + 1
257 if n_lines == 1:
258 n_chars = abs(cursor.get_line_offset() - bound.get_line_offset())
259 if n_chars == 1:
260 bytes = to_utf8(self.buffer.get_text(cursor, bound, False))[0]
261 self.status_label.set_text(_('One character selected (%s)') %
262 ' '.join(map(lambda x: '0x%2x' % ord(x), bytes)))
263 else:
264 self.status_label.set_text(_('%d characters selected') % n_chars)
265 else:
266 self.status_label.set_text(_('%d lines selected') % n_lines)
267 self.buffer.connect('mark-set', update_current_line)
268 self.buffer.connect('changed', update_current_line)
271 # Loading might take a while, so get something on the screen
272 # now...
273 g.gdk.flush()
275 if filename:
276 try:
277 self.load_file(filename)
278 if filename != '-':
279 self.save_last_stat = os.stat(filename)
280 except Abort:
281 self.destroy()
282 raise
284 self.buffer.connect('modified-changed', self.update_title)
285 self.buffer.set_modified(False)
287 def button_press(text, event):
288 if event.button != 3:
289 return False
290 menu.popup(self, event)
291 return True
292 self.text.connect('button-press-event', button_press)
293 self.text.connect('popup-menu', lambda text: menu.popup(self, None))
295 menu.attach(self, self)
296 self.buffer.place_cursor(self.buffer.get_start_iter())
297 self.buffer.start_undo_history()
299 def key_press(self, text, kev):
300 if kev.keyval != g.keysyms.Return and kev.keyval != g.keysyms.KP_Enter:
301 return
302 if not auto_indent.int_value:
303 return
304 start = self.buffer.get_iter_at_mark(self.insert_mark)
305 end = start.copy()
306 start.set_line_offset(0)
307 end.forward_to_line_end()
308 line = self.buffer.get_text(start, end, False)
309 indent = ''
310 for x in line:
311 if x in ' \t':
312 indent += x
313 else:
314 break
315 self.buffer.begin_user_action()
316 self.buffer.insert_at_cursor('\n' + indent)
317 self.buffer.end_user_action()
318 return True
320 def destroyed(self, widget):
321 app_options.remove_notify(self.update_styles)
323 def update_styles(self):
324 try:
325 import pango
326 font = pango.FontDescription(default_font.value)
327 bg = g.gdk.color_parse(background_colour.value)
328 fg = g.gdk.color_parse(foreground_colour.value)
330 self.text.set_left_margin(layout_left_margin.int_value)
331 self.text.set_right_margin(layout_right_margin.int_value)
333 self.text.set_pixels_above_lines(layout_before_para.int_value)
334 self.text.set_pixels_below_lines(layout_after_para.int_value)
335 self.text.set_pixels_inside_wrap(layout_inside_para.int_value)
336 self.text.set_indent(layout_indent_para.int_value)
338 self.word_wrap = bool(word_wrap.int_value)
340 if show_toolbar.int_value:
341 self.tools.show()
342 else:
343 self.tools.hide()
344 except:
345 rox.report_exception()
346 else:
347 self.text.modify_font(font)
348 self.text.modify_base(g.STATE_NORMAL, bg)
349 self.text.modify_text(g.STATE_NORMAL, fg)
351 def cut(self): self.text.emit('cut_clipboard')
352 def copy(self): self.text.emit('copy_clipboard')
353 def paste(self): self.text.emit('paste_clipboard')
355 def delete_event(self, window, event):
356 if self.buffer.get_modified():
357 self.save_as(discard = 1)
358 return 1
359 return 0
361 def update_title(self, *unused):
362 title = self.uri or '<Untitled>'
363 if self.buffer.get_modified():
364 title = title + " *"
365 self.set_title(title)
367 def xds_load_from_stream(self, name, t, stream):
368 if t == 'UTF8_STRING':
369 return # Gtk will handle it
370 try:
371 self.insert_data(stream.read())
372 except Abort:
373 pass
375 def get_encoding(self, message):
376 "Returns (encoding, errors), or raises Abort to cancel."
377 box = g.MessageDialog(self, 0, g.MESSAGE_QUESTION, g.BUTTONS_CANCEL, message)
378 box.set_has_separator(False)
380 frame = g.Frame()
381 box.vbox.pack_start(frame, True, True)
382 frame.set_border_width(6)
384 hbox = g.HBox(False, 4)
385 hbox.set_border_width(6)
387 hbox.pack_start(g.Label(_('Encoding:')), False, True, 0)
388 combo = g.Combo()
389 combo.disable_activate()
390 combo.entry.connect('activate', lambda w: box.activate_default())
391 combo.set_popdown_strings(known_codecs)
392 hbox.pack_start(combo, True, True, 0)
393 ignore_errors = g.CheckButton(_('Ignore errors'))
394 hbox.pack_start(ignore_errors, False, True)
396 frame.add(hbox)
398 box.vbox.show_all()
399 box.add_button(g.STOCK_CONVERT, g.RESPONSE_YES)
400 box.set_default_response(g.RESPONSE_YES)
402 while 1:
403 combo.entry.grab_focus()
405 resp = box.run()
406 if resp != g.RESPONSE_YES:
407 box.destroy()
408 raise Abort
410 if ignore_errors.get_active():
411 errors = 'replace'
412 else:
413 errors = 'strict'
414 encoding = combo.entry.get_text()
415 try:
416 codecs.getdecoder(encoding)
417 break
418 except:
419 rox.alert(_("Unknown encoding '%s'") % encoding)
421 box.destroy()
423 return encoding, errors
425 def insert_data(self, data):
426 import codecs
427 errors = 'strict'
428 encoding = 'utf-8'
429 while 1:
430 decoder = codecs.getdecoder(encoding)
431 try:
432 data = decoder(data, errors)[0]
433 if errors == 'strict':
434 assert '\0' not in data
435 else:
436 if '\0' in data:
437 data = data.replace('\0', '\\0')
438 break
439 except:
440 pass
442 encoding, errors = self.get_encoding(
443 _("Data is not valid %s. Please select the file's encoding. "
444 "Turn on 'ignore errors' to try and load it anyway.")
445 % encoding)
447 self.buffer.begin_user_action()
448 self.buffer.insert_at_cursor(data)
449 self.buffer.end_user_action()
450 return 1
452 def load_file(self, path):
453 try:
454 if path == '-':
455 file = sys.stdin
456 else:
457 file = open(path, 'r')
458 contents = file.read()
459 if path != '-':
460 file.close()
461 self.insert_data(contents)
462 except Abort:
463 raise
464 except:
465 rox.report_exception()
466 raise Abort
468 def close(self, button = None):
469 if self.buffer.get_modified():
470 self.save_as(discard = 1)
471 else:
472 self.destroy()
474 def discard(self):
475 self.destroy()
477 def up(self, button = None):
478 if self.uri:
479 filer.show_file(self.uri)
480 else:
481 rox.alert(_('File is not saved to disk yet'))
483 def diff(self, button = None, path = None):
484 path = path or self.uri
485 if not path:
486 rox.alert(_('This file has never been saved; nothing to compare it to!\n'
487 'Note: you can drop a file onto the toolbar button to see '
488 'the changes from that file.'))
489 return
490 diff.show_diff(path, self.save_to_stream)
492 def has_selection(self):
493 s, e = self.get_selection_range()
494 return not e.equal(s)
496 def get_marked_range(self):
497 s = self.buffer.get_iter_at_mark(self.mark_start)
498 e = self.buffer.get_iter_at_mark(self.mark_end)
499 if s.compare(e) > 0:
500 return e, s
501 return s, e
503 def get_selection_range(self):
504 s = self.buffer.get_iter_at_mark(self.insert_mark)
505 e = self.buffer.get_iter_at_mark(self.selection_bound_mark)
506 if s.compare(e) > 0:
507 return e, s
508 return s, e
510 def save_as(self, widget = None, discard = 0):
511 from rox.saving import SaveBox
513 if self.savebox:
514 self.savebox.destroy()
516 if self.has_selection() and not discard:
517 saver = SelectionSaver(self)
518 self.savebox = SaveBox(saver, 'Selection', 'text/plain')
519 self.savebox.connect('destroy', lambda w: saver.destroy())
520 else:
521 uri = self.uri or 'TextFile'
522 self.savebox = SaveBox(self, uri, 'text/plain', discard)
523 self.savebox.show()
525 def help(self, button = None):
526 filer.open_dir(os.path.join(rox.app_dir, 'Help'))
528 def save_to_stream(self, stream):
529 s = self.buffer.get_start_iter()
530 e = self.buffer.get_end_iter()
531 stream.write(self.buffer.get_text(s, e, True))
533 def set_uri(self, uri):
534 self.uri = uri
535 self.buffer.set_modified(False)
536 self.update_title()
538 def new(self):
539 EditWindow()
541 def change_font(self):
542 style = self.text.get_style().copy()
543 style.font = load_font(options.get('edit_font'))
544 self.text.set_style(style)
546 def show_options(self):
547 rox.edit_options()
549 def set_marked(self, start = None, end = None):
550 "Set the marked region (from the selection if no region is given)."
551 self.clear_marked()
552 assert not self.marked
554 buffer = self.buffer
555 if start:
556 assert end
557 else:
558 assert not end
559 start, end = self.get_selection_range()
560 buffer.move_mark(self.mark_start, start)
561 buffer.move_mark(self.mark_end, end)
562 buffer.apply_tag_by_name('marked',
563 buffer.get_iter_at_mark(self.mark_start),
564 buffer.get_iter_at_mark(self.mark_end))
565 self.marked = 1
567 def clear_marked(self):
568 if not self.marked:
569 return
570 self.marked = 0
571 buffer = self.buffer
572 buffer.remove_tag_by_name('marked',
573 buffer.get_iter_at_mark(self.mark_start),
574 buffer.get_iter_at_mark(self.mark_end))
576 def undo(self, widget = None):
577 self.buffer.undo()
579 def redo(self, widget = None):
580 self.buffer.redo()
582 def goto(self, widget = None):
583 from goto import Goto
584 self.set_minibuffer(Goto())
586 def search(self, widget = None):
587 from search import Search
588 self.set_minibuffer(Search())
590 def search_replace(self, widget = None):
591 from search import Replace
592 Replace(self).show()
594 def set_mini_label(self, label):
595 self.mini_label.set_text(label)
597 def set_minibuffer(self, minibuffer):
598 assert minibuffer is None or isinstance(minibuffer, Minibuffer)
600 self.minibuffer = None
602 if minibuffer:
603 self.mini_entry.set_text('')
604 self.minibuffer = minibuffer
605 minibuffer.setup(self)
606 self.mini_entry.grab_focus()
607 self.mini_hbox.show_all()
608 else:
609 self.mini_hbox.hide()
610 self.text.grab_focus()
612 def mini_key_press(self, entry, kev):
613 if kev.keyval == g.keysyms.Escape:
614 self.set_minibuffer(None)
615 return 1
616 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
617 self.minibuffer.activate()
618 return 1
620 return self.minibuffer.key_press(kev)
622 def mini_changed(self, entry):
623 if not self.minibuffer:
624 return
625 self.minibuffer.changed()
627 def mini_show_info(self, *unused):
628 assert self.minibuffer
629 if self.info_box:
630 self.info_box.destroy()
631 self.info_box = g.MessageDialog(self, 0, g.MESSAGE_INFO, g.BUTTONS_OK,
632 self.minibuffer.info)
633 self.info_box.set_title(_('Minibuffer help'))
634 def destroy(box):
635 self.info_box = None
636 self.info_box.connect('destroy', destroy)
637 self.info_box.show()
638 self.info_box.connect('response', lambda w, r: w.destroy())
640 def process_selected(self, process):
641 """Calls process(line) on each line in the selection, or each line in the file
642 if there is no selection. If the result is not None, the text is replaced."""
643 self.buffer.begin_user_action()
644 try:
645 self._process_selected(process)
646 finally:
647 self.buffer.end_user_action()
649 def _process_selected(self, process):
650 if self.has_selection():
651 def get_end():
652 start, end = self.get_selection_range()
653 if start.compare(end) > 0:
654 return start
655 return end
656 start, end = self.get_selection_range()
657 if start.compare(end) > 0:
658 start = end
659 else:
660 def get_end():
661 return self.buffer.get_end_iter()
662 start = self.buffer.get_start_iter()
663 end = get_end()
665 while start.compare(end) <= 0:
666 line_end = start.copy()
667 line_end.forward_to_line_end()
668 if line_end.compare(end) >= 0:
669 line_end = end
670 line = self.buffer.get_text(start, line_end, False)
671 new = process(line)
672 if new is not None:
673 self.buffer.move_mark(self.mark_tmp, start)
674 self.buffer.insert(line_end, new)
675 start = self.buffer.get_iter_at_mark(self.mark_tmp)
676 line_end = start.copy()
677 line_end.forward_chars(len(line.decode('utf-8')))
678 self.buffer.delete(start, line_end)
680 start = self.buffer.get_iter_at_mark(self.mark_tmp)
681 end = get_end()
682 if not start.forward_line(): break
684 def set_word_wrap(self, value):
685 self._word_wrap = value
686 self.wrap_button.set_active(value)
687 if value:
688 self.text.set_wrap_mode(g.WRAP_WORD)
689 else:
690 self.text.set_wrap_mode(g.WRAP_NONE)
692 word_wrap = property(lambda self: self._word_wrap, set_word_wrap)
694 class SelectionSaver(Saveable):
695 def __init__(self, window):
696 self.window = window
697 window.set_marked()
699 def save_to_stream(self, stream):
700 s, e = self.window.get_marked_range()
701 stream.write(self.window.buffer.get_text(s, e, True))
703 def destroy(self):
704 # Called when savebox is remove. Get rid of the selection marker
705 self.window.clear_marked()