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')
25 word_wrap
= Option('wordwrap', '1')
27 layout_left_margin
= Option('layout_left_margin', 2)
28 layout_right_margin
= Option('layout_right_margin', 4)
30 layout_before_para
= Option('layout_before_para', 0)
31 layout_after_para
= Option('layout_after_para', 0)
32 layout_inside_para
= Option('layout_inside_para', 0)
33 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
),
64 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
65 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
66 "iso8859_13", "iso8859_14", "iso8859_15",
67 "ascii", "base64_codec", "charmap",
68 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
69 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
70 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
71 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
72 "cp869", "cp874", "cp875", "hex_codec",
75 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
76 "mbcs", "quopri_codec", "raw_unicode_escape",
78 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
82 class Abort(Exception):
87 """Called when the minibuffer is opened."""
89 def key_press(self
, kev
):
90 """A keypress event in the minibuffer text entry."""
93 """The minibuffer text has changed."""
96 """Return or Enter pressed."""
98 info
= 'Press Escape to close the minibuffer.'
100 class DiffLoader(XDSLoader
):
101 def __init__(self
, window
):
102 XDSLoader
.__init
__(self
, ['text/plain'])
105 def xds_load_from_file(self
, path
):
106 self
.window
.diff(path
= path
)
108 def xds_load_from_stream(self
, name
, type, stream
):
109 tmp
= diff
.Tmp(suffix
= '-' + (name
or 'tmp'))
111 shutil
.copyfileobj(stream
, tmp
)
113 self
.window
.diff(path
= tmp
.name
)
115 class EditWindow(rox
.Window
, XDSLoader
, Saveable
):
116 def __init__(self
, filename
= None):
117 rox
.Window
.__init
__(self
)
118 XDSLoader
.__init
__(self
, ['text/plain', 'UTF8_STRING'])
119 self
.set_default_size(g
.gdk
.screen_width() * 2 / 3,
120 g
.gdk
.screen_height() / 2)
125 app_options
.add_notify(self
.update_styles
)
127 self
.buffer = Buffer()
129 self
.text
= g
.TextView()
130 self
.text
.set_buffer(self
.buffer)
131 self
.text
.set_size_request(10, 10)
132 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
)
165 tools
.insert_stock(g
.STOCK_REDO
, _('Redo'), None, self
.redo
, None, 0)
166 tools
.insert_stock(g
.STOCK_UNDO
, _('Undo'), None, self
.undo
, None, 0)
167 tools
.insert_stock(g
.STOCK_FIND_AND_REPLACE
, _('Replace'), None, self
.search_replace
, None, 0)
168 tools
.insert_stock(g
.STOCK_FIND
, _('Search'), None, self
.search
, None, 0)
169 tools
.insert_stock(g
.STOCK_SAVE
, _('Save'), None, self
.save
, None, 0)
170 tools
.insert_stock(g
.STOCK_GO_UP
, _('Up'), None, self
.up
, None, 0)
171 tools
.insert_stock(g
.STOCK_CLOSE
, _('Close'), None, self
.close
, None, 0)
172 # Set minimum size to ignore the label
173 tools
.set_size_request(tools
.size_request()[0], -1)
175 swin
= g
.ScrolledWindow()
176 swin
.set_policy(g
.POLICY_AUTOMATIC
, g
.POLICY_AUTOMATIC
)
177 vbox
.pack_start(swin
, True, True)
183 # Create the minibuffer
184 self
.mini_hbox
= g
.HBox(FALSE
)
186 info
.set_relief(g
.RELIEF_NONE
)
187 info
.unset_flags(g
.CAN_FOCUS
)
189 image
.set_from_stock(g
.STOCK_DIALOG_INFO
, size
= g
.ICON_SIZE_SMALL_TOOLBAR
)
192 info
.connect('clicked', self
.mini_show_info
)
194 self
.mini_hbox
.pack_start(info
, FALSE
, TRUE
, 0)
195 self
.mini_label
= g
.Label('')
196 self
.mini_hbox
.pack_start(self
.mini_label
, FALSE
, TRUE
, 0)
197 self
.mini_entry
= g
.Entry()
198 self
.mini_hbox
.pack_start(self
.mini_entry
, TRUE
, TRUE
, 0)
199 vbox
.pack_start(self
.mini_hbox
, FALSE
, TRUE
)
200 self
.mini_entry
.connect('key-press-event', self
.mini_key_press
)
201 self
.mini_entry
.connect('changed', self
.mini_changed
)
203 self
.connect('destroy', self
.destroyed
)
205 self
.connect('delete-event', self
.delete_event
)
206 self
.text
.grab_focus()
207 self
.text
.connect('key-press-event', self
.key_press
)
209 def update_current_line(*unused
):
210 cursor
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
211 bound
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
212 if cursor
.compare(bound
) == 0:
213 n_lines
= self
.buffer.get_line_count()
214 self
.status_label
.set_text(_('Line %s of %d') % (cursor
.get_line() + 1, n_lines
))
216 n_lines
= abs(cursor
.get_line() - bound
.get_line()) + 1
218 n_chars
= abs(cursor
.get_line_offset() - bound
.get_line_offset())
220 bytes
= to_utf8(self
.buffer.get_text(cursor
, bound
, False))[0]
221 self
.status_label
.set_text(_('One character selected (%s)') %
222 ' '.join(map(lambda x
: '0x%2x' % ord(x
), bytes
)))
224 self
.status_label
.set_text(_('%d characters selected') % n_chars
)
226 self
.status_label
.set_text(_('%d lines selected') % n_lines
)
227 self
.buffer.connect('mark-set', update_current_line
)
228 self
.buffer.connect('changed', update_current_line
)
232 self
.uri
= os
.path
.abspath(filename
)
237 # Loading might take a while, so get something on the screen
243 self
.load_file(filename
)
245 self
.save_last_stat
= os
.stat(filename
)
246 self
.save_mode
= self
.save_last_stat
.st_mode
# for older ROX-Libs
251 self
.buffer.connect('modified-changed', self
.update_title
)
252 self
.buffer.set_modified(FALSE
)
254 def button_press(text
, event
):
255 if event
.button
!= 3:
257 menu
.popup(self
, event
)
259 self
.text
.connect('button-press-event', button_press
)
260 self
.text
.connect('popup-menu', lambda text
: menu
.popup(self
, None))
262 menu
.attach(self
, self
)
263 self
.buffer.place_cursor(self
.buffer.get_start_iter())
264 self
.buffer.start_undo_history()
266 def key_press(self
, text
, kev
):
267 if kev
.keyval
!= g
.keysyms
.Return
and kev
.keyval
!= g
.keysyms
.KP_Enter
:
269 if not auto_indent
.int_value
:
271 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
273 start
.set_line_offset(0)
274 end
.forward_to_line_end()
275 line
= self
.buffer.get_text(start
, end
, False)
282 self
.buffer.begin_user_action()
283 self
.buffer.insert_at_cursor('\n' + indent
)
284 self
.buffer.end_user_action()
287 def destroyed(self
, widget
):
288 app_options
.remove_notify(self
.update_styles
)
290 def update_styles(self
):
293 font
= pango
.FontDescription(default_font
.value
)
294 bg
= g
.gdk
.color_parse(background_colour
.value
)
295 fg
= g
.gdk
.color_parse(foreground_colour
.value
)
297 self
.text
.set_left_margin(layout_left_margin
.int_value
)
298 self
.text
.set_right_margin(layout_right_margin
.int_value
)
300 self
.text
.set_pixels_above_lines(layout_before_para
.int_value
)
301 self
.text
.set_pixels_below_lines(layout_after_para
.int_value
)
302 self
.text
.set_pixels_inside_wrap(layout_inside_para
.int_value
)
303 self
.text
.set_indent(layout_indent_para
.int_value
)
305 if word_wrap
.int_value
== 1:
306 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
308 self
.text
.set_wrap_mode(g
.WRAP_NONE
)
310 rox
.report_exception()
312 self
.text
.modify_font(font
)
313 self
.text
.modify_base(g
.STATE_NORMAL
, bg
)
314 self
.text
.modify_text(g
.STATE_NORMAL
, fg
)
316 def cut(self
): self
.text
.emit('cut_clipboard')
317 def copy(self
): self
.text
.emit('copy_clipboard')
318 def paste(self
): self
.text
.emit('paste_clipboard')
320 def delete_event(self
, window
, event
):
321 if self
.buffer.get_modified():
322 self
.save(discard
= 1)
326 def update_title(self
, *unused
):
327 title
= self
.uri
or '<Untitled>'
328 if self
.buffer.get_modified():
330 self
.set_title(title
)
332 def xds_load_from_stream(self
, name
, t
, stream
):
333 if t
== 'UTF8_STRING':
334 return # Gtk will handle it
336 self
.insert_data(stream
.read())
340 def get_encoding(self
, message
):
341 "Returns (encoding, errors), or raises Abort to cancel."
342 box
= g
.MessageDialog(self
, 0, g
.MESSAGE_QUESTION
, g
.BUTTONS_CANCEL
, message
)
343 box
.set_has_separator(FALSE
)
346 box
.vbox
.pack_start(frame
, TRUE
, TRUE
)
347 frame
.set_border_width(6)
349 hbox
= g
.HBox(FALSE
, 4)
350 hbox
.set_border_width(6)
352 hbox
.pack_start(g
.Label('Encoding:'), FALSE
, TRUE
, 0)
354 combo
.disable_activate()
355 combo
.entry
.connect('activate', lambda w
: box
.activate_default())
356 combo
.set_popdown_strings(known_codecs
)
357 hbox
.pack_start(combo
, TRUE
, TRUE
, 0)
358 ignore_errors
= g
.CheckButton('Ignore errors')
359 hbox
.pack_start(ignore_errors
, FALSE
, TRUE
)
364 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
365 box
.set_default_response(g
.RESPONSE_YES
)
368 combo
.entry
.grab_focus()
371 if resp
!= g
.RESPONSE_YES
:
375 if ignore_errors
.get_active():
379 encoding
= combo
.entry
.get_text()
381 codecs
.getdecoder(encoding
)
384 rox
.alert(_("Unknown encoding '%s'") % encoding
)
388 return encoding
, errors
390 def insert_data(self
, data
):
395 decoder
= codecs
.getdecoder(encoding
)
397 data
= decoder(data
, errors
)[0]
398 if errors
== 'strict':
399 assert '\0' not in data
402 data
= data
.replace('\0', '\\0')
407 encoding
, errors
= self
.get_encoding(
408 _("Data is not valid %s. Please select the file's encoding. "
409 "Turn on 'ignore errors' to try and load it anyway.")
412 self
.buffer.begin_user_action()
413 self
.buffer.insert_at_cursor(data
)
414 self
.buffer.end_user_action()
417 def load_file(self
, path
):
422 file = open(path
, 'r')
423 contents
= file.read()
426 self
.insert_data(contents
)
430 rox
.report_exception()
433 def close(self
, button
= None):
434 if self
.buffer.get_modified():
435 self
.save(discard
= 1)
442 def up(self
, button
= None):
444 filer
.show_file(self
.uri
)
446 rox
.alert(_('File is not saved to disk yet'))
448 def diff(self
, button
= None, path
= None):
449 path
= path
or self
.uri
451 rox
.alert(_('This file has never been saved; nothing to compare it to!\n'
452 'Note: you can drop a file onto the toolbar button to see '
453 'the changes from that file.'))
455 diff
.show_diff(path
, self
.save_to_stream
)
457 def has_selection(self
):
458 s
, e
= self
.get_selection_range()
459 return not e
.equal(s
)
461 def get_marked_range(self
):
462 s
= self
.buffer.get_iter_at_mark(self
.mark_start
)
463 e
= self
.buffer.get_iter_at_mark(self
.mark_end
)
468 def get_selection_range(self
):
469 s
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
470 e
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
475 def save(self
, widget
= None, discard
= 0):
476 from rox
.saving
import SaveBox
479 self
.savebox
.destroy()
481 if self
.has_selection() and not discard
:
482 saver
= SelectionSaver(self
)
483 self
.savebox
= SaveBox(saver
, 'Selection', 'text/plain')
484 self
.savebox
.connect('destroy', lambda w
: saver
.destroy())
486 uri
= self
.uri
or 'TextFile'
487 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
490 def help(self
, button
= None):
491 filer
.open_dir(os
.path
.join(rox
.app_dir
, 'Help'))
493 def save_to_stream(self
, stream
):
494 s
= self
.buffer.get_start_iter()
495 e
= self
.buffer.get_end_iter()
496 stream
.write(self
.buffer.get_text(s
, e
, TRUE
))
498 def set_uri(self
, uri
):
500 self
.buffer.set_modified(FALSE
)
506 def change_font(self
):
507 style
= self
.text
.get_style().copy()
508 style
.font
= load_font(options
.get('edit_font'))
509 self
.text
.set_style(style
)
511 def show_options(self
):
514 def set_marked(self
, start
= None, end
= None):
515 "Set the marked region (from the selection if no region is given)."
517 assert not self
.marked
524 start
, end
= self
.get_selection_range()
525 buffer.move_mark(self
.mark_start
, start
)
526 buffer.move_mark(self
.mark_end
, end
)
527 buffer.apply_tag_by_name('marked',
528 buffer.get_iter_at_mark(self
.mark_start
),
529 buffer.get_iter_at_mark(self
.mark_end
))
532 def clear_marked(self
):
537 buffer.remove_tag_by_name('marked',
538 buffer.get_iter_at_mark(self
.mark_start
),
539 buffer.get_iter_at_mark(self
.mark_end
))
541 def undo(self
, widget
= None):
544 def redo(self
, widget
= None):
547 def goto(self
, widget
= None):
548 from goto
import Goto
549 self
.set_minibuffer(Goto())
551 def search(self
, widget
= None):
552 from search
import Search
553 self
.set_minibuffer(Search())
555 def search_replace(self
, widget
= None):
556 from search
import Replace
559 def set_mini_label(self
, label
):
560 self
.mini_label
.set_text(label
)
562 def set_minibuffer(self
, minibuffer
):
563 assert minibuffer
is None or isinstance(minibuffer
, Minibuffer
)
565 self
.minibuffer
= None
568 self
.mini_entry
.set_text('')
569 self
.minibuffer
= minibuffer
570 minibuffer
.setup(self
)
571 self
.mini_entry
.grab_focus()
572 self
.mini_hbox
.show_all()
574 self
.mini_hbox
.hide()
575 self
.text
.grab_focus()
577 def mini_key_press(self
, entry
, kev
):
578 if kev
.keyval
== g
.keysyms
.Escape
:
579 self
.set_minibuffer(None)
581 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
582 self
.minibuffer
.activate()
585 return self
.minibuffer
.key_press(kev
)
587 def mini_changed(self
, entry
):
588 if not self
.minibuffer
:
590 self
.minibuffer
.changed()
592 def mini_show_info(self
, *unused
):
593 assert self
.minibuffer
595 self
.info_box
.destroy()
596 self
.info_box
= g
.MessageDialog(self
, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
,
597 self
.minibuffer
.info
)
598 self
.info_box
.set_title(_('Minibuffer help'))
601 self
.info_box
.connect('destroy', destroy
)
603 self
.info_box
.connect('response', lambda w
, r
: w
.destroy())
605 def process_selected(self
, process
):
606 """Calls process(line) on each line in the selection, or each line in the file
607 if there is no selection. If the result is not None, the text is replaced."""
608 self
.buffer.begin_user_action()
609 self
._process
_selected
(process
)
610 self
.buffer.end_user_action()
612 def _process_selected(self
, process
):
613 if self
.has_selection():
615 start
, end
= self
.get_selection_range()
616 if start
.compare(end
) > 0:
619 start
, end
= self
.get_selection_range()
620 if start
.compare(end
) > 0:
624 return self
.buffer.get_end_iter()
625 start
= self
.buffer.get_start_iter()
628 while start
.compare(end
) <= 0:
629 line_end
= start
.copy()
630 line_end
.forward_to_line_end()
631 if line_end
.compare(end
) >= 0:
633 line
= self
.buffer.get_text(start
, line_end
, False)
636 self
.buffer.move_mark(self
.mark_tmp
, start
)
637 self
.buffer.insert(line_end
, new
)
638 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
639 line_end
= start
.copy()
640 line_end
.forward_chars(len(line
))
641 self
.buffer.delete(start
, line_end
)
643 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
645 if not start
.forward_line(): break
647 class SelectionSaver(Saveable
):
648 def __init__(self
, window
):
652 def save_to_stream(self
, stream
):
653 s
, e
= self
.window
.get_marked_range()
654 stream
.write(self
.window
.buffer.get_text(s
, e
, TRUE
))
657 # Called when savebox is remove. Get rid of the selection marker
658 self
.window
.clear_marked()