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