Updated local Zero Install interface to newer format.
[rox-edit/bju.git] / EditWindow.py
blob2f8f11956a53716f830fa8819b1c77f2a3522166
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 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.SourceView(self.buffer)
226 pixbuf = g.gdk.pixbuf_new_from_file(rox.app_dir+"/images/marker.png")
227 self.text.set_marker_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_markers(show_line_markers.int_value)
517 self.text.set_auto_indent(auto_indent.int_value)
518 self.text.set_tabs_width(tab_width.int_value)
519 self.text.set_insert_spaces_instead_of_tabs(use_spaces_for_tabs.int_value)
520 self.text.set_margin(right_margin.int_value)
521 self.text.set_show_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.show()
744 def help(self, button = None):
745 filer.open_dir(os.path.join(rox.app_dir, 'Help'))
747 def save_to_stream(self, stream):
748 s = self.buffer.get_start_iter()
749 e = self.buffer.get_end_iter()
750 stream.write(self.buffer.get_text(s, e, True))
752 def set_uri(self, uri):
753 self.uri = uri
754 self.buffer.set_modified(False)
755 self.update_title()
757 def new(self):
758 EditWindow()
760 def change_font(self):
761 style = self.text.get_style().copy()
762 style.font = load_font(options.get('edit_font'))
763 self.text.set_style(style)
765 def show_options(self):
766 rox.edit_options()
768 def set_marked(self, start = None, end = None):
769 "Set the marked region (from the selection if no region is given)."
770 self.clear_marked()
771 assert not self.marked
773 buffer = self.buffer
774 if start:
775 assert end
776 else:
777 assert not end
778 start, end = self.get_selection_range()
779 buffer.move_mark(self.mark_start, start)
780 buffer.move_mark(self.mark_end, end)
781 buffer.apply_tag_by_name('marked',
782 buffer.get_iter_at_mark(self.mark_start),
783 buffer.get_iter_at_mark(self.mark_end))
784 self.marked = 1
786 def clear_marked(self):
787 if not self.marked:
788 return
789 self.marked = 0
790 buffer = self.buffer
791 buffer.remove_tag_by_name('marked',
792 buffer.get_iter_at_mark(self.mark_start),
793 buffer.get_iter_at_mark(self.mark_end))
795 def undo(self, widget = None):
796 self.buffer.undo()
797 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
798 self.text.scroll_to_iter(cursor, 0.05, False)
800 def redo(self, widget = None):
801 self.buffer.redo()
802 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
803 self.text.scroll_to_iter(cursor, 0.05, False)
805 def goto(self, widget = None):
806 from goto import Goto
807 self.set_minibuffer(Goto())
809 def search(self, widget = None):
810 if self.search_minibuffer is None:
811 from search import Search
812 self.search_minibuffer = Search()
813 self.set_minibuffer(self.search_minibuffer)
815 def search_again(self, widget = None):
816 if self.search_minibuffer and self.search_minibuffer is self.minibuffer:
817 self.minibuffer.activate() # Search again with same text
819 if self.minibuffer is None:
820 # Search mini-buffer not yet open
821 self.search()
822 self.minibuffer.restore_previous_search()
823 self.minibuffer.search_again()
825 def search_replace(self, widget = None):
826 from search import Replace
827 Replace(self).show()
829 def toggle_bookmark(self):
830 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
831 name = str(cursor.get_line())
832 marker = self.buffer.get_marker(name)
833 if marker:
834 self.buffer.delete_marker(marker);
835 else:
836 marker = self.buffer.create_marker(name, "bookmark", cursor);
838 def next_bookmark(self):
839 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
840 cursor.forward_char()
841 marker = self.buffer.get_next_marker(cursor)
842 if marker:
843 self.buffer.get_iter_at_marker (cursor, marker)
844 self.buffer.place_cursor(cursor)
845 self.text.scroll_to_iter(cursor, 0.05, False)
847 def prev_bookmark(self):
848 cursor = self.buffer.get_iter_at_mark(self.insert_mark)
849 cursor.backward_char()
850 marker = self.buffer.get_prev_marker(cursor)
851 if marker:
852 self.buffer.get_iter_at_marker (cursor, marker)
853 self.buffer.place_cursor(cursor)
854 self.text.scroll_to_iter(cursor, 0.05, False)
856 def set_mini_label(self, label):
857 self.mini_label.set_text(label)
859 def set_minibuffer(self, minibuffer):
860 assert minibuffer is None or isinstance(minibuffer, Minibuffer)
862 if self.minibuffer:
863 self.minibuffer.close()
865 self.minibuffer = None
867 if minibuffer:
868 self.mini_entry.set_text('')
869 self.minibuffer = minibuffer
870 minibuffer.setup(self)
871 self.mini_entry.grab_focus()
872 self.mini_hbox.show_all()
873 else:
874 self.mini_hbox.hide()
875 self.text.grab_focus()
877 def mini_key_press(self, entry, kev):
878 if kev.keyval == g.keysyms.Escape:
879 self.set_minibuffer(None)
880 return 1
881 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
882 self.minibuffer.activate()
883 return 1
885 return self.minibuffer.key_press(kev)
887 def mini_changed(self, entry):
888 if not self.minibuffer:
889 return
890 self.minibuffer.changed()
892 def mini_show_info(self, *unused):
893 assert self.minibuffer
894 if self.info_box:
895 self.info_box.destroy()
896 self.info_box = g.MessageDialog(self, 0, g.MESSAGE_INFO, g.BUTTONS_OK,
897 self.minibuffer.info)
898 self.info_box.set_title(_('Minibuffer help'))
899 def destroy(box):
900 self.info_box = None
901 self.info_box.connect('destroy', destroy)
902 self.info_box.show()
903 self.info_box.connect('response', lambda w, r: w.destroy())
905 def process_selected(self, process):
906 """Calls process(line) on each line in the selection, or each line in the file
907 if there is no selection. If the result is not None, the text is replaced."""
908 self.buffer.begin_user_action()
909 try:
910 self._process_selected(process)
911 finally:
912 self.buffer.end_user_action()
914 def _process_selected(self, process):
915 if self.has_selection():
916 def get_end():
917 start, end = self.get_selection_range()
918 if start.compare(end) > 0:
919 return start
920 return end
921 start, end = self.get_selection_range()
922 if start.compare(end) > 0:
923 start = end
924 else:
925 def get_end():
926 return self.buffer.get_end_iter()
927 start = self.buffer.get_start_iter()
928 end = get_end()
930 while start.compare(end) <= 0:
931 line_end = start.copy()
932 line_end.forward_to_line_end()
933 if line_end.compare(end) >= 0:
934 line_end = end
935 line = self.buffer.get_text(start, line_end, False)
936 new = process(line)
937 if new is not None:
938 self.buffer.move_mark(self.mark_tmp, start)
939 self.buffer.insert(line_end, new)
940 start = self.buffer.get_iter_at_mark(self.mark_tmp)
941 line_end = start.copy()
942 line_end.forward_chars(len(line.decode('utf-8')))
943 self.buffer.delete(start, line_end)
945 start = self.buffer.get_iter_at_mark(self.mark_tmp)
946 end = get_end()
947 if not start.forward_line(): break
949 def set_word_wrap(self, value):
950 self._word_wrap = value
951 self.wrap_button.set_active(value)
952 if value:
953 self.text.set_wrap_mode(g.WRAP_WORD)
954 else:
955 self.text.set_wrap_mode(g.WRAP_NONE)
957 word_wrap = property(lambda self: self._word_wrap, set_word_wrap)
959 class SelectionSaver(Saveable):
960 def __init__(self, window):
961 self.window = window
962 window.set_marked()
964 def save_to_stream(self, stream):
965 s, e = self.window.get_marked_range()
966 stream.write(self.window.buffer.get_text(s, e, True))
968 def destroy(self):
969 # Called when savebox is remove. Get rid of the selection marker
970 self.window.clear_marked()