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')
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)
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
),
42 Action(_('New'), 'new', '', g
.STOCK_NEW
)]),
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
),
49 Action(_('Undo'), 'undo', '<Ctrl>Z', g
.STOCK_UNDO
),
50 Action(_('Redo'), 'redo', '<Ctrl>Y', g
.STOCK_REDO
),
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
),
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",
73 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
74 "mbcs", "quopri_codec", "raw_unicode_escape",
76 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
80 class Abort(Exception):
85 """Called when the minibuffer is opened."""
87 def key_press(self
, kev
):
88 """A keypress event in the minibuffer text entry."""
91 """The minibuffer text has changed."""
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'])
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'))
109 shutil
.copyfileobj(stream
, tmp
)
111 self
.window
.diff(path
= tmp
.name
)
113 class EditWindow(rox
.Window
, XDSLoader
, Saveable
):
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)
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')
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 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,
171 lambda button
: self
.set_word_wrap(button
.get_active()),
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)
192 # Create the minibuffer
193 self
.mini_hbox
= g
.HBox(False)
195 info
.set_relief(g
.RELIEF_NONE
)
196 info
.unset_flags(g
.CAN_FOCUS
)
198 image
.set_from_stock(g
.STOCK_DIALOG_INFO
, size
= g
.ICON_SIZE_SMALL_TOOLBAR
)
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
))
225 n_lines
= abs(cursor
.get_line() - bound
.get_line()) + 1
227 n_chars
= abs(cursor
.get_line_offset() - bound
.get_line_offset())
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
)))
233 self
.status_label
.set_text(_('%d characters selected') % n_chars
)
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
)
241 self
.uri
= os
.path
.abspath(filename
)
246 # Loading might take a while, so get something on the screen
252 self
.load_file(filename
)
254 self
.save_last_stat
= os
.stat(filename
)
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:
265 menu
.popup(self
, event
)
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
:
277 if not auto_indent
.int_value
:
279 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
281 start
.set_line_offset(0)
282 end
.forward_to_line_end()
283 line
= self
.buffer.get_text(start
, end
, False)
290 self
.buffer.begin_user_action()
291 self
.buffer.insert_at_cursor('\n' + indent
)
292 self
.buffer.end_user_action()
295 def destroyed(self
, widget
):
296 app_options
.remove_notify(self
.update_styles
)
298 def update_styles(self
):
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
)
315 rox
.report_exception()
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)
331 def update_title(self
, *unused
):
332 title
= self
.uri
or '<Untitled>'
333 if self
.buffer.get_modified():
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
341 self
.insert_data(stream
.read())
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)
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)
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)
369 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
370 box
.set_default_response(g
.RESPONSE_YES
)
373 combo
.entry
.grab_focus()
376 if resp
!= g
.RESPONSE_YES
:
380 if ignore_errors
.get_active():
384 encoding
= combo
.entry
.get_text()
386 codecs
.getdecoder(encoding
)
389 rox
.alert(_("Unknown encoding '%s'") % encoding
)
393 return encoding
, errors
395 def insert_data(self
, data
):
400 decoder
= codecs
.getdecoder(encoding
)
402 data
= decoder(data
, errors
)[0]
403 if errors
== 'strict':
404 assert '\0' not in data
407 data
= data
.replace('\0', '\\0')
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.")
417 self
.buffer.begin_user_action()
418 self
.buffer.insert_at_cursor(data
)
419 self
.buffer.end_user_action()
422 def load_file(self
, path
):
427 file = open(path
, 'r')
428 contents
= file.read()
431 self
.insert_data(contents
)
435 rox
.report_exception()
438 def close(self
, button
= None):
439 if self
.buffer.get_modified():
440 self
.save(discard
= 1)
447 def up(self
, button
= None):
449 filer
.show_file(self
.uri
)
451 rox
.alert(_('File is not saved to disk yet'))
453 def diff(self
, button
= None, path
= None):
454 path
= path
or self
.uri
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.'))
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
)
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
)
480 def save(self
, widget
= None, discard
= 0):
481 from rox
.saving
import 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())
491 uri
= self
.uri
or 'TextFile'
492 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
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
):
505 self
.buffer.set_modified(False)
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
):
519 def set_marked(self
, start
= None, end
= None):
520 "Set the marked region (from the selection if no region is given)."
522 assert not self
.marked
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
))
537 def clear_marked(self
):
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):
549 def redo(self
, widget
= None):
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
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
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()
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)
586 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
587 self
.minibuffer
.activate()
590 return self
.minibuffer
.key_press(kev
)
592 def mini_changed(self
, entry
):
593 if not self
.minibuffer
:
595 self
.minibuffer
.changed()
597 def mini_show_info(self
, *unused
):
598 assert self
.minibuffer
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'))
606 self
.info_box
.connect('destroy', destroy
)
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():
620 start
, end
= self
.get_selection_range()
621 if start
.compare(end
) > 0:
624 start
, end
= self
.get_selection_range()
625 if start
.compare(end
) > 0:
629 return self
.buffer.get_end_iter()
630 start
= self
.buffer.get_start_iter()
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:
638 line
= self
.buffer.get_text(start
, line_end
, False)
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
)
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
)
656 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
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
):
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))
672 # Called when savebox is remove. Get rid of the selection marker
673 self
.window
.clear_marked()