2 from rox
import g
, filer
, app_options
4 from rox
.loading
import XDSLoader
5 from rox
.options
import Option
6 from rox
.saving
import Saveable
11 # WARNING: This is a temporary hack, until we write a way choose between
12 # the two ways of doing toolbars or we abandon the old method entirely
14 warnings
.filterwarnings('ignore', category
=DeprecationWarning,
18 to_utf8
= codecs
.getencoder('utf-8')
20 from buffer import Buffer
, have_sourceview
21 from rox
.Menu
import Menu
, set_save_name
, SubMenu
, Separator
, Action
, ToggleItem
23 default_font
= Option('default_font', 'serif')
25 background_colour
= Option('background', '#fff')
26 foreground_colour
= Option('foreground', '#000')
28 auto_indent
= Option('autoindent', '1')
29 word_wrap
= Option('wordwrap', '1')
31 layout_left_margin
= Option('layout_left_margin', 2)
32 layout_right_margin
= Option('layout_right_margin', 4)
34 layout_before_para
= Option('layout_before_para', 0)
35 layout_after_para
= Option('layout_after_para', 0)
36 layout_inside_para
= Option('layout_inside_para', 0)
37 layout_indent_para
= Option('layout_indent_para', 2)
39 show_toolbar
= Option('show_toolbar', 1)
45 Action(_('Save As...'), 'save_as', '<Ctrl>S', g
.STOCK_SAVE
),
46 Action(_('Open Parent'), 'up', '', g
.STOCK_GO_UP
),
47 Action(_('Show Changes'), 'diff', '', 'rox-diff'),
48 ToggleItem(_('Word Wrap'), 'word_wrap'),
49 Action(_('Close'), 'close', '', g
.STOCK_CLOSE
),
51 Action(_('New'), 'new', '', g
.STOCK_NEW
)]),
54 Action(_('Cut'), 'cut', '<Ctrl>X', g
.STOCK_CUT
),
55 Action(_('Copy'), 'copy', '<Ctrl>C', g
.STOCK_COPY
),
56 Action(_('Paste'), 'paste', '<Ctrl>V', g
.STOCK_PASTE
),
58 Action(_('Undo'), 'undo', '<Ctrl>Z', g
.STOCK_UNDO
),
59 Action(_('Redo'), 'redo', '<Ctrl>Y', g
.STOCK_REDO
),
61 Action(_('Search...'), 'search', 'F4', g
.STOCK_FIND
),
62 Action(_('Search and Replace....'), 'search_replace',
63 '<Ctrl>F4', g
.STOCK_FIND_AND_REPLACE
),
64 Action(_('Goto line...'), 'goto', 'F5', g
.STOCK_JUMP_TO
)]),
66 Action(_('Options'), 'show_options', '', g
.STOCK_PROPERTIES
),
67 Action(_('Help'), 'help', 'F1', g
.STOCK_HELP
),
71 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
72 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
73 "iso8859_13", "iso8859_14", "iso8859_15",
74 "ascii", "base64_codec", "charmap",
75 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
76 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
77 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
78 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
79 "cp869", "cp874", "cp875", "hex_codec",
82 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
83 "mbcs", "quopri_codec", "raw_unicode_escape",
85 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
89 class Abort(Exception):
94 """Called when the minibuffer is opened."""
96 def key_press(self
, kev
):
97 """A keypress event in the minibuffer text entry."""
100 """The minibuffer text has changed."""
103 """Return or Enter pressed."""
105 info
= 'Press Escape to close the minibuffer.'
107 class DiffLoader(XDSLoader
):
108 def __init__(self
, window
):
109 XDSLoader
.__init
__(self
, ['text/plain'])
112 def xds_load_from_file(self
, path
):
113 self
.window
.diff(path
= path
)
115 def xds_load_from_stream(self
, name
, type, stream
):
116 tmp
= diff
.Tmp(suffix
= '-' + (name
or 'tmp'))
118 shutil
.copyfileobj(stream
, tmp
)
120 self
.window
.diff(path
= tmp
.name
)
122 class EditWindow(rox
.Window
, XDSLoader
, Saveable
):
126 def __init__(self
, filename
= None, show
= True):
127 rox
.Window
.__init
__(self
)
128 XDSLoader
.__init
__(self
, ['text/plain', 'UTF8_STRING'])
129 self
.set_default_size(g
.gdk
.screen_width() * 2 / 3,
130 g
.gdk
.screen_height() / 2)
135 app_options
.add_notify(self
.update_styles
)
139 self
.uri
= os
.path
.abspath(filename
)
143 self
.buffer = Buffer()
147 self
.text
= gtksourceview
.SourceView(self
.buffer)
148 self
.text
.set_show_line_numbers(True)
149 self
.text
.set_show_line_markers(True)
150 self
.text
.set_auto_indent(True)
151 self
.text
.set_smart_home_end(True)
154 self
.buffer.set_type(mime
.get_type(self
.uri
, 1))
156 self
.text
= g
.TextView()
157 self
.text
.set_buffer(self
.buffer)
161 self
.text
.set_size_request(10, 10)
162 self
.xds_proxy_for(self
.text
)
164 self
.insert_mark
= self
.buffer.get_mark('insert')
165 self
.selection_bound_mark
= self
.buffer.get_mark('selection_bound')
166 start
= self
.buffer.get_start_iter()
167 self
.mark_start
= self
.buffer.create_mark('mark_start', start
, True)
168 self
.mark_end
= self
.buffer.create_mark('mark_end', start
, False)
169 self
.mark_tmp
= self
.buffer.create_mark('mark_tmp', start
, False)
170 tag
= self
.buffer.create_tag('marked')
171 tag
.set_property('background', 'green')
174 # When searching, this is where the cursor was when the minibuffer
176 start
= self
.buffer.get_start_iter()
177 self
.search_base
= self
.buffer.create_mark('search_base', start
, True)
183 tools
.set_style(g
.TOOLBAR_ICONS
)
184 vbox
.pack_start(tools
, False, True, 0)
186 self
.status_label
= g
.Label('')
187 tools
.append_widget(self
.status_label
, None, None)
188 tools
.insert_stock(g
.STOCK_HELP
, _('Help'), None, self
.help, None, 0)
189 diff
= tools
.insert_stock('rox-diff', _('Show changes from saved copy.\n'
190 'Or, drop a backup file onto this button to see changes from that.'),
191 None, self
.diff
, None, 0)
192 DiffLoader(self
).xds_proxy_for(diff
)
194 image_wrap
= g
.Image()
195 image_wrap
.set_from_file(rox
.app_dir
+ '/images/rox-word-wrap.png')
196 self
.wrap_button
= tools
.insert_element(g
.TOOLBAR_CHILD_TOGGLEBUTTON
,
197 None, _("Word Wrap"), _("Word Wrap"), None,
199 lambda button
: self
.set_word_wrap(button
.get_active()),
201 tools
.insert_stock(g
.STOCK_REDO
, _('Redo'), None, self
.redo
, None, 0)
202 tools
.insert_stock(g
.STOCK_UNDO
, _('Undo'), None, self
.undo
, None, 0)
203 tools
.insert_stock(g
.STOCK_FIND_AND_REPLACE
, _('Replace'), None, self
.search_replace
, None, 0)
204 tools
.insert_stock(g
.STOCK_FIND
, _('Search'), None, self
.search
, None, 0)
205 tools
.insert_stock(g
.STOCK_SAVE
, _('Save'), None, self
.save_as
, None, 0)
206 tools
.insert_stock(g
.STOCK_GO_UP
, _('Up'), None, self
.up
, None, 0)
207 tools
.insert_stock(g
.STOCK_CLOSE
, _('Close'), None, self
.close
, None, 0)
208 # Set minimum size to ignore the label
209 tools
.set_size_request(tools
.size_request()[0], -1)
213 swin
= g
.ScrolledWindow()
214 swin
.set_policy(g
.POLICY_AUTOMATIC
, g
.POLICY_AUTOMATIC
)
215 vbox
.pack_start(swin
, True, True)
223 # Create the minibuffer
224 self
.mini_hbox
= g
.HBox(False)
226 info
.set_relief(g
.RELIEF_NONE
)
227 info
.unset_flags(g
.CAN_FOCUS
)
229 image
.set_from_stock(g
.STOCK_DIALOG_INFO
, size
= g
.ICON_SIZE_SMALL_TOOLBAR
)
232 info
.connect('clicked', self
.mini_show_info
)
234 self
.mini_hbox
.pack_start(info
, False, True, 0)
235 self
.mini_label
= g
.Label('')
236 self
.mini_hbox
.pack_start(self
.mini_label
, False, True, 0)
237 self
.mini_entry
= g
.Entry()
238 self
.mini_hbox
.pack_start(self
.mini_entry
, True, True, 0)
239 vbox
.pack_start(self
.mini_hbox
, False, True)
240 self
.mini_entry
.connect('key-press-event', self
.mini_key_press
)
241 self
.mini_entry
.connect('changed', self
.mini_changed
)
243 self
.connect('destroy', self
.destroyed
)
245 self
.connect('delete-event', self
.delete_event
)
246 self
.text
.grab_focus()
247 self
.text
.connect('key-press-event', self
.key_press
)
249 def update_current_line(*unused
):
250 cursor
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
251 bound
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
252 if cursor
.compare(bound
) == 0:
253 n_lines
= self
.buffer.get_line_count()
254 self
.status_label
.set_text(_('Line %s of %d') % (cursor
.get_line() + 1, n_lines
))
256 n_lines
= abs(cursor
.get_line() - bound
.get_line()) + 1
258 n_chars
= abs(cursor
.get_line_offset() - bound
.get_line_offset())
260 bytes
= to_utf8(self
.buffer.get_text(cursor
, bound
, False))[0]
261 self
.status_label
.set_text(_('One character selected (%s)') %
262 ' '.join(map(lambda x
: '0x%2x' % ord(x
), bytes
)))
264 self
.status_label
.set_text(_('%d characters selected') % n_chars
)
266 self
.status_label
.set_text(_('%d lines selected') % n_lines
)
267 self
.buffer.connect('mark-set', update_current_line
)
268 self
.buffer.connect('changed', update_current_line
)
271 # Loading might take a while, so get something on the screen
277 self
.load_file(filename
)
279 self
.save_last_stat
= os
.stat(filename
)
284 self
.buffer.connect('modified-changed', self
.update_title
)
285 self
.buffer.set_modified(False)
287 def button_press(text
, event
):
288 if event
.button
!= 3:
290 menu
.popup(self
, event
)
292 self
.text
.connect('button-press-event', button_press
)
293 self
.text
.connect('popup-menu', lambda text
: menu
.popup(self
, None))
295 menu
.attach(self
, self
)
296 self
.buffer.place_cursor(self
.buffer.get_start_iter())
297 self
.buffer.start_undo_history()
299 def key_press(self
, text
, kev
):
300 if kev
.keyval
!= g
.keysyms
.Return
and kev
.keyval
!= g
.keysyms
.KP_Enter
:
302 if not auto_indent
.int_value
:
304 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
306 start
.set_line_offset(0)
307 end
.forward_to_line_end()
308 line
= self
.buffer.get_text(start
, end
, False)
315 self
.buffer.begin_user_action()
316 self
.buffer.insert_at_cursor('\n' + indent
)
317 self
.buffer.end_user_action()
320 def destroyed(self
, widget
):
321 app_options
.remove_notify(self
.update_styles
)
323 def update_styles(self
):
326 font
= pango
.FontDescription(default_font
.value
)
327 bg
= g
.gdk
.color_parse(background_colour
.value
)
328 fg
= g
.gdk
.color_parse(foreground_colour
.value
)
330 self
.text
.set_left_margin(layout_left_margin
.int_value
)
331 self
.text
.set_right_margin(layout_right_margin
.int_value
)
333 self
.text
.set_pixels_above_lines(layout_before_para
.int_value
)
334 self
.text
.set_pixels_below_lines(layout_after_para
.int_value
)
335 self
.text
.set_pixels_inside_wrap(layout_inside_para
.int_value
)
336 self
.text
.set_indent(layout_indent_para
.int_value
)
338 self
.word_wrap
= bool(word_wrap
.int_value
)
340 if show_toolbar
.int_value
:
345 rox
.report_exception()
347 self
.text
.modify_font(font
)
348 self
.text
.modify_base(g
.STATE_NORMAL
, bg
)
349 self
.text
.modify_text(g
.STATE_NORMAL
, fg
)
351 def cut(self
): self
.text
.emit('cut_clipboard')
352 def copy(self
): self
.text
.emit('copy_clipboard')
353 def paste(self
): self
.text
.emit('paste_clipboard')
355 def delete_event(self
, window
, event
):
356 if self
.buffer.get_modified():
357 self
.save_as(discard
= 1)
361 def update_title(self
, *unused
):
362 title
= self
.uri
or '<Untitled>'
363 if self
.buffer.get_modified():
365 self
.set_title(title
)
367 def xds_load_from_stream(self
, name
, t
, stream
):
368 if t
== 'UTF8_STRING':
369 return # Gtk will handle it
371 self
.insert_data(stream
.read())
375 def get_encoding(self
, message
):
376 "Returns (encoding, errors), or raises Abort to cancel."
377 box
= g
.MessageDialog(self
, 0, g
.MESSAGE_QUESTION
, g
.BUTTONS_CANCEL
, message
)
378 box
.set_has_separator(False)
381 box
.vbox
.pack_start(frame
, True, True)
382 frame
.set_border_width(6)
384 hbox
= g
.HBox(False, 4)
385 hbox
.set_border_width(6)
387 hbox
.pack_start(g
.Label(_('Encoding:')), False, True, 0)
389 combo
.disable_activate()
390 combo
.entry
.connect('activate', lambda w
: box
.activate_default())
391 combo
.set_popdown_strings(known_codecs
)
392 hbox
.pack_start(combo
, True, True, 0)
393 ignore_errors
= g
.CheckButton(_('Ignore errors'))
394 hbox
.pack_start(ignore_errors
, False, True)
399 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
400 box
.set_default_response(g
.RESPONSE_YES
)
403 combo
.entry
.grab_focus()
406 if resp
!= g
.RESPONSE_YES
:
410 if ignore_errors
.get_active():
414 encoding
= combo
.entry
.get_text()
416 codecs
.getdecoder(encoding
)
419 rox
.alert(_("Unknown encoding '%s'") % encoding
)
423 return encoding
, errors
425 def insert_data(self
, data
):
430 decoder
= codecs
.getdecoder(encoding
)
432 data
= decoder(data
, errors
)[0]
433 if errors
== 'strict':
434 assert '\0' not in data
437 data
= data
.replace('\0', '\\0')
442 encoding
, errors
= self
.get_encoding(
443 _("Data is not valid %s. Please select the file's encoding. "
444 "Turn on 'ignore errors' to try and load it anyway.")
447 self
.buffer.begin_user_action()
448 self
.buffer.insert_at_cursor(data
)
449 self
.buffer.end_user_action()
452 def load_file(self
, path
):
457 file = open(path
, 'r')
458 contents
= file.read()
461 self
.insert_data(contents
)
465 rox
.report_exception()
468 def close(self
, button
= None):
469 if self
.buffer.get_modified():
470 self
.save_as(discard
= 1)
477 def up(self
, button
= None):
479 filer
.show_file(self
.uri
)
481 rox
.alert(_('File is not saved to disk yet'))
483 def diff(self
, button
= None, path
= None):
484 path
= path
or self
.uri
486 rox
.alert(_('This file has never been saved; nothing to compare it to!\n'
487 'Note: you can drop a file onto the toolbar button to see '
488 'the changes from that file.'))
490 diff
.show_diff(path
, self
.save_to_stream
)
492 def has_selection(self
):
493 s
, e
= self
.get_selection_range()
494 return not e
.equal(s
)
496 def get_marked_range(self
):
497 s
= self
.buffer.get_iter_at_mark(self
.mark_start
)
498 e
= self
.buffer.get_iter_at_mark(self
.mark_end
)
503 def get_selection_range(self
):
504 s
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
505 e
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
510 def save_as(self
, widget
= None, discard
= 0):
511 from rox
.saving
import SaveBox
514 self
.savebox
.destroy()
516 if self
.has_selection() and not discard
:
517 saver
= SelectionSaver(self
)
518 self
.savebox
= SaveBox(saver
, 'Selection', 'text/plain')
519 self
.savebox
.connect('destroy', lambda w
: saver
.destroy())
521 uri
= self
.uri
or 'TextFile'
522 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
525 def help(self
, button
= None):
526 filer
.open_dir(os
.path
.join(rox
.app_dir
, 'Help'))
528 def save_to_stream(self
, stream
):
529 s
= self
.buffer.get_start_iter()
530 e
= self
.buffer.get_end_iter()
531 stream
.write(self
.buffer.get_text(s
, e
, True))
533 def set_uri(self
, uri
):
535 self
.buffer.set_modified(False)
541 def change_font(self
):
542 style
= self
.text
.get_style().copy()
543 style
.font
= load_font(options
.get('edit_font'))
544 self
.text
.set_style(style
)
546 def show_options(self
):
549 def set_marked(self
, start
= None, end
= None):
550 "Set the marked region (from the selection if no region is given)."
552 assert not self
.marked
559 start
, end
= self
.get_selection_range()
560 buffer.move_mark(self
.mark_start
, start
)
561 buffer.move_mark(self
.mark_end
, end
)
562 buffer.apply_tag_by_name('marked',
563 buffer.get_iter_at_mark(self
.mark_start
),
564 buffer.get_iter_at_mark(self
.mark_end
))
567 def clear_marked(self
):
572 buffer.remove_tag_by_name('marked',
573 buffer.get_iter_at_mark(self
.mark_start
),
574 buffer.get_iter_at_mark(self
.mark_end
))
576 def undo(self
, widget
= None):
579 def redo(self
, widget
= None):
582 def goto(self
, widget
= None):
583 from goto
import Goto
584 self
.set_minibuffer(Goto())
586 def search(self
, widget
= None):
587 from search
import Search
588 self
.set_minibuffer(Search())
590 def search_replace(self
, widget
= None):
591 from search
import Replace
594 def set_mini_label(self
, label
):
595 self
.mini_label
.set_text(label
)
597 def set_minibuffer(self
, minibuffer
):
598 assert minibuffer
is None or isinstance(minibuffer
, Minibuffer
)
600 self
.minibuffer
= None
603 self
.mini_entry
.set_text('')
604 self
.minibuffer
= minibuffer
605 minibuffer
.setup(self
)
606 self
.mini_entry
.grab_focus()
607 self
.mini_hbox
.show_all()
609 self
.mini_hbox
.hide()
610 self
.text
.grab_focus()
612 def mini_key_press(self
, entry
, kev
):
613 if kev
.keyval
== g
.keysyms
.Escape
:
614 self
.set_minibuffer(None)
616 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
617 self
.minibuffer
.activate()
620 return self
.minibuffer
.key_press(kev
)
622 def mini_changed(self
, entry
):
623 if not self
.minibuffer
:
625 self
.minibuffer
.changed()
627 def mini_show_info(self
, *unused
):
628 assert self
.minibuffer
630 self
.info_box
.destroy()
631 self
.info_box
= g
.MessageDialog(self
, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
,
632 self
.minibuffer
.info
)
633 self
.info_box
.set_title(_('Minibuffer help'))
636 self
.info_box
.connect('destroy', destroy
)
638 self
.info_box
.connect('response', lambda w
, r
: w
.destroy())
640 def process_selected(self
, process
):
641 """Calls process(line) on each line in the selection, or each line in the file
642 if there is no selection. If the result is not None, the text is replaced."""
643 self
.buffer.begin_user_action()
645 self
._process
_selected
(process
)
647 self
.buffer.end_user_action()
649 def _process_selected(self
, process
):
650 if self
.has_selection():
652 start
, end
= self
.get_selection_range()
653 if start
.compare(end
) > 0:
656 start
, end
= self
.get_selection_range()
657 if start
.compare(end
) > 0:
661 return self
.buffer.get_end_iter()
662 start
= self
.buffer.get_start_iter()
665 while start
.compare(end
) <= 0:
666 line_end
= start
.copy()
667 line_end
.forward_to_line_end()
668 if line_end
.compare(end
) >= 0:
670 line
= self
.buffer.get_text(start
, line_end
, False)
673 self
.buffer.move_mark(self
.mark_tmp
, start
)
674 self
.buffer.insert(line_end
, new
)
675 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
676 line_end
= start
.copy()
677 line_end
.forward_chars(len(line
.decode('utf-8')))
678 self
.buffer.delete(start
, line_end
)
680 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
682 if not start
.forward_line(): break
684 def set_word_wrap(self
, value
):
685 self
._word
_wrap
= value
686 self
.wrap_button
.set_active(value
)
688 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
690 self
.text
.set_wrap_mode(g
.WRAP_NONE
)
692 word_wrap
= property(lambda self
: self
._word
_wrap
, set_word_wrap
)
694 class SelectionSaver(Saveable
):
695 def __init__(self
, window
):
699 def save_to_stream(self
, stream
):
700 s
, e
= self
.window
.get_marked_range()
701 stream
.write(self
.window
.buffer.get_text(s
, e
, True))
704 # Called when savebox is remove. Get rid of the selection marker
705 self
.window
.clear_marked()