2 from rox
import g
, filer
, app_options
4 from rox
.loading
import XDSLoader
5 from rox
.options
import Option
6 from buffer import Buffer
7 from rox
.saving
import Saveable
12 to_utf8
= codecs
.getencoder('utf-8')
17 from rox
.Menu
import Menu
, set_save_name
19 default_font
= Option('default_font', 'serif')
21 background_colour
= Option('background', '#fff')
22 foreground_colour
= Option('foreground', '#000')
24 auto_indent
= Option('autoindent', '1')
26 layout_left_margin
= Option('layout_left_margin', 2)
27 layout_right_margin
= Option('layout_right_margin', 4)
29 layout_before_para
= Option('layout_before_para', 0)
30 layout_after_para
= Option('layout_after_para', 0)
31 layout_inside_para
= Option('layout_inside_para', 0)
32 layout_indent_para
= Option('layout_indent_para', 2)
38 (_('/File'), '', '<Branch>'),
39 (_('/File/') + _('Save'), 'save', '<StockItem>', '<Ctrl>S', g
.STOCK_SAVE
),
40 (_('/File/') + _('Open Parent'), 'up', '<StockItem>', '', g
.STOCK_GO_UP
),
41 (_('/File/') + _('Show Changes'), 'diff', '<StockItem>', '', 'rox-diff'),
42 (_('/File/') + _('Close'), 'close', '<StockItem>', '', g
.STOCK_CLOSE
),
43 (_('/File/'), '', '<Separator>'),
44 (_('/File/') + _('New'), 'new', '<StockItem>', '', g
.STOCK_NEW
),
46 (_('/Edit'), '', '<Branch>'),
47 (_('/Edit/') + _('Cut'), 'cut', '<StockItem>', '<Ctrl>X', g
.STOCK_CUT
),
48 (_('/Edit/') + _('Copy'), 'copy', '<StockItem>', '<Ctrl>C', g
.STOCK_COPY
),
49 (_('/Edit/') + _('Paste'), 'paste', '<StockItem>', '<Ctrl>V', g
.STOCK_PASTE
),
50 (_('/Edit/'), '', '<Separator>'),
51 (_('/Edit/') + _('Undo'), 'undo', '<StockItem>', '<Ctrl>Z', g
.STOCK_UNDO
),
52 (_('/Edit/') + _('Redo'), 'redo', '<StockItem>', '<Ctrl>Y', g
.STOCK_REDO
),
53 (_('/Edit/'), '', '<Separator>'),
54 (_('/Edit/') + _('Search...'), 'search', '<StockItem>', 'F4', g
.STOCK_FIND
),
55 (_('/Edit/') + _('Search and Replace....'), 'search_replace',
56 '<StockItem>', '<Ctrl>F4', g
.STOCK_FIND_AND_REPLACE
),
57 (_('/Edit/') + _('Goto line...'), 'goto', '<StockItem>', 'F5', g
.STOCK_JUMP_TO
),
59 (_('/Options'), 'show_options', '<StockItem>', '', g
.STOCK_PROPERTIES
),
60 (_('/Help'), 'help', '<StockItem>', 'F1', g
.STOCK_HELP
),
63 rox
.croak(_('Edit requires ROX-Lib2 1.9.8 or later'))
66 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
67 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
68 "iso8859_13", "iso8859_14", "iso8859_15",
69 "ascii", "base64_codec", "charmap",
70 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
71 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
72 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
73 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
74 "cp869", "cp874", "cp875", "hex_codec",
77 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
78 "mbcs", "quopri_codec", "raw_unicode_escape",
80 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
84 class Abort(Exception):
89 """Called when the minibuffer is opened."""
91 def key_press(self
, kev
):
92 """A keypress event in the minibuffer text entry."""
95 """The minibuffer text has changed."""
98 """Return or Enter pressed."""
100 info
= 'Press Escape to close the minibuffer.'
102 class DiffLoader(XDSLoader
):
103 def __init__(self
, window
):
104 XDSLoader
.__init
__(self
, ['text/plain'])
107 def xds_load_from_file(self
, path
):
108 self
.window
.diff(path
= path
)
110 def xds_load_from_stream(self
, name
, type, stream
):
111 tmp
= diff
.Tmp(suffix
= '-' + (name
or 'tmp'))
113 shutil
.copyfileobj(stream
, tmp
)
115 self
.window
.diff(path
= tmp
.name
)
117 class EditWindow(rox
.Window
, XDSLoader
, Saveable
):
118 def __init__(self
, filename
= None):
119 rox
.Window
.__init
__(self
)
120 XDSLoader
.__init
__(self
, ['text/plain', 'UTF8_STRING'])
121 self
.set_default_size(g
.gdk
.screen_width() * 2 / 3,
122 g
.gdk
.screen_height() / 2)
127 app_options
.add_notify(self
.update_styles
)
129 self
.buffer = Buffer()
131 self
.text
= g
.TextView()
132 self
.text
.set_buffer(self
.buffer)
133 self
.text
.set_size_request(10, 10)
134 self
.xds_proxy_for(self
.text
)
135 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
138 self
.insert_mark
= self
.buffer.get_mark('insert')
139 self
.selection_bound_mark
= self
.buffer.get_mark('selection_bound')
140 start
= self
.buffer.get_start_iter()
141 self
.mark_start
= self
.buffer.create_mark('mark_start', start
, TRUE
)
142 self
.mark_end
= self
.buffer.create_mark('mark_end', start
, FALSE
)
143 self
.mark_tmp
= self
.buffer.create_mark('mark_tmp', start
, FALSE
)
144 tag
= self
.buffer.create_tag('marked')
145 tag
.set_property('background', 'green')
148 # When searching, this is where the cursor was when the minibuffer
150 start
= self
.buffer.get_start_iter()
151 self
.search_base
= self
.buffer.create_mark('search_base', start
, TRUE
)
157 tools
.set_style(g
.TOOLBAR_ICONS
)
158 vbox
.pack_start(tools
, FALSE
, TRUE
, 0)
161 self
.status_label
= g
.Label('')
162 tools
.append_widget(self
.status_label
, None, None)
163 tools
.insert_stock(g
.STOCK_HELP
, _('Help'), None, self
.help, None, 0)
164 diff
= tools
.insert_stock('rox-diff', _('Show changes from saved copy.\n'
165 'Or, drop a backup file onto this button to see changes from that.'),
166 None, self
.diff
, None, 0)
167 DiffLoader(self
).xds_proxy_for(diff
)
168 tools
.insert_stock(g
.STOCK_REDO
, _('Redo'), None, self
.redo
, None, 0)
169 tools
.insert_stock(g
.STOCK_UNDO
, _('Undo'), None, self
.undo
, None, 0)
170 tools
.insert_stock(g
.STOCK_FIND_AND_REPLACE
, _('Replace'), None, self
.search_replace
, None, 0)
171 tools
.insert_stock(g
.STOCK_FIND
, _('Search'), None, self
.search
, None, 0)
172 tools
.insert_stock(g
.STOCK_SAVE
, _('Save'), None, self
.save
, None, 0)
173 tools
.insert_stock(g
.STOCK_GO_UP
, _('Up'), None, self
.up
, None, 0)
174 tools
.insert_stock(g
.STOCK_CLOSE
, _('Close'), None, self
.close
, None, 0)
175 # Set minimum size to ignore the label
176 tools
.set_size_request(tools
.size_request()[0], -1)
178 swin
= g
.ScrolledWindow()
179 swin
.set_policy(g
.POLICY_NEVER
, g
.POLICY_AUTOMATIC
)
180 vbox
.pack_start(swin
, True, True)
186 # Create the minibuffer
187 self
.mini_hbox
= g
.HBox(FALSE
)
189 info
.set_relief(g
.RELIEF_NONE
)
190 info
.unset_flags(g
.CAN_FOCUS
)
192 image
.set_from_stock(g
.STOCK_DIALOG_INFO
, size
= g
.ICON_SIZE_SMALL_TOOLBAR
)
195 info
.connect('clicked', self
.mini_show_info
)
197 self
.mini_hbox
.pack_start(info
, FALSE
, TRUE
, 0)
198 self
.mini_label
= g
.Label('')
199 self
.mini_hbox
.pack_start(self
.mini_label
, FALSE
, TRUE
, 0)
200 self
.mini_entry
= g
.Entry()
201 self
.mini_hbox
.pack_start(self
.mini_entry
, TRUE
, TRUE
, 0)
202 vbox
.pack_start(self
.mini_hbox
, FALSE
, TRUE
)
203 self
.mini_entry
.connect('key-press-event', self
.mini_key_press
)
204 self
.mini_entry
.connect('changed', self
.mini_changed
)
206 self
.connect('destroy', self
.destroyed
)
208 self
.connect('delete-event', self
.delete_event
)
209 self
.text
.grab_focus()
210 self
.text
.connect('key-press-event', self
.key_press
)
212 def update_current_line(*unused
):
213 cursor
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
214 bound
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
215 if cursor
.compare(bound
) == 0:
216 n_lines
= self
.buffer.get_line_count()
217 self
.status_label
.set_text(_('Line %s of %d') % (cursor
.get_line() + 1, n_lines
))
219 n_lines
= abs(cursor
.get_line() - bound
.get_line()) + 1
221 n_chars
= abs(cursor
.get_line_offset() - bound
.get_line_offset())
223 bytes
= to_utf8(self
.buffer.get_text(cursor
, bound
, False))[0]
224 self
.status_label
.set_text(_('One character selected (%s)') %
225 ' '.join(map(lambda x
: '0x%2x' % ord(x
), bytes
)))
227 self
.status_label
.set_text('%d characters selected' % n_chars
)
229 self
.status_label
.set_text('%d lines selected' % n_lines
)
230 self
.buffer.connect('mark-set', update_current_line
)
231 self
.buffer.connect('changed', update_current_line
)
235 self
.uri
= os
.path
.abspath(filename
)
240 # Loading might take a while, so get something on the screen
246 self
.load_file(filename
)
248 self
.save_mode
= os
.stat(filename
).st_mode
253 self
.buffer.connect('modified-changed', self
.update_title
)
254 self
.buffer.set_modified(FALSE
)
256 def button_press(text
, event
):
257 if event
.button
!= 3:
259 menu
.popup(self
, event
)
261 self
.text
.connect('button-press-event', button_press
)
262 self
.text
.connect('popup-menu', lambda text
: menu
.popup(self
, None))
264 menu
.attach(self
, self
)
265 self
.buffer.place_cursor(self
.buffer.get_start_iter())
266 self
.buffer.start_undo_history()
268 def key_press(self
, text
, kev
):
269 if kev
.keyval
!= g
.keysyms
.Return
and kev
.keyval
!= g
.keysyms
.KP_Enter
:
271 if not auto_indent
.int_value
:
273 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
275 start
.set_line_offset(0)
276 end
.forward_to_line_end()
277 line
= self
.buffer.get_text(start
, end
, False)
284 self
.buffer.begin_user_action()
285 self
.buffer.insert_at_cursor('\n' + indent
)
286 self
.buffer.end_user_action()
289 def destroyed(self
, widget
):
290 app_options
.remove_notify(self
.update_styles
)
292 def update_styles(self
):
295 font
= pango
.FontDescription(default_font
.value
)
296 bg
= g
.gdk
.color_parse(background_colour
.value
)
297 fg
= g
.gdk
.color_parse(foreground_colour
.value
)
299 self
.text
.set_left_margin(layout_left_margin
.int_value
)
300 self
.text
.set_right_margin(layout_right_margin
.int_value
)
302 self
.text
.set_pixels_above_lines(layout_before_para
.int_value
)
303 self
.text
.set_pixels_below_lines(layout_after_para
.int_value
)
304 self
.text
.set_pixels_inside_wrap(layout_inside_para
.int_value
)
305 self
.text
.set_indent(layout_indent_para
.int_value
)
307 rox
.report_exception()
309 self
.text
.modify_font(font
)
310 self
.text
.modify_base(g
.STATE_NORMAL
, bg
)
311 self
.text
.modify_text(g
.STATE_NORMAL
, fg
)
313 def cut(self
): self
.text
.emit('cut_clipboard')
314 def copy(self
): self
.text
.emit('copy_clipboard')
315 def paste(self
): self
.text
.emit('paste_clipboard')
317 def delete_event(self
, window
, event
):
318 if self
.buffer.get_modified():
319 self
.save(discard
= 1)
323 def update_title(self
, *unused
):
324 title
= self
.uri
or '<Untitled>'
325 if self
.buffer.get_modified():
327 self
.set_title(title
)
329 def xds_load_from_stream(self
, name
, t
, stream
):
330 if t
== 'UTF8_STRING':
331 return # Gtk will handle it
333 self
.insert_data(stream
.read())
337 def get_encoding(self
, message
):
338 "Returns (encoding, errors), or raises Abort to cancel."
339 box
= g
.MessageDialog(self
, 0, g
.MESSAGE_QUESTION
, g
.BUTTONS_CANCEL
, message
)
340 box
.set_has_separator(FALSE
)
343 box
.vbox
.pack_start(frame
, TRUE
, TRUE
)
344 frame
.set_border_width(6)
346 hbox
= g
.HBox(FALSE
, 4)
347 hbox
.set_border_width(6)
349 hbox
.pack_start(g
.Label('Encoding:'), FALSE
, TRUE
, 0)
351 combo
.disable_activate()
352 combo
.entry
.connect('activate', lambda w
: box
.activate_default())
353 combo
.set_popdown_strings(known_codecs
)
354 hbox
.pack_start(combo
, TRUE
, TRUE
, 0)
355 ignore_errors
= g
.CheckButton('Ignore errors')
356 hbox
.pack_start(ignore_errors
, FALSE
, TRUE
)
361 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
362 box
.set_default_response(g
.RESPONSE_YES
)
365 combo
.entry
.grab_focus()
368 if resp
!= g
.RESPONSE_YES
:
372 if ignore_errors
.get_active():
376 encoding
= combo
.entry
.get_text()
378 codecs
.getdecoder(encoding
)
381 rox
.alert(_("Unknown encoding '%s'") % encoding
)
385 return encoding
, errors
387 def insert_data(self
, data
):
392 decoder
= codecs
.getdecoder(encoding
)
394 data
= decoder(data
, errors
)[0]
395 if errors
== 'strict':
396 assert '\0' not in data
399 data
= data
.replace('\0', '\\0')
404 encoding
, errors
= self
.get_encoding(
405 _("Data is not valid %s. Please select the file's encoding. "
406 "Turn on 'ignore errors' to try and load it anyway.")
409 self
.buffer.begin_user_action()
410 self
.buffer.insert_at_cursor(data
)
411 self
.buffer.end_user_action()
414 def load_file(self
, path
):
419 file = open(path
, 'r')
420 contents
= file.read()
423 self
.insert_data(contents
)
427 rox
.report_exception()
430 def close(self
, button
= None):
431 if self
.buffer.get_modified():
432 self
.save(discard
= 1)
439 def up(self
, button
= None):
441 filer
.show_file(self
.uri
)
443 rox
.alert(_('File is not saved to disk yet'))
445 def diff(self
, button
= None, path
= None):
446 path
= path
or self
.uri
448 rox
.alert(_('This file has never been saved; nothing to compare it to!\n'
449 'Note: you can drop a file onto the toolbar button to see '
450 'the changes from that file.'))
452 diff
.show_diff(path
, self
.save_to_stream
)
454 def has_selection(self
):
455 s
, e
= self
.get_selection_range()
456 return not e
.equal(s
)
458 def get_marked_range(self
):
459 s
= self
.buffer.get_iter_at_mark(self
.mark_start
)
460 e
= self
.buffer.get_iter_at_mark(self
.mark_end
)
465 def get_selection_range(self
):
466 s
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
467 e
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
472 def save(self
, widget
= None, discard
= 0):
473 from rox
.saving
import SaveBox
476 self
.savebox
.destroy()
478 if self
.has_selection() and not discard
:
479 saver
= SelectionSaver(self
)
480 self
.savebox
= SaveBox(saver
, 'Selection', 'text/plain')
481 self
.savebox
.connect('destroy', lambda w
: saver
.destroy())
483 uri
= self
.uri
or 'TextFile'
484 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
487 def help(self
, button
= None):
488 filer
.open_dir(os
.path
.join(rox
.app_dir
, 'Help'))
490 def save_to_stream(self
, stream
):
491 s
= self
.buffer.get_start_iter()
492 e
= self
.buffer.get_end_iter()
493 stream
.write(self
.buffer.get_text(s
, e
, TRUE
))
495 def set_uri(self
, uri
):
497 self
.buffer.set_modified(FALSE
)
503 def change_font(self
):
504 style
= self
.text
.get_style().copy()
505 style
.font
= load_font(options
.get('edit_font'))
506 self
.text
.set_style(style
)
508 def show_options(self
):
511 def set_marked(self
, start
= None, end
= None):
512 "Set the marked region (from the selection if no region is given)."
514 assert not self
.marked
521 start
, end
= self
.get_selection_range()
522 buffer.move_mark(self
.mark_start
, start
)
523 buffer.move_mark(self
.mark_end
, end
)
524 buffer.apply_tag_by_name('marked',
525 buffer.get_iter_at_mark(self
.mark_start
),
526 buffer.get_iter_at_mark(self
.mark_end
))
529 def clear_marked(self
):
534 buffer.remove_tag_by_name('marked',
535 buffer.get_iter_at_mark(self
.mark_start
),
536 buffer.get_iter_at_mark(self
.mark_end
))
538 def undo(self
, widget
= None):
541 def redo(self
, widget
= None):
544 def goto(self
, widget
= None):
545 from goto
import Goto
546 self
.set_minibuffer(Goto())
548 def search(self
, widget
= None):
549 from search
import Search
550 self
.set_minibuffer(Search())
552 def search_replace(self
, widget
= None):
553 from search
import Replace
556 def set_mini_label(self
, label
):
557 self
.mini_label
.set_text(label
)
559 def set_minibuffer(self
, minibuffer
):
560 assert minibuffer
is None or isinstance(minibuffer
, Minibuffer
)
562 self
.minibuffer
= None
565 self
.mini_entry
.set_text('')
566 self
.minibuffer
= minibuffer
567 minibuffer
.setup(self
)
568 self
.mini_entry
.grab_focus()
569 self
.mini_hbox
.show_all()
571 self
.mini_hbox
.hide()
572 self
.text
.grab_focus()
574 def mini_key_press(self
, entry
, kev
):
575 if kev
.keyval
== g
.keysyms
.Escape
:
576 self
.set_minibuffer(None)
578 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
579 self
.minibuffer
.activate()
582 return self
.minibuffer
.key_press(kev
)
584 def mini_changed(self
, entry
):
585 if not self
.minibuffer
:
587 self
.minibuffer
.changed()
589 def mini_show_info(self
, *unused
):
590 assert self
.minibuffer
592 self
.info_box
.destroy()
593 self
.info_box
= g
.MessageDialog(self
, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
,
594 self
.minibuffer
.info
)
595 self
.info_box
.set_title(_('Minibuffer help'))
598 self
.info_box
.connect('destroy', destroy
)
600 self
.info_box
.connect('response', lambda w
, r
: w
.destroy())
602 def process_selected(self
, process
):
603 """Calls process(line) on each line in the selection, or each line in the file
604 if there is no selection. If the result is not None, the text is replaced."""
605 self
.buffer.begin_user_action()
606 self
._process
_selected
(process
)
607 self
.buffer.end_user_action()
609 def _process_selected(self
, process
):
610 if self
.has_selection():
612 start
, end
= self
.get_selection_range()
613 if start
.compare(end
) > 0:
616 start
, end
= self
.get_selection_range()
617 if start
.compare(end
) > 0:
621 return self
.buffer.get_end_iter()
622 start
= self
.buffer.get_start_iter()
625 while start
.compare(end
) <= 0:
626 line_end
= start
.copy()
627 line_end
.forward_to_line_end()
628 if line_end
.compare(end
) >= 0:
630 line
= self
.buffer.get_text(start
, line_end
, False)
633 self
.buffer.move_mark(self
.mark_tmp
, start
)
634 self
.buffer.insert(line_end
, new
)
635 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
636 line_end
= start
.copy()
637 line_end
.forward_chars(len(line
))
638 self
.buffer.delete(start
, line_end
)
640 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
642 if not start
.forward_line(): break
644 class SelectionSaver(Saveable
):
645 def __init__(self
, window
):
649 def save_to_stream(self
, stream
):
650 s
, e
= self
.window
.get_marked_range()
651 stream
.write(self
.window
.buffer.get_text(s
, e
, TRUE
))
654 # Called when savebox is remove. Get rid of the selection marker
655 self
.window
.clear_marked()