use gtksourceview 2.x
[rox-edit.git] / EditWindow.py
blobd50ce5a97b7e17e2f909d17edd2cfd3a58da55a1
1 import rox
2 from rox import g, filer, app_options, mime
3 import sys
4 from rox.loading import XDSLoader
5 from rox.options import Option
6 from rox import OptionsBox
7 from rox.saving import Saveable
8 import os
9 import diff
10 import codecs
12 # WARNING: This is a temporary hack, until we write a way choose between
13 # the two ways of doing toolbars or we abandon the old method entirely
14 import warnings
15 warnings.filterwarnings('ignore', category=DeprecationWarning,
16 module='EditWindow')
17 # End temporary hack
19 def optional_section(available):
20 """If requires is None, the section is enabled. Otherwise,
21 the section is shaded and the requires message is shown at
22 the top."""
23 if available:
24 def build_enabled_section(box, node, label):
25 return box.do_box(node, None, g.VBox(False, 0))
26 return build_enabled_section
27 else:
28 def build_disabled_section(box, node, label):
29 assert label is not None
30 box, = box.do_box(node, None, g.VBox(False, 0))
31 box.set_sensitive(False)
32 frame = g.Frame(label)
33 box.set_border_width(4)
34 frame.add(box)
35 return [frame]
36 return build_disabled_section
38 try:
39 import gtkspell
40 have_spell = True
41 except:
42 have_spell = False
44 to_utf8 = codecs.getencoder('utf-8')
46 from buffer import Buffer, have_sourceview
47 from rox.Menu import Menu, set_save_name, SubMenu, Separator, Action, ToggleItem
48 if have_sourceview: import gtksourceview2 as gtksourceview
50 OptionsBox.widget_registry['source-view-only'] = optional_section(have_sourceview)
52 default_font = Option('default_font', 'serif')
54 background_colour = Option('background', '#fff')
55 foreground_colour = Option('foreground', '#000')
57 auto_indent = Option('autoindent', '1')
58 word_wrap = Option('wordwrap', '1')
60 layout_left_margin = Option('layout_left_margin', 2)
61 layout_right_margin = Option('layout_right_margin', 4)
63 layout_before_para = Option('layout_before_para', 0)
64 layout_after_para = Option('layout_after_para', 0)
65 layout_inside_para = Option('layout_inside_para', 0)
66 layout_indent_para = Option('layout_indent_para', 2)
68 right_margin = Option('right_margin', 80)
69 show_margin = Option('show_margin', True)
70 smart_home_end = Option('smart_home_end', True)
71 show_line_numbers = Option('show_line_numbers', True)
72 show_line_markers = Option('show_line_markers', True)
73 tab_width = Option('tab_width', 4)
74 use_spaces_for_tabs = Option('use_spaces_for_tabs', False)
76 show_toolbar = Option('show_toolbar', 1)
78 set_save_name('Edit', site='rox.sourceforge.net')
80 edit_menu = [
81 Action(_('Cut'), 'cut', '<Ctrl>X', g.STOCK_CUT),
82 Action(_('Copy'), 'copy', '<Ctrl>C', g.STOCK_COPY),
83 Action(_('Paste'), 'paste', '<Ctrl>V', g.STOCK_PASTE),
84 Separator(),
85 Action(_('Undo'), 'undo', '<Ctrl>Z', g.STOCK_UNDO),
86 Action(_('Redo'), 'redo', '<Ctrl>Y', g.STOCK_REDO),
87 Separator(),
88 Action(_('Search...'), 'search', 'F4', g.STOCK_FIND),
89 Action(_('Search Again'), 'search_again', '<Shift>F4', g.STOCK_GO_FORWARD),
90 Action(_('Search and Replace....'), 'search_replace',
91 '<Ctrl>F4', g.STOCK_FIND_AND_REPLACE),
92 Action(_('Goto line...'), 'goto', 'F5', g.STOCK_JUMP_TO),
95 bookmark_menu = [
96 Separator(),
97 Action(_('Toggle Bookmark'), 'toggle_bookmark', '<Ctrl>F2'),
98 Action(_('Next Bookmark'), 'next_bookmark', 'F2'),
99 Action(_('Previous Bookmark'), 'prev_bookmark', '<Shift>F2'),
102 if have_sourceview:
103 edit_menu += bookmark_menu
105 menu = Menu('main', [
106 SubMenu(_('File'), [
107 Action(_('Save'), 'save', '<Ctrl>S', g.STOCK_SAVE),
108 Action(_('Save As...'), 'save_as', 'F3', g.STOCK_SAVE_AS),
109 Action(_('Open Parent'), 'up', '', g.STOCK_GO_UP),
110 Action(_('Show Changes'), 'diff', '', 'rox-diff'),
111 ToggleItem(_('Word Wrap'), 'word_wrap'),
112 Action(_('Close'), 'close', '', g.STOCK_CLOSE),
113 Separator(),
114 Action(_('New'), 'new', '', g.STOCK_NEW)]),
116 SubMenu(_('Edit'), edit_menu),
118 Action(_('Options'), 'show_options', '', g.STOCK_PROPERTIES),
119 Action(_('Help'), 'help', 'F1', g.STOCK_HELP),
122 known_codecs = (
123 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
124 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
125 "iso8859_13", "iso8859_14", "iso8859_15",
126 "ascii", "base64_codec", "charmap",
127 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
128 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
129 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
130 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
131 "cp869", "cp874", "cp875", "hex_codec",
132 "koi8_r",
133 "latin_1",
134 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
135 "mbcs", "quopri_codec", "raw_unicode_escape",
136 "rot_13",
137 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
138 "zlib_codec"
141 class Abort(Exception):
142 pass
144 class Minibuffer:
145 def setup(self):
146 """Called when the minibuffer is opened."""
148 def key_press(self, kev):
149 """A keypress event in the minibuffer text entry."""
151 def changed(self):
152 """The minibuffer text has changed."""
154 def activate(self):
155 """Return or Enter pressed."""
157 def close(self):
158 """Called when the minibuffer is closed.
159 Remove any widgets created in setup."""
161 info = 'Press Escape to close the minibuffer.'
163 class DiffLoader(XDSLoader):
164 def __init__(self, window):
165 XDSLoader.__init__(self, ['text/plain'])
166 self.window = window
168 def xds_load_from_file(self, path):
169 self.window.diff(path = path)
171 def xds_load_from_stream(self, name, type, stream):
172 tmp = diff.Tmp(suffix = '-' + (name or 'tmp'))
173 import shutil
174 shutil.copyfileobj(stream, tmp)
175 tmp.seek(0)
176 self.window.diff(path = tmp.name)
178 class DndToolbar(g.Toolbar, XDSLoader):
179 def __init__(self):
180 g.Toolbar.__init__(self)
181 XDSLoader.__init__(self, ['text/plain'])
183 def xds_load_from_path(self, path):
184 EditWindow(path)
186 def xds_load_from_stream(self, name, type, stream):
187 EditWindow(contents = stream.read())
189 class EditWindow(rox.Window, XDSLoader, Saveable):
190 _word_wrap = False
191 wrap_button = None
192 minibuffer = None
193 search_minibuffer = None # (persists for search_again)
195 def __init__(self, filename = None, show = True, line_number = None, contents = None):
196 rox.Window.__init__(self)
197 XDSLoader.__init__(self, ['text/plain', 'UTF8_STRING'])
199 self.savebox = None
200 self.info_box = None
201 self.language = None
203 app_options.add_notify(self.update_styles)
205 if filename:
206 import os.path
207 if not os.path.exists(filename):
208 try:
209 filename2, line_number = filename.split(':')
210 line_number = long(line_number)
211 except ValueError:
212 # Either there was no ':', or it wasn't followed by a number
213 pass
214 else:
215 filename = filename2
216 self.uri = os.path.abspath(filename)
217 self.mime_type = mime.get_type(self.uri, 1)
218 else:
219 self.uri = None
220 self.mime_type = mime.lookup('text', 'plain')
222 self.buffer = Buffer()
224 if have_sourceview:
225 self.text = gtksourceview.View(self.buffer)
226 pixbuf = g.gdk.pixbuf_new_from_file(rox.app_dir+"/images/marker.png")
227 self.text.set_mark_category_pixbuf("bookmark", pixbuf)
228 if self.mime_type:
229 self.buffer.set_type(self.mime_type)
230 else:
231 self.text = g.TextView()
232 self.text.set_buffer(self.buffer)
234 self.text.set_size_request(10, 10)
235 self.xds_proxy_for(self.text)
237 self.insert_mark = self.buffer.get_mark('insert')
238 self.selection_bound_mark = self.buffer.get_mark('selection_bound')
239 start = self.buffer.get_start_iter()
240 self.mark_start = self.buffer.create_mark('mark_start', start, True)
241 self.mark_end = self.buffer.create_mark('mark_end', start, False)
242 self.mark_tmp = self.buffer.create_mark('mark_tmp', start, False)
243 tag = self.buffer.create_tag('marked')
244 tag.set_property('background', 'green')
245 self.marked = 0
247 # When searching, this is where the cursor was when the minibuffer
248 # was opened.
249 start = self.buffer.get_start_iter()
250 self.search_base = self.buffer.create_mark('search_base', start, True)
252 vbox = g.VBox(False)
253 self.add(vbox)
255 tools = DndToolbar()
256 tools.set_style(g.TOOLBAR_ICONS)
257 vbox.pack_start(tools, False, True, 0)
259 self.status_label = g.Label('')
260 tools.append_widget(self.status_label, None, None)
261 tools.insert_stock(g.STOCK_HELP, _('Help'), None, self.help, None, 0)
262 diff = tools.insert_stock('rox-diff', _('Show changes from saved copy.\n'
263 'Or, drop a backup file onto this button to see changes from that.'),
264 None, self.diff, None, 0)
265 DiffLoader(self).xds_proxy_for(diff)
267 if have_spell:
268 self.spell = None
269 image_spell = g.Image()
270 image_spell.set_from_stock(g.STOCK_SPELL_CHECK, tools.get_icon_size())
271 self.spell_button = tools.insert_element(g.TOOLBAR_CHILD_TOGGLEBUTTON,
272 None, _("Check Spelling"), _("Check Spelling"), None,
273 image_spell, self.toggle_spell, None, 0)
275 image_wrap = g.Image()
276 image_wrap.set_from_file(rox.app_dir + '/images/rox-word-wrap.png')
277 self.wrap_button = tools.insert_element(g.TOOLBAR_CHILD_TOGGLEBUTTON,
278 None, _("Word Wrap"), _("Word Wrap"), None, image_wrap,
279 lambda button: self.set_word_wrap(button.get_active()),
280 None, 0)
281 tools.insert_stock(g.STOCK_REDO, _('Redo'), None, self.redo, None, 0)
282 tools.insert_stock(g.STOCK_UNDO, _('Undo'), None, self.undo, None, 0)
283 tools.insert_stock(g.STOCK_FIND_AND_REPLACE, _('Replace'), None, self.search_replace, None, 0)
284 tools.insert_stock(g.STOCK_FIND, _('Search'), None, self.search, None, 0)
285 tools.insert_stock(g.STOCK_SAVE_AS, _('Save As'), None, self.save_as, None, 0)
286 self.save_button = tools.insert_stock(g.STOCK_SAVE, _('Save'), None, self.save, None, 0)
287 tools.insert_stock(g.STOCK_GO_UP, _('Up'), None, self.up, None, 0)
288 tools.insert_stock(g.STOCK_CLOSE, _('Close'), None, self.close, None, 0)
289 # Set minimum size to ignore the label
290 tools.set_size_request(tools.size_request()[0], -1)
292 self.tools = tools
294 swin = g.ScrolledWindow()
295 swin.set_policy(g.POLICY_AUTOMATIC, g.POLICY_AUTOMATIC)
296 vbox.pack_start(swin, True, True)
298 swin.add(self.text)
300 # Aim for a width of about 100 chars
301 layout = self.text.create_pango_layout("mmmmmiiiii")
302 default_width = layout.get_pixel_extents()[1][2] * 10
303 self.set_default_size(min(g.gdk.screen_width() * 2 / 3, default_width),
304 g.gdk.screen_height() / 2)
306 if show:
307 self.show_all()
308 self.update_styles()
310 self.update_title()
312 # Create the minibuffer
313 self.mini_hbox = g.HBox(False)
315 info = rox.ButtonMixed(g.STOCK_DIALOG_INFO, '')
316 info.set_relief(g.RELIEF_NONE)
317 info.unset_flags(g.CAN_FOCUS)
318 info.connect('clicked', self.mini_show_info)
320 close = rox.ButtonMixed(g.STOCK_STOP, '')
321 close.set_relief(g.RELIEF_NONE)
322 close.unset_flags(g.CAN_FOCUS)
323 close.connect('clicked', lambda e: self.set_minibuffer(None))
325 self.mini_hbox.pack_end(info, False, True, 0)
326 self.mini_hbox.pack_start(close, False, True, 0)
327 self.mini_label = g.Label('')
328 self.mini_hbox.pack_start(self.mini_label, False, True, 0)
329 self.mini_entry = g.Entry()
330 self.mini_hbox.pack_start(self.mini_entry, True, True, 0)
331 vbox.pack_start(self.mini_hbox, False, True)
332 self.mini_entry.connect('key-press-event', self.mini_key_press)
333 self.mini_entry.connect('changed', self.mini_changed)
335 self.connect('destroy', self.destroyed)
337 self.connect('delete-event', self.delete_event)
338 self.text.grab_focus()
339 self.text.connect('key-press-event', self.key_press)
341 # FIXME: why does this freeze Edit?
342 #if have_spell:
343 #if self.mime_type.media == 'text' and self.mime_type.subtype == 'plain':
344 #self.toggle_spell()
345 ##self.spell.set_language ("en_US")
347 def update_current_line(*unused):
348 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
349 bound = self.buffer.get_iter_at_mark(self.selection_bound_mark)
350 if cursor.compare(bound) == 0:
351 n_lines = self.buffer.get_line_count()
352 self.status_label.set_text(_('Line %s of %d') % (cursor.get_line() + 1, n_lines))
353 else:
354 n_lines = abs(cursor.get_line() - bound.get_line()) + 1
355 if n_lines == 1:
356 n_chars = abs(cursor.get_line_offset() - bound.get_line_offset())
357 if n_chars == 1:
358 bytes = to_utf8(self.buffer.get_text(cursor, bound, False))[0]
359 self.status_label.set_text(_('One character selected (%s)') %
360 ' '.join(map(lambda x: '0x%2x' % ord(x), bytes)))
361 else:
362 self.status_label.set_text(_('%d characters selected') % n_chars)
363 else:
364 self.status_label.set_text(_('%d lines selected') % n_lines)
365 self.buffer.connect('mark-set', update_current_line)
366 self.buffer.connect('changed', update_current_line)
369 # Loading might take a while, so get something on the screen
370 # now...
371 g.gdk.flush()
373 if filename:
374 try:
375 self.load_file(filename)
376 if filename != '-':
377 self.save_last_stat = os.stat(filename)
378 except Abort:
379 self.destroy()
380 raise
381 if contents:
382 self.insert_data(contents)
384 self.buffer.connect('modified-changed', self.update_title)
385 self.buffer.set_modified(False)
387 def button_press(text, event):
388 if event.button != 3:
389 return False
390 #self.text.emit('populate-popup', menu.menu)
391 menu.popup(self, event)
392 return True
393 self.text.connect('button-press-event', button_press)
394 self.text.connect('popup-menu', lambda text: menu.popup(self, None))
396 menu.attach(self, self)
397 self.buffer.place_cursor(self.buffer.get_start_iter())
398 self.buffer.start_undo_history()
400 if line_number:
401 iter = self.buffer.get_iter_at_line(int(line_number) - 1)
402 self.buffer.place_cursor(iter)
403 self.text.scroll_to_mark(self.insert_mark, 0.05, False)
405 def key_press(self, text, kev):
406 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
407 return self.auto_indent()
408 elif kev.keyval == g.keysyms.Tab or kev.keyval == g.keysyms.KP_Tab:
409 return self.indent_block()
410 elif kev.keyval == g.keysyms.ISO_Left_Tab:
411 return self.unindent_block()
412 elif kev.keyval == g.keysyms.Escape:
413 self.set_minibuffer(None)
414 return True
415 return False
417 def auto_indent(self):
418 if not auto_indent.int_value:
419 return False
421 start = self.buffer.get_iter_at_mark(self.insert_mark)
422 end = start.copy()
423 start.set_line_offset(0)
424 end.forward_to_line_end()
425 line = self.buffer.get_text(start, end, False)
426 indent = ''
428 if self.mime_type.subtype == 'x-python':
429 try:
430 l = line.split('\n')[0]
431 except:
432 l = line
433 if l.endswith(':') and not l.startswith('#'):
434 if use_spaces_for_tabs.int_value:
435 indent += ' ' * tab_width.int_value
436 else:
437 indent += '\t'
438 elif have_sourceview:
439 return False
441 for x in line:
442 if x in ' \t':
443 indent += x
444 else:
445 break
447 self.buffer.begin_user_action()
448 self.buffer.insert_at_cursor('\n' + indent)
449 self.buffer.end_user_action()
450 return True
452 def indent_block(self):
453 try:
454 (start, end) = self.buffer.get_selection_bounds()
455 start_line = start.get_line()
456 end_line = end.get_line()
457 self.buffer.begin_user_action()
458 for i in range(start_line, end_line+1):
459 iter = self.buffer.get_iter_at_line(i)
460 self.buffer.insert(iter, '\t')
461 self.buffer.end_user_action()
462 return True
463 except:
464 return False
466 def unindent_block(self):
467 try:
468 (start, end) = self.buffer.get_selection_bounds()
469 start_line = start.get_line()
470 end_line = end.get_line()
471 self.buffer.begin_user_action()
472 for i in range(start_line, end_line+1):
473 iter = self.buffer.get_iter_at_line(i)
474 if iter.get_char() == '\t':
475 next_char = iter.copy()
476 next_char.forward_char()
477 self.buffer.delete(iter, next_char)
478 self.buffer.end_user_action()
479 return True
480 except:
481 return False
483 def destroyed(self, widget):
484 app_options.remove_notify(self.update_styles)
486 def update_styles(self):
487 try:
488 import pango
489 font = pango.FontDescription(default_font.value)
490 bg = g.gdk.color_parse(background_colour.value)
491 fg = g.gdk.color_parse(foreground_colour.value)
493 self.text.set_left_margin(layout_left_margin.int_value)
494 self.text.set_right_margin(layout_right_margin.int_value)
496 self.text.set_pixels_above_lines(layout_before_para.int_value)
497 self.text.set_pixels_below_lines(layout_after_para.int_value)
498 self.text.set_pixels_inside_wrap(layout_inside_para.int_value)
499 self.text.set_indent(layout_indent_para.int_value)
501 self.word_wrap = bool(word_wrap.int_value)
503 if show_toolbar.int_value:
504 self.tools.show()
505 else:
506 self.tools.hide()
507 except:
508 rox.report_exception()
509 else:
510 self.text.modify_font(font)
511 self.text.modify_base(g.STATE_NORMAL, bg)
512 self.text.modify_text(g.STATE_NORMAL, fg)
514 if have_sourceview:
515 self.text.set_show_line_numbers(show_line_numbers.int_value)
516 self.text.set_show_line_marks(show_line_markers.int_value)
517 self.text.set_auto_indent(auto_indent.int_value)
518 self.text.set_tab_width(tab_width.int_value)
519 self.text.set_insert_spaces_instead_of_tabs(use_spaces_for_tabs.int_value)
520 self.text.set_right_margin(right_margin.int_value)
521 self.text.set_show_right_margin(show_margin.int_value)
522 self.text.set_smart_home_end(smart_home_end.int_value)
523 if self.buffer.language == 'Python':
524 self.text.set_auto_indent(False)
526 def cut(self): self.text.emit('cut_clipboard')
527 def copy(self): self.text.emit('copy_clipboard')
528 def paste(self): self.text.emit('paste_clipboard')
530 def delete_event(self, window, event):
531 if self.buffer.get_modified():
532 self.save_as(discard = 1)
533 return 1
534 return 0
536 def update_title(self, *unused):
537 title = self.uri or '<'+_('Untitled')+'>'
538 if self.buffer.get_modified():
539 title = title + " *"
540 self.save_button.set_sensitive(True)
541 else:
542 self.save_button.set_sensitive(False)
543 self.set_title(title)
545 def xds_load_from_stream(self, name, t, stream):
546 if t == 'UTF8_STRING':
547 return # Gtk will handle it
548 try:
549 dnd_mark = self.buffer.get_mark('gtk_drag_target')
550 if dnd_mark:
551 dnd_pos = self.buffer.get_iter_at_mark(dnd_mark)
552 self.buffer.move_mark(self.insert_mark, dnd_pos)
553 self.insert_data(stream.read())
554 except Abort:
555 pass
557 def get_encoding(self, message):
558 "Returns (encoding, errors), or raises Abort to cancel."
559 box = g.MessageDialog(self, 0, g.MESSAGE_QUESTION, g.BUTTONS_CANCEL, message)
560 box.set_has_separator(False)
562 frame = g.Frame()
563 box.vbox.pack_start(frame, True, True)
564 frame.set_border_width(6)
566 hbox = g.HBox(False, 4)
567 hbox.set_border_width(6)
569 hbox.pack_start(g.Label(_('Encoding:')), False, True, 0)
570 combo = g.Combo()
571 combo.disable_activate()
572 combo.entry.connect('activate', lambda w: box.activate_default())
573 combo.set_popdown_strings(known_codecs)
574 hbox.pack_start(combo, True, True, 0)
575 ignore_errors = g.CheckButton(_('Ignore errors'))
576 hbox.pack_start(ignore_errors, False, True)
578 frame.add(hbox)
580 box.vbox.show_all()
581 box.add_button(g.STOCK_CONVERT, g.RESPONSE_YES)
582 box.set_default_response(g.RESPONSE_YES)
584 while 1:
585 combo.entry.grab_focus()
587 resp = box.run()
588 if resp != int(g.RESPONSE_YES):
589 box.destroy()
590 raise Abort
592 if ignore_errors.get_active():
593 errors = 'replace'
594 else:
595 errors = 'strict'
596 encoding = combo.entry.get_text()
597 try:
598 codecs.getdecoder(encoding)
599 break
600 except:
601 rox.alert(_("Unknown encoding '%s'") % encoding)
603 box.destroy()
605 return encoding, errors
607 def insert_data(self, data):
608 import codecs
609 errors = 'strict'
610 encoding = 'utf-8'
611 while 1:
612 decoder = codecs.getdecoder(encoding)
613 try:
614 data = decoder(data, errors)[0]
615 if errors == 'strict':
616 assert '\0' not in data
617 else:
618 if '\0' in data:
619 data = data.replace('\0', '\\0')
620 break
621 except:
622 pass
624 encoding, errors = self.get_encoding(
625 _("Data is not valid %s. Please select the file's encoding. "
626 "Turn on 'ignore errors' to try and load it anyway.")
627 % encoding)
629 self.buffer.begin_user_action()
630 self.buffer.insert_at_cursor(data)
631 self.buffer.end_user_action()
632 return 1
634 def load_file(self, path):
635 try:
636 if path == '-':
637 file = sys.stdin
638 else:
639 file = open(path, 'r')
640 contents = file.read()
641 if path != '-':
642 file.close()
644 self.buffer.begin_not_undoable_action()
645 self.insert_data(contents)
646 self.buffer.end_not_undoable_action()
647 except Abort:
648 raise
649 except:
650 rox.report_exception()
651 raise Abort
653 def close(self, button = None):
654 if self.buffer.get_modified():
655 self.save_as(discard = 1)
656 else:
657 self.destroy()
659 def discard(self):
660 self.destroy()
662 def up(self, button = None):
663 if self.uri:
664 filer.show_file(self.uri)
665 else:
666 rox.alert(_('File is not saved to disk yet'))
669 def toggle_spell(self, button = None):
670 if self.spell:
671 self.spell.detach()
672 self.spell = None
673 self.spell_button.set_active(False)
674 elif not self.spell_button.get_active():
675 # Probably a failed attempt to turn it on
676 pass
677 else:
678 try:
679 self.spell = gtkspell.Spell(self.text)
680 self.spell_button.set_active(True)
681 except Exception, ex:
682 self.spell = None
683 self.spell_button.set_active(False)
684 rox.report_exception()
686 #self.spell_button.set_active(self.spell != None)
688 def diff(self, button = None, path = None):
689 path = path or self.uri
690 if not path:
691 rox.alert(_('This file has never been saved; nothing to compare it to!\n'
692 'Note: you can drop a file onto the toolbar button to see '
693 'the changes from that file.'))
694 return
695 diff.show_diff(path, self.save_to_stream)
697 def has_selection(self):
698 s, e = self.get_selection_range()
699 return not e.equal(s)
701 def get_marked_range(self):
702 s = self.buffer.get_iter_at_mark(self.mark_start)
703 e = self.buffer.get_iter_at_mark(self.mark_end)
704 if s.compare(e) > 0:
705 return e, s
706 return s, e
708 def get_selection_range(self):
709 s = self.buffer.get_iter_at_mark(self.insert_mark)
710 e = self.buffer.get_iter_at_mark(self.selection_bound_mark)
711 if s.compare(e) > 0:
712 return e, s
713 return s, e
715 def save(self, widget = None):
716 if self.uri:
717 self.save_to_file(self.uri)
718 self.buffer.set_modified(False)
719 else:
720 self.save_as(discard=0)
722 def save_as(self, widget = None, discard = 0):
723 from rox.saving import SaveBox
725 if self.savebox:
726 self.savebox.destroy()
728 try:
729 self.mime_type = mime.get_type(self.uri, 1)
730 except:
731 self.mime_type = mime.lookup('text', 'plain')
733 mime_text = self.mime_type.media + '/' + self.mime_type.subtype
735 if self.has_selection() and not discard:
736 saver = SelectionSaver(self)
737 self.savebox = SaveBox(saver, 'Selection', mime_text)
738 self.savebox.connect('destroy', lambda w: saver.destroy())
739 else:
740 uri = self.uri or _('TextFile')
741 self.savebox = SaveBox(self, uri, mime_text, discard)
742 self.savebox.set_transient_for(self)
743 self.savebox.show()
745 def help(self, button = None):
746 filer.open_dir(os.path.join(rox.app_dir, 'Help'))
748 def save_to_stream(self, stream):
749 s = self.buffer.get_start_iter()
750 e = self.buffer.get_end_iter()
751 stream.write(self.buffer.get_text(s, e, True))
753 def set_uri(self, uri):
754 self.uri = uri
755 self.buffer.set_modified(False)
756 self.update_title()
758 def new(self):
759 EditWindow()
761 def change_font(self):
762 style = self.text.get_style().copy()
763 style.font = load_font(options.get('edit_font'))
764 self.text.set_style(style)
766 def show_options(self):
767 rox.edit_options()
769 def set_marked(self, start = None, end = None):
770 "Set the marked region (from the selection if no region is given)."
771 self.clear_marked()
772 assert not self.marked
774 buffer = self.buffer
775 if start:
776 assert end
777 else:
778 assert not end
779 start, end = self.get_selection_range()
780 buffer.move_mark(self.mark_start, start)
781 buffer.move_mark(self.mark_end, end)
782 buffer.apply_tag_by_name('marked',
783 buffer.get_iter_at_mark(self.mark_start),
784 buffer.get_iter_at_mark(self.mark_end))
785 self.marked = 1
787 def clear_marked(self):
788 if not self.marked:
789 return
790 self.marked = 0
791 buffer = self.buffer
792 buffer.remove_tag_by_name('marked',
793 buffer.get_iter_at_mark(self.mark_start),
794 buffer.get_iter_at_mark(self.mark_end))
796 def undo(self, widget = None):
797 self.buffer.undo()
798 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
799 self.text.scroll_to_iter(cursor, 0.05, False)
801 def redo(self, widget = None):
802 self.buffer.redo()
803 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
804 self.text.scroll_to_iter(cursor, 0.05, False)
806 def goto(self, widget = None):
807 from goto import Goto
808 self.set_minibuffer(Goto())
810 def search(self, widget = None):
811 if self.search_minibuffer is None:
812 from search import Search
813 self.search_minibuffer = Search()
814 self.set_minibuffer(self.search_minibuffer)
816 def search_again(self, widget = None):
817 if self.search_minibuffer and self.search_minibuffer is self.minibuffer:
818 self.minibuffer.activate() # Search again with same text
820 if self.minibuffer is None:
821 # Search mini-buffer not yet open
822 self.search()
823 self.minibuffer.restore_previous_search()
824 self.minibuffer.search_again()
826 def search_replace(self, widget = None):
827 from search import Replace
828 Replace(self).show()
830 def toggle_bookmark(self):
831 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
832 name = str(cursor.get_line())
833 marker = self.buffer.get_marker(name)
834 if marker:
835 self.buffer.delete_marker(marker);
836 else:
837 marker = self.buffer.create_marker(name, "bookmark", cursor);
839 def next_bookmark(self):
840 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
841 cursor.forward_char()
842 marker = self.buffer.get_next_marker(cursor)
843 if marker:
844 self.buffer.get_iter_at_marker (cursor, marker)
845 self.buffer.place_cursor(cursor)
846 self.text.scroll_to_iter(cursor, 0.05, False)
848 def prev_bookmark(self):
849 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
850 cursor.backward_char()
851 marker = self.buffer.get_prev_marker(cursor)
852 if marker:
853 self.buffer.get_iter_at_marker (cursor, marker)
854 self.buffer.place_cursor(cursor)
855 self.text.scroll_to_iter(cursor, 0.05, False)
857 def set_mini_label(self, label):
858 self.mini_label.set_text(label)
860 def set_minibuffer(self, minibuffer):
861 assert minibuffer is None or isinstance(minibuffer, Minibuffer)
863 if self.minibuffer:
864 self.minibuffer.close()
866 self.minibuffer = None
868 if minibuffer:
869 self.mini_entry.set_text('')
870 self.minibuffer = minibuffer
871 minibuffer.setup(self)
872 self.mini_entry.grab_focus()
873 self.mini_hbox.show_all()
874 else:
875 self.mini_hbox.hide()
876 self.text.grab_focus()
878 def mini_key_press(self, entry, kev):
879 if kev.keyval == g.keysyms.Escape:
880 self.set_minibuffer(None)
881 return 1
882 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
883 self.minibuffer.activate()
884 return 1
886 return self.minibuffer.key_press(kev)
888 def mini_changed(self, entry):
889 if not self.minibuffer:
890 return
891 self.minibuffer.changed()
893 def mini_show_info(self, *unused):
894 assert self.minibuffer
895 if self.info_box:
896 self.info_box.destroy()
897 self.info_box = g.MessageDialog(self, 0, g.MESSAGE_INFO, g.BUTTONS_OK,
898 self.minibuffer.info)
899 self.info_box.set_title(_('Minibuffer help'))
900 def destroy(box):
901 self.info_box = None
902 self.info_box.connect('destroy', destroy)
903 self.info_box.show()
904 self.info_box.connect('response', lambda w, r: w.destroy())
906 def process_selected(self, process):
907 """Calls process(line) on each line in the selection, or each line in the file
908 if there is no selection. If the result is not None, the text is replaced."""
909 self.buffer.begin_user_action()
910 try:
911 self._process_selected(process)
912 finally:
913 self.buffer.end_user_action()
915 def _process_selected(self, process):
916 if self.has_selection():
917 def get_end():
918 start, end = self.get_selection_range()
919 if start.compare(end) > 0:
920 return start
921 return end
922 start, end = self.get_selection_range()
923 if start.compare(end) > 0:
924 start = end
925 else:
926 def get_end():
927 return self.buffer.get_end_iter()
928 start = self.buffer.get_start_iter()
929 end = get_end()
931 while start.compare(end) <= 0:
932 line_end = start.copy()
933 line_end.forward_to_line_end()
934 if line_end.compare(end) >= 0:
935 line_end = end
936 line = self.buffer.get_text(start, line_end, False)
937 new = process(line)
938 if new is not None:
939 self.buffer.move_mark(self.mark_tmp, start)
940 self.buffer.insert(line_end, new)
941 start = self.buffer.get_iter_at_mark(self.mark_tmp)
942 line_end = start.copy()
943 line_end.forward_chars(len(line.decode('utf-8')))
944 self.buffer.delete(start, line_end)
946 start = self.buffer.get_iter_at_mark(self.mark_tmp)
947 end = get_end()
948 if not start.forward_line(): break
950 def set_word_wrap(self, value):
951 self._word_wrap = value
952 self.wrap_button.set_active(value)
953 if value:
954 self.text.set_wrap_mode(g.WRAP_WORD)
955 else:
956 self.text.set_wrap_mode(g.WRAP_NONE)
958 word_wrap = property(lambda self: self._word_wrap, set_word_wrap)
960 class SelectionSaver(Saveable):
961 def __init__(self, window):
962 self.window = window
963 window.set_marked()
965 def save_to_stream(self, stream):
966 s, e = self.window.get_marked_range()
967 stream.write(self.window.buffer.get_text(s, e, True))
969 def destroy(self):
970 # Called when savebox is remove. Get rid of the selection marker
971 self.window.clear_marked()