If turning on spell checking doesn't work, report the error.
[rox-edit.git] / EditWindow.py
blob5a934c43eef39a023cb8dc075d4ee2ffdcdac714
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, gtksourceview
47 from rox.Menu import Menu, set_save_name, SubMenu, Separator, Action, ToggleItem
49 OptionsBox.widget_registry['source-view-only'] = optional_section(have_sourceview)
51 default_font = Option('default_font', 'serif')
53 background_colour = Option('background', '#fff')
54 foreground_colour = Option('foreground', '#000')
56 auto_indent = Option('autoindent', '1')
57 word_wrap = Option('wordwrap', '1')
59 layout_left_margin = Option('layout_left_margin', 2)
60 layout_right_margin = Option('layout_right_margin', 4)
62 layout_before_para = Option('layout_before_para', 0)
63 layout_after_para = Option('layout_after_para', 0)
64 layout_inside_para = Option('layout_inside_para', 0)
65 layout_indent_para = Option('layout_indent_para', 2)
67 right_margin = Option('right_margin', 80)
68 show_margin = Option('show_margin', True)
69 smart_home_end = Option('smart_home_end', True)
70 show_line_numbers = Option('show_line_numbers', True)
71 show_line_markers = Option('show_line_markers', True)
72 tab_width = Option('tab_width', 4)
73 use_spaces_for_tabs = Option('use_spaces_for_tabs', False)
75 show_toolbar = Option('show_toolbar', 1)
77 set_save_name('Edit', site='rox.sourceforge.net')
79 edit_menu = [
80 Action(_('Cut'), 'cut', '<Ctrl>X', g.STOCK_CUT),
81 Action(_('Copy'), 'copy', '<Ctrl>C', g.STOCK_COPY),
82 Action(_('Paste'), 'paste', '<Ctrl>V', g.STOCK_PASTE),
83 Separator(),
84 Action(_('Undo'), 'undo', '<Ctrl>Z', g.STOCK_UNDO),
85 Action(_('Redo'), 'redo', '<Ctrl>Y', g.STOCK_REDO),
86 Separator(),
87 Action(_('Search...'), 'search', 'F4', g.STOCK_FIND),
88 Action(_('Search Again'), 'search_again', '<Shift>F4', g.STOCK_GO_FORWARD),
89 Action(_('Search and Replace....'), 'search_replace',
90 '<Ctrl>F4', g.STOCK_FIND_AND_REPLACE),
91 Action(_('Goto line...'), 'goto', 'F5', g.STOCK_JUMP_TO),
94 bookmark_menu = [
95 Separator(),
96 Action(_('Toggle Bookmark'), 'toggle_bookmark', '<Ctrl>F2'),
97 Action(_('Next Bookmark'), 'next_bookmark', 'F2'),
98 Action(_('Previous Bookmark'), 'prev_bookmark', '<Shift>F2'),
101 if have_sourceview:
102 edit_menu += bookmark_menu
104 menu = Menu('main', [
105 SubMenu(_('File'), [
106 Action(_('Save'), 'save', '<Ctrl>S', g.STOCK_SAVE),
107 Action(_('Save As...'), 'save_as', 'F3', g.STOCK_SAVE_AS),
108 Action(_('Open Parent'), 'up', '', g.STOCK_GO_UP),
109 Action(_('Show Changes'), 'diff', '', 'rox-diff'),
110 ToggleItem(_('Word Wrap'), 'word_wrap'),
111 Action(_('Close'), 'close', '', g.STOCK_CLOSE),
112 Separator(),
113 Action(_('New'), 'new', '', g.STOCK_NEW)]),
115 SubMenu(_('Edit'), edit_menu),
117 Action(_('Options'), 'show_options', '', g.STOCK_PROPERTIES),
118 Action(_('Help'), 'help', 'F1', g.STOCK_HELP),
121 known_codecs = (
122 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
123 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
124 "iso8859_13", "iso8859_14", "iso8859_15",
125 "ascii", "base64_codec", "charmap",
126 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
127 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
128 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
129 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
130 "cp869", "cp874", "cp875", "hex_codec",
131 "koi8_r",
132 "latin_1",
133 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
134 "mbcs", "quopri_codec", "raw_unicode_escape",
135 "rot_13",
136 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
137 "zlib_codec"
140 class Abort(Exception):
141 pass
143 class Minibuffer:
144 def setup(self):
145 """Called when the minibuffer is opened."""
147 def key_press(self, kev):
148 """A keypress event in the minibuffer text entry."""
150 def changed(self):
151 """The minibuffer text has changed."""
153 def activate(self):
154 """Return or Enter pressed."""
156 def close(self):
157 """Called when the minibuffer is closed.
158 Remove any widgets created in setup."""
160 info = 'Press Escape to close the minibuffer.'
162 class DiffLoader(XDSLoader):
163 def __init__(self, window):
164 XDSLoader.__init__(self, ['text/plain'])
165 self.window = window
167 def xds_load_from_file(self, path):
168 self.window.diff(path = path)
170 def xds_load_from_stream(self, name, type, stream):
171 tmp = diff.Tmp(suffix = '-' + (name or 'tmp'))
172 import shutil
173 shutil.copyfileobj(stream, tmp)
174 tmp.seek(0)
175 self.window.diff(path = tmp.name)
177 class DndToolbar(g.Toolbar, XDSLoader):
178 def __init__(self):
179 g.Toolbar.__init__(self)
180 XDSLoader.__init__(self, ['text/plain'])
182 def xds_load_from_path(self, path):
183 EditWindow(path)
185 def xds_load_from_stream(self, name, type, stream):
186 EditWindow(contents = stream.read())
188 class EditWindow(rox.Window, XDSLoader, Saveable):
189 _word_wrap = False
190 wrap_button = None
191 minibuffer = None
192 search_minibuffer = None # (persists for search_again)
194 def __init__(self, filename = None, show = True, line_number = None, contents = None):
195 rox.Window.__init__(self)
196 XDSLoader.__init__(self, ['text/plain', 'UTF8_STRING'])
198 self.savebox = None
199 self.info_box = None
200 self.language = None
202 app_options.add_notify(self.update_styles)
204 if filename:
205 import os.path
206 if not os.path.exists(filename):
207 try:
208 filename2, line_number = filename.split(':')
209 line_number = long(line_number)
210 except ValueError:
211 # Either there was no ':', or it wasn't followed by a number
212 pass
213 else:
214 filename = filename2
215 self.uri = os.path.abspath(filename)
216 self.mime_type = mime.get_type(self.uri, 1)
217 else:
218 self.uri = None
219 self.mime_type = mime.lookup('text', 'plain')
221 self.buffer = Buffer()
223 if have_sourceview:
224 self.text = gtksourceview.SourceView(self.buffer)
225 pixbuf = g.gdk.pixbuf_new_from_file(rox.app_dir+"/images/marker.png")
226 self.text.set_marker_pixbuf("bookmark", pixbuf)
227 if self.mime_type:
228 self.buffer.set_type(self.mime_type)
229 else:
230 self.text = g.TextView()
231 self.text.set_buffer(self.buffer)
233 self.text.set_size_request(10, 10)
234 self.xds_proxy_for(self.text)
236 self.insert_mark = self.buffer.get_mark('insert')
237 self.selection_bound_mark = self.buffer.get_mark('selection_bound')
238 start = self.buffer.get_start_iter()
239 self.mark_start = self.buffer.create_mark('mark_start', start, True)
240 self.mark_end = self.buffer.create_mark('mark_end', start, False)
241 self.mark_tmp = self.buffer.create_mark('mark_tmp', start, False)
242 tag = self.buffer.create_tag('marked')
243 tag.set_property('background', 'green')
244 self.marked = 0
246 # When searching, this is where the cursor was when the minibuffer
247 # was opened.
248 start = self.buffer.get_start_iter()
249 self.search_base = self.buffer.create_mark('search_base', start, True)
251 vbox = g.VBox(False)
252 self.add(vbox)
254 tools = DndToolbar()
255 tools.set_style(g.TOOLBAR_ICONS)
256 vbox.pack_start(tools, False, True, 0)
258 self.status_label = g.Label('')
259 tools.append_widget(self.status_label, None, None)
260 tools.insert_stock(g.STOCK_HELP, _('Help'), None, self.help, None, 0)
261 diff = tools.insert_stock('rox-diff', _('Show changes from saved copy.\n'
262 'Or, drop a backup file onto this button to see changes from that.'),
263 None, self.diff, None, 0)
264 DiffLoader(self).xds_proxy_for(diff)
266 if have_spell:
267 self.spell = None
268 image_spell = g.Image()
269 image_spell.set_from_stock(g.STOCK_SPELL_CHECK, tools.get_icon_size())
270 self.spell_button = tools.insert_element(g.TOOLBAR_CHILD_TOGGLEBUTTON,
271 None, _("Check Spelling"), _("Check Spelling"), None,
272 image_spell, self.toggle_spell, None, 0)
274 image_wrap = g.Image()
275 image_wrap.set_from_file(rox.app_dir + '/images/rox-word-wrap.png')
276 self.wrap_button = tools.insert_element(g.TOOLBAR_CHILD_TOGGLEBUTTON,
277 None, _("Word Wrap"), _("Word Wrap"), None, image_wrap,
278 lambda button: self.set_word_wrap(button.get_active()),
279 None, 0)
280 tools.insert_stock(g.STOCK_REDO, _('Redo'), None, self.redo, None, 0)
281 tools.insert_stock(g.STOCK_UNDO, _('Undo'), None, self.undo, None, 0)
282 tools.insert_stock(g.STOCK_FIND_AND_REPLACE, _('Replace'), None, self.search_replace, None, 0)
283 tools.insert_stock(g.STOCK_FIND, _('Search'), None, self.search, None, 0)
284 tools.insert_stock(g.STOCK_SAVE_AS, _('Save As'), None, self.save_as, None, 0)
285 self.save_button = tools.insert_stock(g.STOCK_SAVE, _('Save'), None, self.save, None, 0)
286 tools.insert_stock(g.STOCK_GO_UP, _('Up'), None, self.up, None, 0)
287 tools.insert_stock(g.STOCK_CLOSE, _('Close'), None, self.close, None, 0)
288 # Set minimum size to ignore the label
289 tools.set_size_request(tools.size_request()[0], -1)
291 self.tools = tools
293 swin = g.ScrolledWindow()
294 swin.set_policy(g.POLICY_AUTOMATIC, g.POLICY_AUTOMATIC)
295 vbox.pack_start(swin, True, True)
297 swin.add(self.text)
299 # Aim for a width of about 100 chars
300 layout = self.text.create_pango_layout("mmmmmiiiii")
301 default_width = layout.get_pixel_extents()[1][2] * 10
302 self.set_default_size(min(g.gdk.screen_width() * 2 / 3, default_width),
303 g.gdk.screen_height() / 2)
305 if show:
306 self.show_all()
307 self.update_styles()
309 self.update_title()
311 # Create the minibuffer
312 self.mini_hbox = g.HBox(False)
314 info = rox.ButtonMixed(g.STOCK_DIALOG_INFO, '')
315 info.set_relief(g.RELIEF_NONE)
316 info.unset_flags(g.CAN_FOCUS)
317 info.connect('clicked', self.mini_show_info)
319 close = rox.ButtonMixed(g.STOCK_STOP, '')
320 close.set_relief(g.RELIEF_NONE)
321 close.unset_flags(g.CAN_FOCUS)
322 close.connect('clicked', lambda e: self.set_minibuffer(None))
324 self.mini_hbox.pack_end(info, False, True, 0)
325 self.mini_hbox.pack_start(close, False, True, 0)
326 self.mini_label = g.Label('')
327 self.mini_hbox.pack_start(self.mini_label, False, True, 0)
328 self.mini_entry = g.Entry()
329 self.mini_hbox.pack_start(self.mini_entry, True, True, 0)
330 vbox.pack_start(self.mini_hbox, False, True)
331 self.mini_entry.connect('key-press-event', self.mini_key_press)
332 self.mini_entry.connect('changed', self.mini_changed)
334 self.connect('destroy', self.destroyed)
336 self.connect('delete-event', self.delete_event)
337 self.text.grab_focus()
338 self.text.connect('key-press-event', self.key_press)
340 # FIXME: why does this freeze Edit?
341 #if have_spell:
342 #if self.mime_type.media == 'text' and self.mime_type.subtype == 'plain':
343 #self.toggle_spell()
344 ##self.spell.set_language ("en_US")
346 def update_current_line(*unused):
347 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
348 bound = self.buffer.get_iter_at_mark(self.selection_bound_mark)
349 if cursor.compare(bound) == 0:
350 n_lines = self.buffer.get_line_count()
351 self.status_label.set_text(_('Line %s of %d') % (cursor.get_line() + 1, n_lines))
352 else:
353 n_lines = abs(cursor.get_line() - bound.get_line()) + 1
354 if n_lines == 1:
355 n_chars = abs(cursor.get_line_offset() - bound.get_line_offset())
356 if n_chars == 1:
357 bytes = to_utf8(self.buffer.get_text(cursor, bound, False))[0]
358 self.status_label.set_text(_('One character selected (%s)') %
359 ' '.join(map(lambda x: '0x%2x' % ord(x), bytes)))
360 else:
361 self.status_label.set_text(_('%d characters selected') % n_chars)
362 else:
363 self.status_label.set_text(_('%d lines selected') % n_lines)
364 self.buffer.connect('mark-set', update_current_line)
365 self.buffer.connect('changed', update_current_line)
368 # Loading might take a while, so get something on the screen
369 # now...
370 g.gdk.flush()
372 if filename:
373 try:
374 self.load_file(filename)
375 if filename != '-':
376 self.save_last_stat = os.stat(filename)
377 except Abort:
378 self.destroy()
379 raise
380 if contents:
381 self.insert_data(contents)
383 self.buffer.connect('modified-changed', self.update_title)
384 self.buffer.set_modified(False)
386 def button_press(text, event):
387 if event.button != 3:
388 return False
389 #self.text.emit('populate-popup', menu.menu)
390 menu.popup(self, event)
391 return True
392 self.text.connect('button-press-event', button_press)
393 self.text.connect('popup-menu', lambda text: menu.popup(self, None))
395 menu.attach(self, self)
396 self.buffer.place_cursor(self.buffer.get_start_iter())
397 self.buffer.start_undo_history()
399 if line_number:
400 iter = self.buffer.get_iter_at_line(int(line_number) - 1)
401 self.buffer.place_cursor(iter)
402 self.text.scroll_to_mark(self.insert_mark, 0.05, False)
404 def key_press(self, text, kev):
405 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
406 return self.auto_indent()
407 elif kev.keyval == g.keysyms.Tab or kev.keyval == g.keysyms.KP_Tab:
408 return self.indent_block()
409 elif kev.keyval == g.keysyms.ISO_Left_Tab:
410 return self.unindent_block()
411 elif kev.keyval == g.keysyms.Escape:
412 self.set_minibuffer(None)
413 return True
414 return False
416 def auto_indent(self):
417 if not auto_indent.int_value:
418 return False
420 start = self.buffer.get_iter_at_mark(self.insert_mark)
421 end = start.copy()
422 start.set_line_offset(0)
423 end.forward_to_line_end()
424 line = self.buffer.get_text(start, end, False)
425 indent = ''
427 if self.mime_type.subtype == 'x-python':
428 try:
429 l = line.split('\n')[0]
430 except:
431 l = line
432 if l.endswith(':') and not l.startswith('#'):
433 if use_spaces_for_tabs.int_value:
434 indent += ' ' * tab_width.int_value
435 else:
436 indent += '\t'
437 elif have_sourceview:
438 return False
440 for x in line:
441 if x in ' \t':
442 indent += x
443 else:
444 break
446 self.buffer.begin_user_action()
447 self.buffer.insert_at_cursor('\n' + indent)
448 self.buffer.end_user_action()
449 return True
451 def indent_block(self):
452 try:
453 (start, end) = self.buffer.get_selection_bounds()
454 start_line = start.get_line()
455 end_line = end.get_line()
456 self.buffer.begin_user_action()
457 for i in range(start_line, end_line+1):
458 iter = self.buffer.get_iter_at_line(i)
459 self.buffer.insert(iter, '\t')
460 self.buffer.end_user_action()
461 return True
462 except:
463 return False
465 def unindent_block(self):
466 try:
467 (start, end) = self.buffer.get_selection_bounds()
468 start_line = start.get_line()
469 end_line = end.get_line()
470 self.buffer.begin_user_action()
471 for i in range(start_line, end_line+1):
472 iter = self.buffer.get_iter_at_line(i)
473 if iter.get_char() == '\t':
474 next_char = iter.copy()
475 next_char.forward_char()
476 self.buffer.delete(iter, next_char)
477 self.buffer.end_user_action()
478 return True
479 except:
480 return False
482 def destroyed(self, widget):
483 app_options.remove_notify(self.update_styles)
485 def update_styles(self):
486 try:
487 import pango
488 font = pango.FontDescription(default_font.value)
489 bg = g.gdk.color_parse(background_colour.value)
490 fg = g.gdk.color_parse(foreground_colour.value)
492 self.text.set_left_margin(layout_left_margin.int_value)
493 self.text.set_right_margin(layout_right_margin.int_value)
495 self.text.set_pixels_above_lines(layout_before_para.int_value)
496 self.text.set_pixels_below_lines(layout_after_para.int_value)
497 self.text.set_pixels_inside_wrap(layout_inside_para.int_value)
498 self.text.set_indent(layout_indent_para.int_value)
500 self.word_wrap = bool(word_wrap.int_value)
502 if show_toolbar.int_value:
503 self.tools.show()
504 else:
505 self.tools.hide()
506 except:
507 rox.report_exception()
508 else:
509 self.text.modify_font(font)
510 self.text.modify_base(g.STATE_NORMAL, bg)
511 self.text.modify_text(g.STATE_NORMAL, fg)
513 if have_sourceview:
514 self.text.set_show_line_numbers(show_line_numbers.int_value)
515 self.text.set_show_line_markers(show_line_markers.int_value)
516 self.text.set_auto_indent(auto_indent.int_value)
517 self.text.set_tabs_width(tab_width.int_value)
518 self.text.set_insert_spaces_instead_of_tabs(use_spaces_for_tabs.int_value)
519 self.text.set_margin(right_margin.int_value)
520 self.text.set_show_margin(show_margin.int_value)
521 self.text.set_smart_home_end(smart_home_end.int_value)
522 if self.buffer.language == 'Python':
523 self.text.set_auto_indent(False)
525 def cut(self): self.text.emit('cut_clipboard')
526 def copy(self): self.text.emit('copy_clipboard')
527 def paste(self): self.text.emit('paste_clipboard')
529 def delete_event(self, window, event):
530 if self.buffer.get_modified():
531 self.save_as(discard = 1)
532 return 1
533 return 0
535 def update_title(self, *unused):
536 title = self.uri or '<'+_('Untitled')+'>'
537 if self.buffer.get_modified():
538 title = title + " *"
539 self.save_button.set_sensitive(True)
540 else:
541 self.save_button.set_sensitive(False)
542 self.set_title(title)
544 def xds_load_from_stream(self, name, t, stream):
545 if t == 'UTF8_STRING':
546 return # Gtk will handle it
547 try:
548 dnd_mark = self.buffer.get_mark('gtk_drag_target')
549 if dnd_mark:
550 dnd_pos = self.buffer.get_iter_at_mark(dnd_mark)
551 self.buffer.move_mark(self.insert_mark, dnd_pos)
552 self.insert_data(stream.read())
553 except Abort:
554 pass
556 def get_encoding(self, message):
557 "Returns (encoding, errors), or raises Abort to cancel."
558 box = g.MessageDialog(self, 0, g.MESSAGE_QUESTION, g.BUTTONS_CANCEL, message)
559 box.set_has_separator(False)
561 frame = g.Frame()
562 box.vbox.pack_start(frame, True, True)
563 frame.set_border_width(6)
565 hbox = g.HBox(False, 4)
566 hbox.set_border_width(6)
568 hbox.pack_start(g.Label(_('Encoding:')), False, True, 0)
569 combo = g.Combo()
570 combo.disable_activate()
571 combo.entry.connect('activate', lambda w: box.activate_default())
572 combo.set_popdown_strings(known_codecs)
573 hbox.pack_start(combo, True, True, 0)
574 ignore_errors = g.CheckButton(_('Ignore errors'))
575 hbox.pack_start(ignore_errors, False, True)
577 frame.add(hbox)
579 box.vbox.show_all()
580 box.add_button(g.STOCK_CONVERT, g.RESPONSE_YES)
581 box.set_default_response(g.RESPONSE_YES)
583 while 1:
584 combo.entry.grab_focus()
586 resp = box.run()
587 if resp != g.RESPONSE_YES:
588 box.destroy()
589 raise Abort
591 if ignore_errors.get_active():
592 errors = 'replace'
593 else:
594 errors = 'strict'
595 encoding = combo.entry.get_text()
596 try:
597 codecs.getdecoder(encoding)
598 break
599 except:
600 rox.alert(_("Unknown encoding '%s'") % encoding)
602 box.destroy()
604 return encoding, errors
606 def insert_data(self, data):
607 import codecs
608 errors = 'strict'
609 encoding = 'utf-8'
610 while 1:
611 decoder = codecs.getdecoder(encoding)
612 try:
613 data = decoder(data, errors)[0]
614 if errors == 'strict':
615 assert '\0' not in data
616 else:
617 if '\0' in data:
618 data = data.replace('\0', '\\0')
619 break
620 except:
621 pass
623 encoding, errors = self.get_encoding(
624 _("Data is not valid %s. Please select the file's encoding. "
625 "Turn on 'ignore errors' to try and load it anyway.")
626 % encoding)
628 self.buffer.begin_user_action()
629 self.buffer.insert_at_cursor(data)
630 self.buffer.end_user_action()
631 return 1
633 def load_file(self, path):
634 try:
635 if path == '-':
636 file = sys.stdin
637 else:
638 file = open(path, 'r')
639 contents = file.read()
640 if path != '-':
641 file.close()
643 self.buffer.begin_not_undoable_action()
644 self.insert_data(contents)
645 self.buffer.end_not_undoable_action()
646 except Abort:
647 raise
648 except:
649 rox.report_exception()
650 raise Abort
652 def close(self, button = None):
653 if self.buffer.get_modified():
654 self.save_as(discard = 1)
655 else:
656 self.destroy()
658 def discard(self):
659 self.destroy()
661 def up(self, button = None):
662 if self.uri:
663 filer.show_file(self.uri)
664 else:
665 rox.alert(_('File is not saved to disk yet'))
668 def toggle_spell(self, button = None):
669 if self.spell:
670 self.spell.detach()
671 self.spell = None
672 self.spell_button.set_active(False)
673 elif not self.spell_button.get_active():
674 # Probably a failed attempt to turn it on
675 pass
676 else:
677 try:
678 self.spell = gtkspell.Spell(self.text)
679 self.spell_button.set_active(True)
680 except Exception, ex:
681 self.spell = None
682 self.spell_button.set_active(False)
683 rox.report_exception()
685 #self.spell_button.set_active(self.spell != None)
687 def diff(self, button = None, path = None):
688 path = path or self.uri
689 if not path:
690 rox.alert(_('This file has never been saved; nothing to compare it to!\n'
691 'Note: you can drop a file onto the toolbar button to see '
692 'the changes from that file.'))
693 return
694 diff.show_diff(path, self.save_to_stream)
696 def has_selection(self):
697 s, e = self.get_selection_range()
698 return not e.equal(s)
700 def get_marked_range(self):
701 s = self.buffer.get_iter_at_mark(self.mark_start)
702 e = self.buffer.get_iter_at_mark(self.mark_end)
703 if s.compare(e) > 0:
704 return e, s
705 return s, e
707 def get_selection_range(self):
708 s = self.buffer.get_iter_at_mark(self.insert_mark)
709 e = self.buffer.get_iter_at_mark(self.selection_bound_mark)
710 if s.compare(e) > 0:
711 return e, s
712 return s, e
714 def save(self, widget = None):
715 if self.uri:
716 self.save_to_file(self.uri)
717 self.buffer.set_modified(False)
718 else:
719 self.save_as(discard=0)
721 def save_as(self, widget = None, discard = 0):
722 from rox.saving import SaveBox
724 if self.savebox:
725 self.savebox.destroy()
727 try:
728 self.mime_type = mime.get_type(self.uri, 1)
729 except:
730 self.mime_type = mime.lookup('text', 'plain')
732 mime_text = self.mime_type.media + '/' + self.mime_type.subtype
734 if self.has_selection() and not discard:
735 saver = SelectionSaver(self)
736 self.savebox = SaveBox(saver, 'Selection', mime_text)
737 self.savebox.connect('destroy', lambda w: saver.destroy())
738 else:
739 uri = self.uri or _('TextFile')
740 self.savebox = SaveBox(self, uri, mime_text, discard)
741 self.savebox.show()
743 def help(self, button = None):
744 filer.open_dir(os.path.join(rox.app_dir, 'Help'))
746 def save_to_stream(self, stream):
747 s = self.buffer.get_start_iter()
748 e = self.buffer.get_end_iter()
749 stream.write(self.buffer.get_text(s, e, True))
751 def set_uri(self, uri):
752 self.uri = uri
753 self.buffer.set_modified(False)
754 self.update_title()
756 def new(self):
757 EditWindow()
759 def change_font(self):
760 style = self.text.get_style().copy()
761 style.font = load_font(options.get('edit_font'))
762 self.text.set_style(style)
764 def show_options(self):
765 rox.edit_options()
767 def set_marked(self, start = None, end = None):
768 "Set the marked region (from the selection if no region is given)."
769 self.clear_marked()
770 assert not self.marked
772 buffer = self.buffer
773 if start:
774 assert end
775 else:
776 assert not end
777 start, end = self.get_selection_range()
778 buffer.move_mark(self.mark_start, start)
779 buffer.move_mark(self.mark_end, end)
780 buffer.apply_tag_by_name('marked',
781 buffer.get_iter_at_mark(self.mark_start),
782 buffer.get_iter_at_mark(self.mark_end))
783 self.marked = 1
785 def clear_marked(self):
786 if not self.marked:
787 return
788 self.marked = 0
789 buffer = self.buffer
790 buffer.remove_tag_by_name('marked',
791 buffer.get_iter_at_mark(self.mark_start),
792 buffer.get_iter_at_mark(self.mark_end))
794 def undo(self, widget = None):
795 self.buffer.undo()
796 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
797 self.text.scroll_to_iter(cursor, 0.05, False)
799 def redo(self, widget = None):
800 self.buffer.redo()
801 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
802 self.text.scroll_to_iter(cursor, 0.05, False)
804 def goto(self, widget = None):
805 from goto import Goto
806 self.set_minibuffer(Goto())
808 def search(self, widget = None):
809 if self.search_minibuffer is None:
810 from search import Search
811 self.search_minibuffer = Search()
812 self.set_minibuffer(self.search_minibuffer)
814 def search_again(self, widget = None):
815 if self.search_minibuffer and self.search_minibuffer is self.minibuffer:
816 self.minibuffer.activate() # Search again with same text
818 if self.minibuffer is None:
819 # Search mini-buffer not yet open
820 self.search()
821 self.minibuffer.restore_previous_search()
822 self.minibuffer.search_again()
824 def search_replace(self, widget = None):
825 from search import Replace
826 Replace(self).show()
828 def toggle_bookmark(self):
829 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
830 name = str(cursor.get_line())
831 marker = self.buffer.get_marker(name)
832 if marker:
833 self.buffer.delete_marker(marker);
834 else:
835 marker = self.buffer.create_marker(name, "bookmark", cursor);
837 def next_bookmark(self):
838 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
839 cursor.forward_char()
840 marker = self.buffer.get_next_marker(cursor)
841 if marker:
842 self.buffer.get_iter_at_marker (cursor, marker)
843 self.buffer.place_cursor(cursor)
844 self.text.scroll_to_iter(cursor, 0.05, False)
846 def prev_bookmark(self):
847 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
848 cursor.backward_char()
849 marker = self.buffer.get_prev_marker(cursor)
850 if marker:
851 self.buffer.get_iter_at_marker (cursor, marker)
852 self.buffer.place_cursor(cursor)
853 self.text.scroll_to_iter(cursor, 0.05, False)
855 def set_mini_label(self, label):
856 self.mini_label.set_text(label)
858 def set_minibuffer(self, minibuffer):
859 assert minibuffer is None or isinstance(minibuffer, Minibuffer)
861 if self.minibuffer:
862 self.minibuffer.close()
864 self.minibuffer = None
866 if minibuffer:
867 self.mini_entry.set_text('')
868 self.minibuffer = minibuffer
869 minibuffer.setup(self)
870 self.mini_entry.grab_focus()
871 self.mini_hbox.show_all()
872 else:
873 self.mini_hbox.hide()
874 self.text.grab_focus()
876 def mini_key_press(self, entry, kev):
877 if kev.keyval == g.keysyms.Escape:
878 self.set_minibuffer(None)
879 return 1
880 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
881 self.minibuffer.activate()
882 return 1
884 return self.minibuffer.key_press(kev)
886 def mini_changed(self, entry):
887 if not self.minibuffer:
888 return
889 self.minibuffer.changed()
891 def mini_show_info(self, *unused):
892 assert self.minibuffer
893 if self.info_box:
894 self.info_box.destroy()
895 self.info_box = g.MessageDialog(self, 0, g.MESSAGE_INFO, g.BUTTONS_OK,
896 self.minibuffer.info)
897 self.info_box.set_title(_('Minibuffer help'))
898 def destroy(box):
899 self.info_box = None
900 self.info_box.connect('destroy', destroy)
901 self.info_box.show()
902 self.info_box.connect('response', lambda w, r: w.destroy())
904 def process_selected(self, process):
905 """Calls process(line) on each line in the selection, or each line in the file
906 if there is no selection. If the result is not None, the text is replaced."""
907 self.buffer.begin_user_action()
908 try:
909 self._process_selected(process)
910 finally:
911 self.buffer.end_user_action()
913 def _process_selected(self, process):
914 if self.has_selection():
915 def get_end():
916 start, end = self.get_selection_range()
917 if start.compare(end) > 0:
918 return start
919 return end
920 start, end = self.get_selection_range()
921 if start.compare(end) > 0:
922 start = end
923 else:
924 def get_end():
925 return self.buffer.get_end_iter()
926 start = self.buffer.get_start_iter()
927 end = get_end()
929 while start.compare(end) <= 0:
930 line_end = start.copy()
931 line_end.forward_to_line_end()
932 if line_end.compare(end) >= 0:
933 line_end = end
934 line = self.buffer.get_text(start, line_end, False)
935 new = process(line)
936 if new is not None:
937 self.buffer.move_mark(self.mark_tmp, start)
938 self.buffer.insert(line_end, new)
939 start = self.buffer.get_iter_at_mark(self.mark_tmp)
940 line_end = start.copy()
941 line_end.forward_chars(len(line.decode('utf-8')))
942 self.buffer.delete(start, line_end)
944 start = self.buffer.get_iter_at_mark(self.mark_tmp)
945 end = get_end()
946 if not start.forward_line(): break
948 def set_word_wrap(self, value):
949 self._word_wrap = value
950 self.wrap_button.set_active(value)
951 if value:
952 self.text.set_wrap_mode(g.WRAP_WORD)
953 else:
954 self.text.set_wrap_mode(g.WRAP_NONE)
956 word_wrap = property(lambda self: self._word_wrap, set_word_wrap)
958 class SelectionSaver(Saveable):
959 def __init__(self, window):
960 self.window = window
961 window.set_marked()
963 def save_to_stream(self, stream):
964 s, e = self.window.get_marked_range()
965 stream.write(self.window.buffer.get_text(s, e, True))
967 def destroy(self):
968 # Called when savebox is remove. Get rid of the selection marker
969 self.window.clear_marked()