Marked strings as translatable and added Italian translation (Roberto
[rox-edit.git] / EditWindow.py
blobc72e62fb80315891096947cf701949f133e7f562
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
10 FALSE = g.FALSE
11 TRUE = g.TRUE
13 from rox.Menu import Menu, set_save_name
15 default_font = Option('default_font', 'serif')
17 background_colour = Option('background', '#fff')
18 foreground_colour = Option('foreground', '#000')
20 auto_indent = Option('autoindent', '1')
22 layout_left_margin = Option('layout_left_margin', 2)
23 layout_right_margin = Option('layout_right_margin', 4)
25 layout_before_para = Option('layout_before_para', 0)
26 layout_after_para = Option('layout_after_para', 0)
27 layout_inside_para = Option('layout_inside_para', 0)
28 layout_indent_para = Option('layout_indent_para', 2)
30 set_save_name('Edit')
32 try:
33 menu = Menu('main', [
34 (_('/File'), '', '<Branch>'),
35 (_('/File/') + _('Save'), 'save', '<StockItem>', '<Ctrl>S', g.STOCK_SAVE),
36 (_('/File/') + _('Open Parent'), 'up', '<StockItem>', '', g.STOCK_GO_UP),
37 (_('/File/') + _('Close'), 'close', '<StockItem>', '', g.STOCK_CLOSE),
38 (_('/File/'), '', '<Separator>'),
39 (_('/File/') + _('New'), 'new', '<StockItem>', '', g.STOCK_NEW),
41 (_('/Edit'), '', '<Branch>'),
42 (_('/Edit/') + _('Cut'), 'cut', '<StockItem>', '<Ctrl>X', g.STOCK_CUT),
43 (_('/Edit/') + _('Copy'), 'copy', '<StockItem>', '<Ctrl>C', g.STOCK_COPY),
44 (_('/Edit/') + _('Paste'), 'paste', '<StockItem>', '<Ctrl>V', g.STOCK_PASTE),
45 (_('/Edit/'), '', '<Separator>'),
46 (_('/Edit/') + _('Undo'), 'undo', '<StockItem>', '<Ctrl>Z', g.STOCK_UNDO),
47 (_('/Edit/') + _('Redo'), 'redo', '<StockItem>', '<Ctrl>Y', g.STOCK_REDO),
48 (_('/Edit/'), '', '<Separator>'),
49 (_('/Edit/') + _('Search...'), 'search', '<StockItem>', 'F4', g.STOCK_FIND),
50 (_('/Edit/') + _('Search and Replace....'), 'search_replace',
51 '<StockItem>', '<Ctrl>F4', g.STOCK_FIND_AND_REPLACE),
52 (_('/Edit/') + _('Goto line...'), 'goto', '<StockItem>', 'F5', g.STOCK_JUMP_TO),
54 (_('/Options'), 'show_options', '<StockItem>', '', g.STOCK_PROPERTIES),
55 (_('/Help'), 'help', '<StockItem>', 'F1', g.STOCK_HELP),
57 except ValueError:
58 rox.croak(_('Edit requires ROX-Lib2 1.9.8 or later'))
60 known_codecs = (
61 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
62 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
63 "iso8859_13", "iso8859_14", "iso8859_15",
64 "ascii", "base64_codec", "charmap",
65 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
66 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
67 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
68 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
69 "cp869", "cp874", "cp875", "hex_codec",
70 "koi8_r",
71 "latin_1",
72 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
73 "mbcs", "quopri_codec", "raw_unicode_escape",
74 "rot_13",
75 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
76 "zlib_codec"
79 class Abort(Exception):
80 pass
82 class Minibuffer:
83 def setup(self):
84 """Called when the minibuffer is opened."""
86 def key_press(self, kev):
87 """A keypress event in the minibuffer text entry."""
89 def changed(self):
90 """The minibuffer text has changed."""
92 def activate(self):
93 """Return or Enter pressed."""
95 info = 'Press Escape to close the minibuffer.'
97 class EditWindow(rox.Window, XDSLoader, Saveable):
98 def __init__(self, filename = None):
99 rox.Window.__init__(self)
100 XDSLoader.__init__(self, ['text/plain', 'UTF8_STRING'])
101 self.set_default_size(g.gdk.screen_width() * 2 / 3,
102 g.gdk.screen_height() / 2)
104 self.savebox = None
105 self.info_box = None
107 app_options.add_notify(self.update_styles)
109 self.buffer = Buffer()
111 self.text = g.TextView()
112 self.text.set_buffer(self.buffer)
113 self.text.set_size_request(10, 10)
114 self.xds_proxy_for(self.text)
115 self.text.set_wrap_mode(g.WRAP_WORD)
116 self.update_styles()
118 self.insert_mark = self.buffer.get_mark('insert')
119 self.selection_bound_mark = self.buffer.get_mark('selection_bound')
120 start = self.buffer.get_start_iter()
121 self.mark_start = self.buffer.create_mark('mark_start', start, TRUE)
122 self.mark_end = self.buffer.create_mark('mark_end', start, FALSE)
123 self.mark_tmp = self.buffer.create_mark('mark_tmp', start, FALSE)
124 tag = self.buffer.create_tag('marked')
125 tag.set_property('background', 'green')
126 self.marked = 0
128 # When searching, this is where the cursor was when the minibuffer
129 # was opened.
130 start = self.buffer.get_start_iter()
131 self.search_base = self.buffer.create_mark('search_base', start, TRUE)
133 vbox = g.VBox(FALSE)
134 self.add(vbox)
136 tools = g.Toolbar()
137 tools.set_style(g.TOOLBAR_ICONS)
138 vbox.pack_start(tools, FALSE, TRUE, 0)
139 tools.show()
141 tools.insert_stock(g.STOCK_HELP, _('Help'), None, self.help, None, 0)
142 tools.insert_stock(g.STOCK_REDO, _('Redo'), None, self.redo, None, 0)
143 tools.insert_stock(g.STOCK_UNDO, _('Undo'), None, self.undo, None, 0)
144 tools.insert_stock(g.STOCK_FIND_AND_REPLACE, _('Replace'), None, self.search_replace, None, 0)
145 tools.insert_stock(g.STOCK_FIND, _('Search'), None, self.search, None, 0)
146 tools.insert_stock(g.STOCK_SAVE, _('Save'), None, self.save, None, 0)
147 tools.insert_stock(g.STOCK_GO_UP, _('Up'), None, self.up, None, 0)
148 tools.insert_stock(g.STOCK_CLOSE, _('Close'), None, self.close, None, 0)
150 swin = g.ScrolledWindow()
151 swin.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
152 vbox.pack_start(swin, True, True)
154 swin.add(self.text)
156 self.show_all()
158 # Create the minibuffer
159 self.mini_hbox = g.HBox(FALSE)
160 info = g.Button()
161 info.set_relief(g.RELIEF_NONE)
162 info.unset_flags(g.CAN_FOCUS)
163 image = g.Image()
164 image.set_from_stock(g.STOCK_DIALOG_INFO, size = g.ICON_SIZE_SMALL_TOOLBAR)
165 info.add(image)
166 info.show_all()
167 info.connect('clicked', self.mini_show_info)
169 self.mini_hbox.pack_start(info, FALSE, TRUE, 0)
170 self.mini_label = g.Label('')
171 self.mini_hbox.pack_start(self.mini_label, FALSE, TRUE, 0)
172 self.mini_entry = g.Entry()
173 self.mini_hbox.pack_start(self.mini_entry, TRUE, TRUE, 0)
174 vbox.pack_start(self.mini_hbox, FALSE, TRUE)
175 self.mini_entry.connect('key-press-event', self.mini_key_press)
176 self.mini_entry.connect('changed', self.mini_changed)
178 self.connect('destroy', self.destroyed)
180 self.connect('delete-event', self.delete_event)
181 self.text.grab_focus()
182 self.text.connect('key-press-event', self.key_press)
184 if filename:
185 import os.path
186 self.uri = os.path.abspath(filename)
187 else:
188 self.uri = None
189 self.update_title()
191 # Loading might take a while, so get something on the screen
192 # now...
193 g.gdk.flush()
195 if filename:
196 try:
197 self.load_file(filename)
198 if filename != '-':
199 self.save_mode = os.stat(filename).st_mode
200 except Abort:
201 self.destroy()
202 raise
204 self.buffer.connect('modified-changed', self.update_title)
205 self.buffer.set_modified(FALSE)
207 self.text.connect('button-press-event', self.button_press)
209 menu.attach(self, self)
210 self.buffer.place_cursor(self.buffer.get_start_iter())
211 self.buffer.start_undo_history()
213 def key_press(self, text, kev):
214 if kev.keyval != g.keysyms.Return and kev.keyval != g.keysyms.KP_Enter:
215 return
216 if not auto_indent.int_value:
217 return
218 start = self.buffer.get_iter_at_mark(self.insert_mark)
219 end = start.copy()
220 start.set_line_offset(0)
221 end.forward_to_line_end()
222 line = self.buffer.get_text(start, end, False)
223 indent = ''
224 for x in line:
225 if x in ' \t':
226 indent += x
227 else:
228 break
229 self.buffer.begin_user_action()
230 self.buffer.insert_at_cursor('\n' + indent)
231 self.buffer.end_user_action()
232 return True
234 def destroyed(self, widget):
235 app_options.remove_notify(self.update_styles)
237 def update_styles(self):
238 try:
239 import pango
240 font = pango.FontDescription(default_font.value)
241 bg = g.gdk.color_parse(background_colour.value)
242 fg = g.gdk.color_parse(foreground_colour.value)
244 self.text.set_left_margin(layout_left_margin.int_value)
245 self.text.set_right_margin(layout_right_margin.int_value)
247 self.text.set_pixels_above_lines(layout_before_para.int_value)
248 self.text.set_pixels_below_lines(layout_after_para.int_value)
249 self.text.set_pixels_inside_wrap(layout_inside_para.int_value)
250 self.text.set_indent(layout_indent_para.int_value)
251 except:
252 rox.report_exception()
253 else:
254 self.text.modify_font(font)
255 self.text.modify_base(g.STATE_NORMAL, bg)
256 self.text.modify_text(g.STATE_NORMAL, fg)
258 def cut(self): self.text.emit('cut_clipboard')
259 def copy(self): self.text.emit('copy_clipboard')
260 def paste(self): self.text.emit('paste_clipboard')
262 def button_press(self, text, event):
263 if event.button != 3:
264 return 0
265 menu.popup(self, event)
266 return 1
268 def delete_event(self, window, event):
269 if self.buffer.get_modified():
270 self.save(discard = 1)
271 return 1
272 return 0
274 def update_title(self, *unused):
275 title = self.uri or '<Untitled>'
276 if self.buffer.get_modified():
277 title = title + " *"
278 self.set_title(title)
280 def xds_load_from_stream(self, name, t, stream):
281 if t == 'UTF8_STRING':
282 return # Gtk will handle it
283 try:
284 self.insert_data(stream.read())
285 except Abort:
286 pass
288 def get_encoding(self, message):
289 "Returns (encoding, errors), or raises Abort to cancel."
290 import codecs
292 box = g.MessageDialog(self, 0, g.MESSAGE_QUESTION, g.BUTTONS_CANCEL, message)
293 box.set_has_separator(FALSE)
295 frame = g.Frame()
296 box.vbox.pack_start(frame, TRUE, TRUE)
297 frame.set_border_width(6)
299 hbox = g.HBox(FALSE, 4)
300 hbox.set_border_width(6)
302 hbox.pack_start(g.Label('Encoding:'), FALSE, TRUE, 0)
303 combo = g.Combo()
304 combo.disable_activate()
305 combo.entry.connect('activate', lambda w: box.activate_default())
306 combo.set_popdown_strings(known_codecs)
307 hbox.pack_start(combo, TRUE, TRUE, 0)
308 ignore_errors = g.CheckButton('Ignore errors')
309 hbox.pack_start(ignore_errors, FALSE, TRUE)
311 frame.add(hbox)
313 box.vbox.show_all()
314 box.add_button(g.STOCK_CONVERT, g.RESPONSE_YES)
315 box.set_default_response(g.RESPONSE_YES)
317 while 1:
318 combo.entry.grab_focus()
320 resp = box.run()
321 if resp != g.RESPONSE_YES:
322 box.destroy()
323 raise Abort
325 if ignore_errors.get_active():
326 errors = 'replace'
327 else:
328 errors = 'strict'
329 encoding = combo.entry.get_text()
330 try:
331 codecs.getdecoder(encoding)
332 break
333 except:
334 rox.alert(_("Unknown encoding '%s'") % encoding)
336 box.destroy()
338 return encoding, errors
340 def insert_data(self, data):
341 import codecs
342 errors = 'strict'
343 encoding = 'utf-8'
344 while 1:
345 decoder = codecs.getdecoder(encoding)
346 try:
347 data = decoder(data, errors)[0]
348 if errors == 'strict':
349 assert '\0' not in data
350 else:
351 if '\0' in data:
352 data = data.replace('\0', '\\0')
353 break
354 except:
355 pass
357 encoding, errors = self.get_encoding(
358 _("Data is not valid %s. Please select the file's encoding. "
359 "Turn on 'ignore errors' to try and load it anyway.")
360 % encoding)
362 self.buffer.begin_user_action()
363 self.buffer.insert_at_cursor(data)
364 self.buffer.end_user_action()
365 return 1
367 def load_file(self, path):
368 try:
369 if path == '-':
370 file = sys.stdin
371 else:
372 file = open(path, 'r')
373 contents = file.read()
374 if path != '-':
375 file.close()
376 self.insert_data(contents)
377 except Abort:
378 raise
379 except:
380 rox.report_exception()
381 raise Abort
383 def close(self, button = None):
384 if self.buffer.get_modified():
385 self.save(discard = 1)
386 else:
387 self.destroy()
389 def discard(self):
390 self.destroy()
392 def up(self, button = None):
393 if self.uri:
394 filer.show_file(self.uri)
395 else:
396 rox.alert(_('File is not saved to disk yet'))
398 def has_selection(self):
399 s, e = self.get_selection_range()
400 return not e.equal(s)
402 def get_marked_range(self):
403 s = self.buffer.get_iter_at_mark(self.mark_start)
404 e = self.buffer.get_iter_at_mark(self.mark_end)
405 if s.compare(e) > 0:
406 return e, s
407 return s, e
409 def get_selection_range(self):
410 s = self.buffer.get_iter_at_mark(self.insert_mark)
411 e = self.buffer.get_iter_at_mark(self.selection_bound_mark)
412 if s.compare(e) > 0:
413 return e, s
414 return s, e
416 def save(self, widget = None, discard = 0):
417 from rox.saving import SaveBox
419 if self.savebox:
420 self.savebox.destroy()
422 if self.has_selection() and not discard:
423 saver = SelectionSaver(self)
424 self.savebox = SaveBox(saver, 'Selection', 'text/plain')
425 self.savebox.connect('destroy', lambda w: saver.destroy())
426 else:
427 uri = self.uri or 'TextFile'
428 self.savebox = SaveBox(self, uri, 'text/plain', discard)
429 self.savebox.show()
431 def help(self, button = None):
432 filer.open_dir(os.path.join(rox.app_dir, 'Help'))
434 def save_to_stream(self, stream):
435 s = self.buffer.get_start_iter()
436 e = self.buffer.get_end_iter()
437 stream.write(self.buffer.get_text(s, e, TRUE))
439 def set_uri(self, uri):
440 self.uri = uri
441 self.buffer.set_modified(FALSE)
442 self.update_title()
444 def new(self):
445 EditWindow()
447 def change_font(self):
448 style = self.text.get_style().copy()
449 style.font = load_font(options.get('edit_font'))
450 self.text.set_style(style)
452 def show_options(self):
453 rox.edit_options()
455 def set_marked(self, start = None, end = None):
456 "Set the marked region (from the selection if no region is given)."
457 self.clear_marked()
458 assert not self.marked
460 buffer = self.buffer
461 if start:
462 assert end
463 else:
464 assert not end
465 start, end = self.get_selection_range()
466 buffer.move_mark(self.mark_start, start)
467 buffer.move_mark(self.mark_end, end)
468 buffer.apply_tag_by_name('marked',
469 buffer.get_iter_at_mark(self.mark_start),
470 buffer.get_iter_at_mark(self.mark_end))
471 self.marked = 1
473 def clear_marked(self):
474 if not self.marked:
475 return
476 self.marked = 0
477 buffer = self.buffer
478 buffer.remove_tag_by_name('marked',
479 buffer.get_iter_at_mark(self.mark_start),
480 buffer.get_iter_at_mark(self.mark_end))
482 def undo(self, widget = None):
483 self.buffer.undo()
485 def redo(self, widget = None):
486 self.buffer.redo()
488 def goto(self, widget = None):
489 from goto import Goto
490 self.set_minibuffer(Goto())
492 def search(self, widget = None):
493 from search import Search
494 self.set_minibuffer(Search())
496 def search_replace(self, widget = None):
497 from search import Replace
498 Replace(self).show()
500 def set_mini_label(self, label):
501 self.mini_label.set_text(label)
503 def set_minibuffer(self, minibuffer):
504 assert minibuffer is None or isinstance(minibuffer, Minibuffer)
506 self.minibuffer = None
508 if minibuffer:
509 self.mini_entry.set_text('')
510 self.minibuffer = minibuffer
511 minibuffer.setup(self)
512 self.mini_entry.grab_focus()
513 self.mini_hbox.show_all()
514 else:
515 self.mini_hbox.hide()
516 self.text.grab_focus()
518 def mini_key_press(self, entry, kev):
519 if kev.keyval == g.keysyms.Escape:
520 self.set_minibuffer(None)
521 return 1
522 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
523 self.minibuffer.activate()
524 return 1
526 return self.minibuffer.key_press(kev)
528 def mini_changed(self, entry):
529 if not self.minibuffer:
530 return
531 self.minibuffer.changed()
533 def mini_show_info(self, *unused):
534 assert self.minibuffer
535 if self.info_box:
536 self.info_box.destroy()
537 self.info_box = g.MessageDialog(self, 0, g.MESSAGE_INFO, g.BUTTONS_OK,
538 self.minibuffer.info)
539 self.info_box.set_title(_('Minibuffer help'))
540 def destroy(box):
541 self.info_box = None
542 self.info_box.connect('destroy', destroy)
543 self.info_box.show()
544 self.info_box.connect('response', lambda w, r: w.destroy())
546 def process_selected(self, process):
547 """Calls process(line) on each line in the selection, or each line in the file
548 if there is no selection. If the result is not None, the text is replaced."""
549 self.buffer.begin_user_action()
550 self._process_selected(process)
551 self.buffer.end_user_action()
553 def _process_selected(self, process):
554 if self.has_selection():
555 def get_end():
556 start, end = self.get_selection_range()
557 if start.compare(end) > 0:
558 return start
559 return end
560 start, end = self.get_selection_range()
561 if start.compare(end) > 0:
562 start = end
563 else:
564 def get_end():
565 return self.buffer.get_end_iter()
566 start = self.buffer.get_start_iter()
567 end = get_end()
569 while start.compare(end) <= 0:
570 line_end = start.copy()
571 line_end.forward_to_line_end()
572 if line_end.compare(end) >= 0:
573 line_end = end
574 line = self.buffer.get_text(start, line_end, False)
575 new = process(line)
576 if new is not None:
577 self.buffer.move_mark(self.mark_tmp, start)
578 self.buffer.insert(line_end, new)
579 start = self.buffer.get_iter_at_mark(self.mark_tmp)
580 line_end = start.copy()
581 line_end.forward_chars(len(line))
582 self.buffer.delete(start, line_end)
584 start = self.buffer.get_iter_at_mark(self.mark_tmp)
585 end = get_end()
586 if not start.forward_line(): break
588 class SelectionSaver(Saveable):
589 def __init__(self, window):
590 self.window = window
591 window.set_marked()
593 def save_to_stream(self, stream):
594 s, e = self.window.get_marked_range()
595 stream.write(self.window.buffer.get_text(s, e, TRUE))
597 def destroy(self):
598 # Called when savebox is remove. Get rid of the selection marker
599 self.window.clear_marked()