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
13 from rox
.Menu
import Menu
, set_save_name
15 default_font
= Option('default_font', 'serif')
17 background_colour
= Option('background', '#fff')
18 foreground_colour
= Option('foreground', '#000')
20 auto_indent
= Option('autoindent', '1')
22 layout_left_margin
= Option('layout_left_margin', 2)
23 layout_right_margin
= Option('layout_right_margin', 4)
25 layout_before_para
= Option('layout_before_para', 0)
26 layout_after_para
= Option('layout_after_para', 0)
27 layout_inside_para
= Option('layout_inside_para', 0)
28 layout_indent_para
= Option('layout_indent_para', 2)
34 (_('/File'), '', '<Branch>'),
35 (_('/File/') + _('Save'), 'save', '<StockItem>', '<Ctrl>S', g
.STOCK_SAVE
),
36 (_('/File/') + _('Open Parent'), 'up', '<StockItem>', '', g
.STOCK_GO_UP
),
37 (_('/File/') + _('Show Changes'), 'diff', ''),
38 (_('/File/') + _('Close'), 'close', '<StockItem>', '', g
.STOCK_CLOSE
),
39 (_('/File/'), '', '<Separator>'),
40 (_('/File/') + _('New'), 'new', '<StockItem>', '', g
.STOCK_NEW
),
42 (_('/Edit'), '', '<Branch>'),
43 (_('/Edit/') + _('Cut'), 'cut', '<StockItem>', '<Ctrl>X', g
.STOCK_CUT
),
44 (_('/Edit/') + _('Copy'), 'copy', '<StockItem>', '<Ctrl>C', g
.STOCK_COPY
),
45 (_('/Edit/') + _('Paste'), 'paste', '<StockItem>', '<Ctrl>V', g
.STOCK_PASTE
),
46 (_('/Edit/'), '', '<Separator>'),
47 (_('/Edit/') + _('Undo'), 'undo', '<StockItem>', '<Ctrl>Z', g
.STOCK_UNDO
),
48 (_('/Edit/') + _('Redo'), 'redo', '<StockItem>', '<Ctrl>Y', g
.STOCK_REDO
),
49 (_('/Edit/'), '', '<Separator>'),
50 (_('/Edit/') + _('Search...'), 'search', '<StockItem>', 'F4', g
.STOCK_FIND
),
51 (_('/Edit/') + _('Search and Replace....'), 'search_replace',
52 '<StockItem>', '<Ctrl>F4', g
.STOCK_FIND_AND_REPLACE
),
53 (_('/Edit/') + _('Goto line...'), 'goto', '<StockItem>', 'F5', g
.STOCK_JUMP_TO
),
55 (_('/Options'), 'show_options', '<StockItem>', '', g
.STOCK_PROPERTIES
),
56 (_('/Help'), 'help', '<StockItem>', 'F1', g
.STOCK_HELP
),
59 rox
.croak(_('Edit requires ROX-Lib2 1.9.8 or later'))
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 EditWindow(rox
.Window
, XDSLoader
, Saveable
):
99 def __init__(self
, filename
= None):
100 rox
.Window
.__init
__(self
)
101 XDSLoader
.__init
__(self
, ['text/plain', 'UTF8_STRING'])
102 self
.set_default_size(g
.gdk
.screen_width() * 2 / 3,
103 g
.gdk
.screen_height() / 2)
108 app_options
.add_notify(self
.update_styles
)
110 self
.buffer = Buffer()
112 self
.text
= g
.TextView()
113 self
.text
.set_buffer(self
.buffer)
114 self
.text
.set_size_request(10, 10)
115 self
.xds_proxy_for(self
.text
)
116 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
119 self
.insert_mark
= self
.buffer.get_mark('insert')
120 self
.selection_bound_mark
= self
.buffer.get_mark('selection_bound')
121 start
= self
.buffer.get_start_iter()
122 self
.mark_start
= self
.buffer.create_mark('mark_start', start
, TRUE
)
123 self
.mark_end
= self
.buffer.create_mark('mark_end', start
, FALSE
)
124 self
.mark_tmp
= self
.buffer.create_mark('mark_tmp', start
, FALSE
)
125 tag
= self
.buffer.create_tag('marked')
126 tag
.set_property('background', 'green')
129 # When searching, this is where the cursor was when the minibuffer
131 start
= self
.buffer.get_start_iter()
132 self
.search_base
= self
.buffer.create_mark('search_base', start
, TRUE
)
138 tools
.set_style(g
.TOOLBAR_ICONS
)
139 vbox
.pack_start(tools
, FALSE
, TRUE
, 0)
142 tools
.insert_stock(g
.STOCK_HELP
, _('Help'), None, self
.help, None, 0)
143 tools
.insert_stock(g
.STOCK_REDO
, _('Redo'), None, self
.redo
, None, 0)
144 tools
.insert_stock(g
.STOCK_UNDO
, _('Undo'), None, self
.undo
, None, 0)
145 tools
.insert_stock(g
.STOCK_FIND_AND_REPLACE
, _('Replace'), None, self
.search_replace
, None, 0)
146 tools
.insert_stock(g
.STOCK_FIND
, _('Search'), None, self
.search
, None, 0)
147 tools
.insert_stock(g
.STOCK_SAVE
, _('Save'), None, self
.save
, None, 0)
148 tools
.insert_stock(g
.STOCK_GO_UP
, _('Up'), None, self
.up
, None, 0)
149 tools
.insert_stock(g
.STOCK_CLOSE
, _('Close'), None, self
.close
, None, 0)
151 swin
= g
.ScrolledWindow()
152 swin
.set_policy(g
.POLICY_NEVER
, g
.POLICY_AUTOMATIC
)
153 vbox
.pack_start(swin
, True, True)
159 # Create the minibuffer
160 self
.mini_hbox
= g
.HBox(FALSE
)
162 info
.set_relief(g
.RELIEF_NONE
)
163 info
.unset_flags(g
.CAN_FOCUS
)
165 image
.set_from_stock(g
.STOCK_DIALOG_INFO
, size
= g
.ICON_SIZE_SMALL_TOOLBAR
)
168 info
.connect('clicked', self
.mini_show_info
)
170 self
.mini_hbox
.pack_start(info
, FALSE
, TRUE
, 0)
171 self
.mini_label
= g
.Label('')
172 self
.mini_hbox
.pack_start(self
.mini_label
, FALSE
, TRUE
, 0)
173 self
.mini_entry
= g
.Entry()
174 self
.mini_hbox
.pack_start(self
.mini_entry
, TRUE
, TRUE
, 0)
175 vbox
.pack_start(self
.mini_hbox
, FALSE
, TRUE
)
176 self
.mini_entry
.connect('key-press-event', self
.mini_key_press
)
177 self
.mini_entry
.connect('changed', self
.mini_changed
)
179 self
.connect('destroy', self
.destroyed
)
181 self
.connect('delete-event', self
.delete_event
)
182 self
.text
.grab_focus()
183 self
.text
.connect('key-press-event', self
.key_press
)
187 self
.uri
= os
.path
.abspath(filename
)
192 # Loading might take a while, so get something on the screen
198 self
.load_file(filename
)
200 self
.save_mode
= os
.stat(filename
).st_mode
205 self
.buffer.connect('modified-changed', self
.update_title
)
206 self
.buffer.set_modified(FALSE
)
208 def button_press(text
, event
):
209 if event
.button
!= 3:
211 menu
.popup(self
, event
)
213 self
.text
.connect('button-press-event', button_press
)
214 self
.text
.connect('popup-menu', lambda text
: menu
.popup(self
, None))
216 menu
.attach(self
, self
)
217 self
.buffer.place_cursor(self
.buffer.get_start_iter())
218 self
.buffer.start_undo_history()
220 def key_press(self
, text
, kev
):
221 if kev
.keyval
!= g
.keysyms
.Return
and kev
.keyval
!= g
.keysyms
.KP_Enter
:
223 if not auto_indent
.int_value
:
225 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
227 start
.set_line_offset(0)
228 end
.forward_to_line_end()
229 line
= self
.buffer.get_text(start
, end
, False)
236 self
.buffer.begin_user_action()
237 self
.buffer.insert_at_cursor('\n' + indent
)
238 self
.buffer.end_user_action()
241 def destroyed(self
, widget
):
242 app_options
.remove_notify(self
.update_styles
)
244 def update_styles(self
):
247 font
= pango
.FontDescription(default_font
.value
)
248 bg
= g
.gdk
.color_parse(background_colour
.value
)
249 fg
= g
.gdk
.color_parse(foreground_colour
.value
)
251 self
.text
.set_left_margin(layout_left_margin
.int_value
)
252 self
.text
.set_right_margin(layout_right_margin
.int_value
)
254 self
.text
.set_pixels_above_lines(layout_before_para
.int_value
)
255 self
.text
.set_pixels_below_lines(layout_after_para
.int_value
)
256 self
.text
.set_pixels_inside_wrap(layout_inside_para
.int_value
)
257 self
.text
.set_indent(layout_indent_para
.int_value
)
259 rox
.report_exception()
261 self
.text
.modify_font(font
)
262 self
.text
.modify_base(g
.STATE_NORMAL
, bg
)
263 self
.text
.modify_text(g
.STATE_NORMAL
, fg
)
265 def cut(self
): self
.text
.emit('cut_clipboard')
266 def copy(self
): self
.text
.emit('copy_clipboard')
267 def paste(self
): self
.text
.emit('paste_clipboard')
269 def delete_event(self
, window
, event
):
270 if self
.buffer.get_modified():
271 self
.save(discard
= 1)
275 def update_title(self
, *unused
):
276 title
= self
.uri
or '<Untitled>'
277 if self
.buffer.get_modified():
279 self
.set_title(title
)
281 def xds_load_from_stream(self
, name
, t
, stream
):
282 if t
== 'UTF8_STRING':
283 return # Gtk will handle it
285 self
.insert_data(stream
.read())
289 def get_encoding(self
, message
):
290 "Returns (encoding, errors), or raises Abort to cancel."
293 box
= g
.MessageDialog(self
, 0, g
.MESSAGE_QUESTION
, g
.BUTTONS_CANCEL
, message
)
294 box
.set_has_separator(FALSE
)
297 box
.vbox
.pack_start(frame
, TRUE
, TRUE
)
298 frame
.set_border_width(6)
300 hbox
= g
.HBox(FALSE
, 4)
301 hbox
.set_border_width(6)
303 hbox
.pack_start(g
.Label('Encoding:'), FALSE
, TRUE
, 0)
305 combo
.disable_activate()
306 combo
.entry
.connect('activate', lambda w
: box
.activate_default())
307 combo
.set_popdown_strings(known_codecs
)
308 hbox
.pack_start(combo
, TRUE
, TRUE
, 0)
309 ignore_errors
= g
.CheckButton('Ignore errors')
310 hbox
.pack_start(ignore_errors
, FALSE
, TRUE
)
315 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
316 box
.set_default_response(g
.RESPONSE_YES
)
319 combo
.entry
.grab_focus()
322 if resp
!= g
.RESPONSE_YES
:
326 if ignore_errors
.get_active():
330 encoding
= combo
.entry
.get_text()
332 codecs
.getdecoder(encoding
)
335 rox
.alert(_("Unknown encoding '%s'") % encoding
)
339 return encoding
, errors
341 def insert_data(self
, data
):
346 decoder
= codecs
.getdecoder(encoding
)
348 data
= decoder(data
, errors
)[0]
349 if errors
== 'strict':
350 assert '\0' not in data
353 data
= data
.replace('\0', '\\0')
358 encoding
, errors
= self
.get_encoding(
359 _("Data is not valid %s. Please select the file's encoding. "
360 "Turn on 'ignore errors' to try and load it anyway.")
363 self
.buffer.begin_user_action()
364 self
.buffer.insert_at_cursor(data
)
365 self
.buffer.end_user_action()
368 def load_file(self
, path
):
373 file = open(path
, 'r')
374 contents
= file.read()
377 self
.insert_data(contents
)
381 rox
.report_exception()
384 def close(self
, button
= None):
385 if self
.buffer.get_modified():
386 self
.save(discard
= 1)
393 def up(self
, button
= None):
395 filer
.show_file(self
.uri
)
397 rox
.alert(_('File is not saved to disk yet'))
399 def diff(self
, button
= None):
401 rox
.alert(_('This file has never been saved; nothing to compare it to!'))
404 diff
.show_diff(self
.uri
, self
.save_to_stream
)
406 def has_selection(self
):
407 s
, e
= self
.get_selection_range()
408 return not e
.equal(s
)
410 def get_marked_range(self
):
411 s
= self
.buffer.get_iter_at_mark(self
.mark_start
)
412 e
= self
.buffer.get_iter_at_mark(self
.mark_end
)
417 def get_selection_range(self
):
418 s
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
419 e
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
424 def save(self
, widget
= None, discard
= 0):
425 from rox
.saving
import SaveBox
428 self
.savebox
.destroy()
430 if self
.has_selection() and not discard
:
431 saver
= SelectionSaver(self
)
432 self
.savebox
= SaveBox(saver
, 'Selection', 'text/plain')
433 self
.savebox
.connect('destroy', lambda w
: saver
.destroy())
435 uri
= self
.uri
or 'TextFile'
436 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
439 def help(self
, button
= None):
440 filer
.open_dir(os
.path
.join(rox
.app_dir
, 'Help'))
442 def save_to_stream(self
, stream
):
443 s
= self
.buffer.get_start_iter()
444 e
= self
.buffer.get_end_iter()
445 stream
.write(self
.buffer.get_text(s
, e
, TRUE
))
447 def set_uri(self
, uri
):
449 self
.buffer.set_modified(FALSE
)
455 def change_font(self
):
456 style
= self
.text
.get_style().copy()
457 style
.font
= load_font(options
.get('edit_font'))
458 self
.text
.set_style(style
)
460 def show_options(self
):
463 def set_marked(self
, start
= None, end
= None):
464 "Set the marked region (from the selection if no region is given)."
466 assert not self
.marked
473 start
, end
= self
.get_selection_range()
474 buffer.move_mark(self
.mark_start
, start
)
475 buffer.move_mark(self
.mark_end
, end
)
476 buffer.apply_tag_by_name('marked',
477 buffer.get_iter_at_mark(self
.mark_start
),
478 buffer.get_iter_at_mark(self
.mark_end
))
481 def clear_marked(self
):
486 buffer.remove_tag_by_name('marked',
487 buffer.get_iter_at_mark(self
.mark_start
),
488 buffer.get_iter_at_mark(self
.mark_end
))
490 def undo(self
, widget
= None):
493 def redo(self
, widget
= None):
496 def goto(self
, widget
= None):
497 from goto
import Goto
498 self
.set_minibuffer(Goto())
500 def search(self
, widget
= None):
501 from search
import Search
502 self
.set_minibuffer(Search())
504 def search_replace(self
, widget
= None):
505 from search
import Replace
508 def set_mini_label(self
, label
):
509 self
.mini_label
.set_text(label
)
511 def set_minibuffer(self
, minibuffer
):
512 assert minibuffer
is None or isinstance(minibuffer
, Minibuffer
)
514 self
.minibuffer
= None
517 self
.mini_entry
.set_text('')
518 self
.minibuffer
= minibuffer
519 minibuffer
.setup(self
)
520 self
.mini_entry
.grab_focus()
521 self
.mini_hbox
.show_all()
523 self
.mini_hbox
.hide()
524 self
.text
.grab_focus()
526 def mini_key_press(self
, entry
, kev
):
527 if kev
.keyval
== g
.keysyms
.Escape
:
528 self
.set_minibuffer(None)
530 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
531 self
.minibuffer
.activate()
534 return self
.minibuffer
.key_press(kev
)
536 def mini_changed(self
, entry
):
537 if not self
.minibuffer
:
539 self
.minibuffer
.changed()
541 def mini_show_info(self
, *unused
):
542 assert self
.minibuffer
544 self
.info_box
.destroy()
545 self
.info_box
= g
.MessageDialog(self
, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
,
546 self
.minibuffer
.info
)
547 self
.info_box
.set_title(_('Minibuffer help'))
550 self
.info_box
.connect('destroy', destroy
)
552 self
.info_box
.connect('response', lambda w
, r
: w
.destroy())
554 def process_selected(self
, process
):
555 """Calls process(line) on each line in the selection, or each line in the file
556 if there is no selection. If the result is not None, the text is replaced."""
557 self
.buffer.begin_user_action()
558 self
._process
_selected
(process
)
559 self
.buffer.end_user_action()
561 def _process_selected(self
, process
):
562 if self
.has_selection():
564 start
, end
= self
.get_selection_range()
565 if start
.compare(end
) > 0:
568 start
, end
= self
.get_selection_range()
569 if start
.compare(end
) > 0:
573 return self
.buffer.get_end_iter()
574 start
= self
.buffer.get_start_iter()
577 while start
.compare(end
) <= 0:
578 line_end
= start
.copy()
579 line_end
.forward_to_line_end()
580 if line_end
.compare(end
) >= 0:
582 line
= self
.buffer.get_text(start
, line_end
, False)
585 self
.buffer.move_mark(self
.mark_tmp
, start
)
586 self
.buffer.insert(line_end
, new
)
587 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
588 line_end
= start
.copy()
589 line_end
.forward_chars(len(line
))
590 self
.buffer.delete(start
, line_end
)
592 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
594 if not start
.forward_line(): break
596 class SelectionSaver(Saveable
):
597 def __init__(self
, window
):
601 def save_to_stream(self
, stream
):
602 s
, e
= self
.window
.get_marked_range()
603 stream
.write(self
.window
.buffer.get_text(s
, e
, TRUE
))
606 # Called when savebox is remove. Get rid of the selection marker
607 self
.window
.clear_marked()