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_mode
= os
.stat(filename
).st_mode
250 self
.buffer.connect('modified-changed', self
.update_title
)
251 self
.buffer.set_modified(FALSE
)
253 def button_press(text
, event
):
254 if event
.button
!= 3:
256 menu
.popup(self
, event
)
258 self
.text
.connect('button-press-event', button_press
)
259 self
.text
.connect('popup-menu', lambda text
: menu
.popup(self
, None))
261 menu
.attach(self
, self
)
262 self
.buffer.place_cursor(self
.buffer.get_start_iter())
263 self
.buffer.start_undo_history()
265 def key_press(self
, text
, kev
):
266 if kev
.keyval
!= g
.keysyms
.Return
and kev
.keyval
!= g
.keysyms
.KP_Enter
:
268 if not auto_indent
.int_value
:
270 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
272 start
.set_line_offset(0)
273 end
.forward_to_line_end()
274 line
= self
.buffer.get_text(start
, end
, False)
281 self
.buffer.begin_user_action()
282 self
.buffer.insert_at_cursor('\n' + indent
)
283 self
.buffer.end_user_action()
286 def destroyed(self
, widget
):
287 app_options
.remove_notify(self
.update_styles
)
289 def update_styles(self
):
292 font
= pango
.FontDescription(default_font
.value
)
293 bg
= g
.gdk
.color_parse(background_colour
.value
)
294 fg
= g
.gdk
.color_parse(foreground_colour
.value
)
296 self
.text
.set_left_margin(layout_left_margin
.int_value
)
297 self
.text
.set_right_margin(layout_right_margin
.int_value
)
299 self
.text
.set_pixels_above_lines(layout_before_para
.int_value
)
300 self
.text
.set_pixels_below_lines(layout_after_para
.int_value
)
301 self
.text
.set_pixels_inside_wrap(layout_inside_para
.int_value
)
302 self
.text
.set_indent(layout_indent_para
.int_value
)
304 if word_wrap
.int_value
== 1:
305 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
307 self
.text
.set_wrap_mode(g
.WRAP_NONE
)
309 rox
.report_exception()
311 self
.text
.modify_font(font
)
312 self
.text
.modify_base(g
.STATE_NORMAL
, bg
)
313 self
.text
.modify_text(g
.STATE_NORMAL
, fg
)
315 def cut(self
): self
.text
.emit('cut_clipboard')
316 def copy(self
): self
.text
.emit('copy_clipboard')
317 def paste(self
): self
.text
.emit('paste_clipboard')
319 def delete_event(self
, window
, event
):
320 if self
.buffer.get_modified():
321 self
.save(discard
= 1)
325 def update_title(self
, *unused
):
326 title
= self
.uri
or '<Untitled>'
327 if self
.buffer.get_modified():
329 self
.set_title(title
)
331 def xds_load_from_stream(self
, name
, t
, stream
):
332 if t
== 'UTF8_STRING':
333 return # Gtk will handle it
335 self
.insert_data(stream
.read())
339 def get_encoding(self
, message
):
340 "Returns (encoding, errors), or raises Abort to cancel."
341 box
= g
.MessageDialog(self
, 0, g
.MESSAGE_QUESTION
, g
.BUTTONS_CANCEL
, message
)
342 box
.set_has_separator(FALSE
)
345 box
.vbox
.pack_start(frame
, TRUE
, TRUE
)
346 frame
.set_border_width(6)
348 hbox
= g
.HBox(FALSE
, 4)
349 hbox
.set_border_width(6)
351 hbox
.pack_start(g
.Label('Encoding:'), FALSE
, TRUE
, 0)
353 combo
.disable_activate()
354 combo
.entry
.connect('activate', lambda w
: box
.activate_default())
355 combo
.set_popdown_strings(known_codecs
)
356 hbox
.pack_start(combo
, TRUE
, TRUE
, 0)
357 ignore_errors
= g
.CheckButton('Ignore errors')
358 hbox
.pack_start(ignore_errors
, FALSE
, TRUE
)
363 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
364 box
.set_default_response(g
.RESPONSE_YES
)
367 combo
.entry
.grab_focus()
370 if resp
!= g
.RESPONSE_YES
:
374 if ignore_errors
.get_active():
378 encoding
= combo
.entry
.get_text()
380 codecs
.getdecoder(encoding
)
383 rox
.alert(_("Unknown encoding '%s'") % encoding
)
387 return encoding
, errors
389 def insert_data(self
, data
):
394 decoder
= codecs
.getdecoder(encoding
)
396 data
= decoder(data
, errors
)[0]
397 if errors
== 'strict':
398 assert '\0' not in data
401 data
= data
.replace('\0', '\\0')
406 encoding
, errors
= self
.get_encoding(
407 _("Data is not valid %s. Please select the file's encoding. "
408 "Turn on 'ignore errors' to try and load it anyway.")
411 self
.buffer.begin_user_action()
412 self
.buffer.insert_at_cursor(data
)
413 self
.buffer.end_user_action()
416 def load_file(self
, path
):
421 file = open(path
, 'r')
422 contents
= file.read()
425 self
.insert_data(contents
)
429 rox
.report_exception()
432 def close(self
, button
= None):
433 if self
.buffer.get_modified():
434 self
.save(discard
= 1)
441 def up(self
, button
= None):
443 filer
.show_file(self
.uri
)
445 rox
.alert(_('File is not saved to disk yet'))
447 def diff(self
, button
= None, path
= None):
448 path
= path
or self
.uri
450 rox
.alert(_('This file has never been saved; nothing to compare it to!\n'
451 'Note: you can drop a file onto the toolbar button to see '
452 'the changes from that file.'))
454 diff
.show_diff(path
, self
.save_to_stream
)
456 def has_selection(self
):
457 s
, e
= self
.get_selection_range()
458 return not e
.equal(s
)
460 def get_marked_range(self
):
461 s
= self
.buffer.get_iter_at_mark(self
.mark_start
)
462 e
= self
.buffer.get_iter_at_mark(self
.mark_end
)
467 def get_selection_range(self
):
468 s
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
469 e
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
474 def save(self
, widget
= None, discard
= 0):
475 from rox
.saving
import SaveBox
478 self
.savebox
.destroy()
480 if self
.has_selection() and not discard
:
481 saver
= SelectionSaver(self
)
482 self
.savebox
= SaveBox(saver
, 'Selection', 'text/plain')
483 self
.savebox
.connect('destroy', lambda w
: saver
.destroy())
485 uri
= self
.uri
or 'TextFile'
486 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
489 def help(self
, button
= None):
490 filer
.open_dir(os
.path
.join(rox
.app_dir
, 'Help'))
492 def save_to_stream(self
, stream
):
493 s
= self
.buffer.get_start_iter()
494 e
= self
.buffer.get_end_iter()
495 stream
.write(self
.buffer.get_text(s
, e
, TRUE
))
497 def set_uri(self
, uri
):
499 self
.buffer.set_modified(FALSE
)
505 def change_font(self
):
506 style
= self
.text
.get_style().copy()
507 style
.font
= load_font(options
.get('edit_font'))
508 self
.text
.set_style(style
)
510 def show_options(self
):
513 def set_marked(self
, start
= None, end
= None):
514 "Set the marked region (from the selection if no region is given)."
516 assert not self
.marked
523 start
, end
= self
.get_selection_range()
524 buffer.move_mark(self
.mark_start
, start
)
525 buffer.move_mark(self
.mark_end
, end
)
526 buffer.apply_tag_by_name('marked',
527 buffer.get_iter_at_mark(self
.mark_start
),
528 buffer.get_iter_at_mark(self
.mark_end
))
531 def clear_marked(self
):
536 buffer.remove_tag_by_name('marked',
537 buffer.get_iter_at_mark(self
.mark_start
),
538 buffer.get_iter_at_mark(self
.mark_end
))
540 def undo(self
, widget
= None):
543 def redo(self
, widget
= None):
546 def goto(self
, widget
= None):
547 from goto
import Goto
548 self
.set_minibuffer(Goto())
550 def search(self
, widget
= None):
551 from search
import Search
552 self
.set_minibuffer(Search())
554 def search_replace(self
, widget
= None):
555 from search
import Replace
558 def set_mini_label(self
, label
):
559 self
.mini_label
.set_text(label
)
561 def set_minibuffer(self
, minibuffer
):
562 assert minibuffer
is None or isinstance(minibuffer
, Minibuffer
)
564 self
.minibuffer
= None
567 self
.mini_entry
.set_text('')
568 self
.minibuffer
= minibuffer
569 minibuffer
.setup(self
)
570 self
.mini_entry
.grab_focus()
571 self
.mini_hbox
.show_all()
573 self
.mini_hbox
.hide()
574 self
.text
.grab_focus()
576 def mini_key_press(self
, entry
, kev
):
577 if kev
.keyval
== g
.keysyms
.Escape
:
578 self
.set_minibuffer(None)
580 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
581 self
.minibuffer
.activate()
584 return self
.minibuffer
.key_press(kev
)
586 def mini_changed(self
, entry
):
587 if not self
.minibuffer
:
589 self
.minibuffer
.changed()
591 def mini_show_info(self
, *unused
):
592 assert self
.minibuffer
594 self
.info_box
.destroy()
595 self
.info_box
= g
.MessageDialog(self
, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
,
596 self
.minibuffer
.info
)
597 self
.info_box
.set_title(_('Minibuffer help'))
600 self
.info_box
.connect('destroy', destroy
)
602 self
.info_box
.connect('response', lambda w
, r
: w
.destroy())
604 def process_selected(self
, process
):
605 """Calls process(line) on each line in the selection, or each line in the file
606 if there is no selection. If the result is not None, the text is replaced."""
607 self
.buffer.begin_user_action()
608 self
._process
_selected
(process
)
609 self
.buffer.end_user_action()
611 def _process_selected(self
, process
):
612 if self
.has_selection():
614 start
, end
= self
.get_selection_range()
615 if start
.compare(end
) > 0:
618 start
, end
= self
.get_selection_range()
619 if start
.compare(end
) > 0:
623 return self
.buffer.get_end_iter()
624 start
= self
.buffer.get_start_iter()
627 while start
.compare(end
) <= 0:
628 line_end
= start
.copy()
629 line_end
.forward_to_line_end()
630 if line_end
.compare(end
) >= 0:
632 line
= self
.buffer.get_text(start
, line_end
, False)
635 self
.buffer.move_mark(self
.mark_tmp
, start
)
636 self
.buffer.insert(line_end
, new
)
637 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
638 line_end
= start
.copy()
639 line_end
.forward_chars(len(line
))
640 self
.buffer.delete(start
, line_end
)
642 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
644 if not start
.forward_line(): break
646 class SelectionSaver(Saveable
):
647 def __init__(self
, window
):
651 def save_to_stream(self
, stream
):
652 s
, e
= self
.window
.get_marked_range()
653 stream
.write(self
.window
.buffer.get_text(s
, e
, TRUE
))
656 # Called when savebox is remove. Get rid of the selection marker
657 self
.window
.clear_marked()