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
14 from rox
.Menu
import Menu
, set_save_name
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')
23 layout_left_margin
= Option('layout_left_margin', 2)
24 layout_right_margin
= Option('layout_right_margin', 4)
26 layout_before_para
= Option('layout_before_para', 0)
27 layout_after_para
= Option('layout_after_para', 0)
28 layout_inside_para
= Option('layout_inside_para', 0)
29 layout_indent_para
= Option('layout_indent_para', 2)
35 (_('/File'), '', '<Branch>'),
36 (_('/File/') + _('Save'), 'save', '<StockItem>', '<Ctrl>S', g
.STOCK_SAVE
),
37 (_('/File/') + _('Open Parent'), 'up', '<StockItem>', '', g
.STOCK_GO_UP
),
38 (_('/File/') + _('Show Changes'), 'diff', '<StockItem>', '', 'rox-diff'),
39 (_('/File/') + _('Close'), 'close', '<StockItem>', '', g
.STOCK_CLOSE
),
40 (_('/File/'), '', '<Separator>'),
41 (_('/File/') + _('New'), 'new', '<StockItem>', '', g
.STOCK_NEW
),
43 (_('/Edit'), '', '<Branch>'),
44 (_('/Edit/') + _('Cut'), 'cut', '<StockItem>', '<Ctrl>X', g
.STOCK_CUT
),
45 (_('/Edit/') + _('Copy'), 'copy', '<StockItem>', '<Ctrl>C', g
.STOCK_COPY
),
46 (_('/Edit/') + _('Paste'), 'paste', '<StockItem>', '<Ctrl>V', g
.STOCK_PASTE
),
47 (_('/Edit/'), '', '<Separator>'),
48 (_('/Edit/') + _('Undo'), 'undo', '<StockItem>', '<Ctrl>Z', g
.STOCK_UNDO
),
49 (_('/Edit/') + _('Redo'), 'redo', '<StockItem>', '<Ctrl>Y', g
.STOCK_REDO
),
50 (_('/Edit/'), '', '<Separator>'),
51 (_('/Edit/') + _('Search...'), 'search', '<StockItem>', 'F4', g
.STOCK_FIND
),
52 (_('/Edit/') + _('Search and Replace....'), 'search_replace',
53 '<StockItem>', '<Ctrl>F4', g
.STOCK_FIND_AND_REPLACE
),
54 (_('/Edit/') + _('Goto line...'), 'goto', '<StockItem>', 'F5', g
.STOCK_JUMP_TO
),
56 (_('/Options'), 'show_options', '<StockItem>', '', g
.STOCK_PROPERTIES
),
57 (_('/Help'), 'help', '<StockItem>', 'F1', g
.STOCK_HELP
),
60 rox
.croak(_('Edit requires ROX-Lib2 1.9.8 or later'))
63 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
64 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
65 "iso8859_13", "iso8859_14", "iso8859_15",
66 "ascii", "base64_codec", "charmap",
67 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
68 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
69 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
70 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
71 "cp869", "cp874", "cp875", "hex_codec",
74 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
75 "mbcs", "quopri_codec", "raw_unicode_escape",
77 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
81 class Abort(Exception):
86 """Called when the minibuffer is opened."""
88 def key_press(self
, kev
):
89 """A keypress event in the minibuffer text entry."""
92 """The minibuffer text has changed."""
95 """Return or Enter pressed."""
97 info
= 'Press Escape to close the minibuffer.'
99 class DiffLoader(XDSLoader
):
100 def __init__(self
, window
):
101 XDSLoader
.__init
__(self
, ['text/plain'])
104 def xds_load_from_file(self
, path
):
105 self
.window
.diff(path
= path
)
107 def xds_load_from_stream(self
, name
, type, stream
):
108 tmp
= diff
.Tmp(suffix
= '-' + (name
or 'tmp'))
110 shutil
.copyfileobj(stream
, tmp
)
112 self
.window
.diff(path
= tmp
.name
)
114 class EditWindow(rox
.Window
, XDSLoader
, Saveable
):
115 def __init__(self
, filename
= None):
116 rox
.Window
.__init
__(self
)
117 XDSLoader
.__init
__(self
, ['text/plain', 'UTF8_STRING'])
118 self
.set_default_size(g
.gdk
.screen_width() * 2 / 3,
119 g
.gdk
.screen_height() / 2)
124 app_options
.add_notify(self
.update_styles
)
126 self
.buffer = Buffer()
128 self
.text
= g
.TextView()
129 self
.text
.set_buffer(self
.buffer)
130 self
.text
.set_size_request(10, 10)
131 self
.xds_proxy_for(self
.text
)
132 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
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')
145 # When searching, this is where the cursor was when the minibuffer
147 start
= self
.buffer.get_start_iter()
148 self
.search_base
= self
.buffer.create_mark('search_base', start
, TRUE
)
154 tools
.set_style(g
.TOOLBAR_ICONS
)
155 vbox
.pack_start(tools
, FALSE
, TRUE
, 0)
158 tools
.insert_stock(g
.STOCK_HELP
, _('Help'), None, self
.help, None, 0)
159 diff
= tools
.insert_stock('rox-diff', _('Show changes from saved copy.\n'
160 'Or, drop a backup file onto this button to see changes from that.'),
161 None, self
.diff
, None, 0)
162 DiffLoader(self
).xds_proxy_for(diff
)
163 tools
.insert_stock(g
.STOCK_REDO
, _('Redo'), None, self
.redo
, None, 0)
164 tools
.insert_stock(g
.STOCK_UNDO
, _('Undo'), None, self
.undo
, None, 0)
165 tools
.insert_stock(g
.STOCK_FIND_AND_REPLACE
, _('Replace'), None, self
.search_replace
, None, 0)
166 tools
.insert_stock(g
.STOCK_FIND
, _('Search'), None, self
.search
, None, 0)
167 tools
.insert_stock(g
.STOCK_SAVE
, _('Save'), None, self
.save
, None, 0)
168 tools
.insert_stock(g
.STOCK_GO_UP
, _('Up'), None, self
.up
, None, 0)
169 tools
.insert_stock(g
.STOCK_CLOSE
, _('Close'), None, self
.close
, None, 0)
171 swin
= g
.ScrolledWindow()
172 swin
.set_policy(g
.POLICY_NEVER
, g
.POLICY_AUTOMATIC
)
173 vbox
.pack_start(swin
, True, True)
179 # Create the minibuffer
180 self
.mini_hbox
= g
.HBox(FALSE
)
182 info
.set_relief(g
.RELIEF_NONE
)
183 info
.unset_flags(g
.CAN_FOCUS
)
185 image
.set_from_stock(g
.STOCK_DIALOG_INFO
, size
= g
.ICON_SIZE_SMALL_TOOLBAR
)
188 info
.connect('clicked', self
.mini_show_info
)
190 self
.mini_hbox
.pack_start(info
, FALSE
, TRUE
, 0)
191 self
.mini_label
= g
.Label('')
192 self
.mini_hbox
.pack_start(self
.mini_label
, FALSE
, TRUE
, 0)
193 self
.mini_entry
= g
.Entry()
194 self
.mini_hbox
.pack_start(self
.mini_entry
, TRUE
, TRUE
, 0)
195 vbox
.pack_start(self
.mini_hbox
, FALSE
, TRUE
)
196 self
.mini_entry
.connect('key-press-event', self
.mini_key_press
)
197 self
.mini_entry
.connect('changed', self
.mini_changed
)
199 self
.connect('destroy', self
.destroyed
)
201 self
.connect('delete-event', self
.delete_event
)
202 self
.text
.grab_focus()
203 self
.text
.connect('key-press-event', self
.key_press
)
207 self
.uri
= os
.path
.abspath(filename
)
212 # Loading might take a while, so get something on the screen
218 self
.load_file(filename
)
220 self
.save_mode
= os
.stat(filename
).st_mode
225 self
.buffer.connect('modified-changed', self
.update_title
)
226 self
.buffer.set_modified(FALSE
)
228 def button_press(text
, event
):
229 if event
.button
!= 3:
231 menu
.popup(self
, event
)
233 self
.text
.connect('button-press-event', button_press
)
234 self
.text
.connect('popup-menu', lambda text
: menu
.popup(self
, None))
236 menu
.attach(self
, self
)
237 self
.buffer.place_cursor(self
.buffer.get_start_iter())
238 self
.buffer.start_undo_history()
240 def key_press(self
, text
, kev
):
241 if kev
.keyval
!= g
.keysyms
.Return
and kev
.keyval
!= g
.keysyms
.KP_Enter
:
243 if not auto_indent
.int_value
:
245 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
247 start
.set_line_offset(0)
248 end
.forward_to_line_end()
249 line
= self
.buffer.get_text(start
, end
, False)
256 self
.buffer.begin_user_action()
257 self
.buffer.insert_at_cursor('\n' + indent
)
258 self
.buffer.end_user_action()
261 def destroyed(self
, widget
):
262 app_options
.remove_notify(self
.update_styles
)
264 def update_styles(self
):
267 font
= pango
.FontDescription(default_font
.value
)
268 bg
= g
.gdk
.color_parse(background_colour
.value
)
269 fg
= g
.gdk
.color_parse(foreground_colour
.value
)
271 self
.text
.set_left_margin(layout_left_margin
.int_value
)
272 self
.text
.set_right_margin(layout_right_margin
.int_value
)
274 self
.text
.set_pixels_above_lines(layout_before_para
.int_value
)
275 self
.text
.set_pixels_below_lines(layout_after_para
.int_value
)
276 self
.text
.set_pixels_inside_wrap(layout_inside_para
.int_value
)
277 self
.text
.set_indent(layout_indent_para
.int_value
)
279 rox
.report_exception()
281 self
.text
.modify_font(font
)
282 self
.text
.modify_base(g
.STATE_NORMAL
, bg
)
283 self
.text
.modify_text(g
.STATE_NORMAL
, fg
)
285 def cut(self
): self
.text
.emit('cut_clipboard')
286 def copy(self
): self
.text
.emit('copy_clipboard')
287 def paste(self
): self
.text
.emit('paste_clipboard')
289 def delete_event(self
, window
, event
):
290 if self
.buffer.get_modified():
291 self
.save(discard
= 1)
295 def update_title(self
, *unused
):
296 title
= self
.uri
or '<Untitled>'
297 if self
.buffer.get_modified():
299 self
.set_title(title
)
301 def xds_load_from_stream(self
, name
, t
, stream
):
302 if t
== 'UTF8_STRING':
303 return # Gtk will handle it
305 self
.insert_data(stream
.read())
309 def get_encoding(self
, message
):
310 "Returns (encoding, errors), or raises Abort to cancel."
313 box
= g
.MessageDialog(self
, 0, g
.MESSAGE_QUESTION
, g
.BUTTONS_CANCEL
, message
)
314 box
.set_has_separator(FALSE
)
317 box
.vbox
.pack_start(frame
, TRUE
, TRUE
)
318 frame
.set_border_width(6)
320 hbox
= g
.HBox(FALSE
, 4)
321 hbox
.set_border_width(6)
323 hbox
.pack_start(g
.Label('Encoding:'), FALSE
, TRUE
, 0)
325 combo
.disable_activate()
326 combo
.entry
.connect('activate', lambda w
: box
.activate_default())
327 combo
.set_popdown_strings(known_codecs
)
328 hbox
.pack_start(combo
, TRUE
, TRUE
, 0)
329 ignore_errors
= g
.CheckButton('Ignore errors')
330 hbox
.pack_start(ignore_errors
, FALSE
, TRUE
)
335 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
336 box
.set_default_response(g
.RESPONSE_YES
)
339 combo
.entry
.grab_focus()
342 if resp
!= g
.RESPONSE_YES
:
346 if ignore_errors
.get_active():
350 encoding
= combo
.entry
.get_text()
352 codecs
.getdecoder(encoding
)
355 rox
.alert(_("Unknown encoding '%s'") % encoding
)
359 return encoding
, errors
361 def insert_data(self
, data
):
366 decoder
= codecs
.getdecoder(encoding
)
368 data
= decoder(data
, errors
)[0]
369 if errors
== 'strict':
370 assert '\0' not in data
373 data
= data
.replace('\0', '\\0')
378 encoding
, errors
= self
.get_encoding(
379 _("Data is not valid %s. Please select the file's encoding. "
380 "Turn on 'ignore errors' to try and load it anyway.")
383 self
.buffer.begin_user_action()
384 self
.buffer.insert_at_cursor(data
)
385 self
.buffer.end_user_action()
388 def load_file(self
, path
):
393 file = open(path
, 'r')
394 contents
= file.read()
397 self
.insert_data(contents
)
401 rox
.report_exception()
404 def close(self
, button
= None):
405 if self
.buffer.get_modified():
406 self
.save(discard
= 1)
413 def up(self
, button
= None):
415 filer
.show_file(self
.uri
)
417 rox
.alert(_('File is not saved to disk yet'))
419 def diff(self
, button
= None, path
= None):
420 path
= path
or self
.uri
422 rox
.alert(_('This file has never been saved; nothing to compare it to!\n'
423 'Note: you can drop a file onto the toolbar button to see '
424 'the changes from that file.'))
426 diff
.show_diff(path
, self
.save_to_stream
)
428 def has_selection(self
):
429 s
, e
= self
.get_selection_range()
430 return not e
.equal(s
)
432 def get_marked_range(self
):
433 s
= self
.buffer.get_iter_at_mark(self
.mark_start
)
434 e
= self
.buffer.get_iter_at_mark(self
.mark_end
)
439 def get_selection_range(self
):
440 s
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
441 e
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
446 def save(self
, widget
= None, discard
= 0):
447 from rox
.saving
import SaveBox
450 self
.savebox
.destroy()
452 if self
.has_selection() and not discard
:
453 saver
= SelectionSaver(self
)
454 self
.savebox
= SaveBox(saver
, 'Selection', 'text/plain')
455 self
.savebox
.connect('destroy', lambda w
: saver
.destroy())
457 uri
= self
.uri
or 'TextFile'
458 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
461 def help(self
, button
= None):
462 filer
.open_dir(os
.path
.join(rox
.app_dir
, 'Help'))
464 def save_to_stream(self
, stream
):
465 s
= self
.buffer.get_start_iter()
466 e
= self
.buffer.get_end_iter()
467 stream
.write(self
.buffer.get_text(s
, e
, TRUE
))
469 def set_uri(self
, uri
):
471 self
.buffer.set_modified(FALSE
)
477 def change_font(self
):
478 style
= self
.text
.get_style().copy()
479 style
.font
= load_font(options
.get('edit_font'))
480 self
.text
.set_style(style
)
482 def show_options(self
):
485 def set_marked(self
, start
= None, end
= None):
486 "Set the marked region (from the selection if no region is given)."
488 assert not self
.marked
495 start
, end
= self
.get_selection_range()
496 buffer.move_mark(self
.mark_start
, start
)
497 buffer.move_mark(self
.mark_end
, end
)
498 buffer.apply_tag_by_name('marked',
499 buffer.get_iter_at_mark(self
.mark_start
),
500 buffer.get_iter_at_mark(self
.mark_end
))
503 def clear_marked(self
):
508 buffer.remove_tag_by_name('marked',
509 buffer.get_iter_at_mark(self
.mark_start
),
510 buffer.get_iter_at_mark(self
.mark_end
))
512 def undo(self
, widget
= None):
515 def redo(self
, widget
= None):
518 def goto(self
, widget
= None):
519 from goto
import Goto
520 self
.set_minibuffer(Goto())
522 def search(self
, widget
= None):
523 from search
import Search
524 self
.set_minibuffer(Search())
526 def search_replace(self
, widget
= None):
527 from search
import Replace
530 def set_mini_label(self
, label
):
531 self
.mini_label
.set_text(label
)
533 def set_minibuffer(self
, minibuffer
):
534 assert minibuffer
is None or isinstance(minibuffer
, Minibuffer
)
536 self
.minibuffer
= None
539 self
.mini_entry
.set_text('')
540 self
.minibuffer
= minibuffer
541 minibuffer
.setup(self
)
542 self
.mini_entry
.grab_focus()
543 self
.mini_hbox
.show_all()
545 self
.mini_hbox
.hide()
546 self
.text
.grab_focus()
548 def mini_key_press(self
, entry
, kev
):
549 if kev
.keyval
== g
.keysyms
.Escape
:
550 self
.set_minibuffer(None)
552 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
553 self
.minibuffer
.activate()
556 return self
.minibuffer
.key_press(kev
)
558 def mini_changed(self
, entry
):
559 if not self
.minibuffer
:
561 self
.minibuffer
.changed()
563 def mini_show_info(self
, *unused
):
564 assert self
.minibuffer
566 self
.info_box
.destroy()
567 self
.info_box
= g
.MessageDialog(self
, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
,
568 self
.minibuffer
.info
)
569 self
.info_box
.set_title(_('Minibuffer help'))
572 self
.info_box
.connect('destroy', destroy
)
574 self
.info_box
.connect('response', lambda w
, r
: w
.destroy())
576 def process_selected(self
, process
):
577 """Calls process(line) on each line in the selection, or each line in the file
578 if there is no selection. If the result is not None, the text is replaced."""
579 self
.buffer.begin_user_action()
580 self
._process
_selected
(process
)
581 self
.buffer.end_user_action()
583 def _process_selected(self
, process
):
584 if self
.has_selection():
586 start
, end
= self
.get_selection_range()
587 if start
.compare(end
) > 0:
590 start
, end
= self
.get_selection_range()
591 if start
.compare(end
) > 0:
595 return self
.buffer.get_end_iter()
596 start
= self
.buffer.get_start_iter()
599 while start
.compare(end
) <= 0:
600 line_end
= start
.copy()
601 line_end
.forward_to_line_end()
602 if line_end
.compare(end
) >= 0:
604 line
= self
.buffer.get_text(start
, line_end
, False)
607 self
.buffer.move_mark(self
.mark_tmp
, start
)
608 self
.buffer.insert(line_end
, new
)
609 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
610 line_end
= start
.copy()
611 line_end
.forward_chars(len(line
))
612 self
.buffer.delete(start
, line_end
)
614 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
616 if not start
.forward_line(): break
618 class SelectionSaver(Saveable
):
619 def __init__(self
, window
):
623 def save_to_stream(self
, stream
):
624 s
, e
= self
.window
.get_marked_range()
625 stream
.write(self
.window
.buffer.get_text(s
, e
, TRUE
))
628 # Called when savebox is remove. Get rid of the selection marker
629 self
.window
.clear_marked()