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 from rox
.Menu
import Menu
, set_save_name
14 default_font
= Option('default_font', 'serif')
16 background_colour
= Option('background', '#fff')
17 foreground_colour
= Option('foreground', '#000')
19 auto_indent
= Option('autoindent', '1')
21 layout_left_margin
= Option('layout_left_margin', 2)
22 layout_right_margin
= Option('layout_right_margin', 4)
24 layout_before_para
= Option('layout_before_para', 0)
25 layout_after_para
= Option('layout_after_para', 0)
26 layout_inside_para
= Option('layout_inside_para', 0)
27 layout_indent_para
= Option('layout_indent_para', 2)
33 ('/File', '', '<Branch>'),
34 ('/File/Save', 'save', '<StockItem>', '<Ctrl>S', g
.STOCK_SAVE
),
35 ('/File/Open Parent', 'up', '<StockItem>', '', g
.STOCK_GO_UP
),
36 ('/File/Close', 'close', '<StockItem>', '', g
.STOCK_CLOSE
),
37 ('/File/', '', '<Separator>'),
38 ('/File/New', 'new', '<StockItem>', '', g
.STOCK_NEW
),
40 ('/Edit', '', '<Branch>'),
41 ('/Edit/Cut', 'cut', '<StockItem>', '<Ctrl>X', g
.STOCK_CUT
),
42 ('/Edit/Copy', 'copy', '<StockItem>', '<Ctrl>C', g
.STOCK_COPY
),
43 ('/Edit/Pase', 'paste', '<StockItem>', '<Ctrl>V', g
.STOCK_PASTE
),
44 ('/Edit/', '', '<Separator>'),
45 ('/Edit/Undo', 'undo', '<StockItem>', '<Ctrl>Z', g
.STOCK_UNDO
),
46 ('/Edit/Redo', 'redo', '<StockItem>', '<Ctrl>Y', g
.STOCK_REDO
),
47 ('/Edit/', '', '<Separator>'),
48 ('/Edit/Search...', 'search', '<StockItem>', 'F4', g
.STOCK_FIND
),
49 ('/Edit/Search and Replace....', 'search_replace',
50 '<StockItem>', '<Ctrl>F4', g
.STOCK_FIND_AND_REPLACE
),
51 ('/Edit/Goto line...', 'goto', '<StockItem>', 'F5', g
.STOCK_JUMP_TO
),
53 ('/Options', 'show_options', '<StockItem>', '', g
.STOCK_PROPERTIES
),
54 ('/Help', 'help', '<StockItem>', 'F1', g
.STOCK_HELP
),
57 rox
.croak('Edit requires ROX-Lib2 1.9.8 or later')
60 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
61 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
62 "iso8859_13", "iso8859_14", "iso8859_15",
63 "ascii", "base64_codec", "charmap",
64 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
65 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
66 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
67 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
68 "cp869", "cp874", "cp875", "hex_codec",
71 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
72 "mbcs", "quopri_codec", "raw_unicode_escape",
74 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
78 class Abort(Exception):
83 """Called when the minibuffer is opened."""
85 def key_press(self
, kev
):
86 """A keypress event in the minibuffer text entry."""
89 """The minibuffer text has changed."""
92 """Return or Enter pressed."""
94 info
= 'Press Escape to close the minibuffer.'
96 class EditWindow(rox
.Window
, XDSLoader
, Saveable
):
97 def __init__(self
, filename
= None):
98 rox
.Window
.__init
__(self
)
99 XDSLoader
.__init
__(self
, ['text/plain', 'UTF8_STRING'])
100 self
.set_default_size(g
.gdk
.screen_width() * 2 / 3,
101 g
.gdk
.screen_height() / 2)
106 app_options
.add_notify(self
.update_styles
)
108 self
.buffer = Buffer()
110 self
.text
= g
.TextView()
111 self
.text
.set_buffer(self
.buffer)
112 self
.text
.set_size_request(10, 10)
113 self
.xds_proxy_for(self
.text
)
114 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
117 self
.insert_mark
= self
.buffer.get_mark('insert')
118 self
.selection_bound_mark
= self
.buffer.get_mark('selection_bound')
119 start
= self
.buffer.get_start_iter()
120 self
.mark_start
= self
.buffer.create_mark('mark_start', start
, TRUE
)
121 self
.mark_end
= self
.buffer.create_mark('mark_end', start
, FALSE
)
122 self
.mark_tmp
= self
.buffer.create_mark('mark_tmp', start
, FALSE
)
123 tag
= self
.buffer.create_tag('marked')
124 tag
.set_property('background', 'green')
127 # When searching, this is where the cursor was when the minibuffer
129 start
= self
.buffer.get_start_iter()
130 self
.search_base
= self
.buffer.create_mark('search_base', start
, TRUE
)
136 tools
.set_style(g
.TOOLBAR_ICONS
)
137 vbox
.pack_start(tools
, FALSE
, TRUE
, 0)
140 tools
.insert_stock(g
.STOCK_HELP
, 'Help', None, self
.help, None, 0)
141 tools
.insert_stock(g
.STOCK_REDO
, 'Redo', None, self
.redo
, None, 0)
142 tools
.insert_stock(g
.STOCK_UNDO
, 'Undo', None, self
.undo
, None, 0)
143 tools
.insert_stock(g
.STOCK_FIND_AND_REPLACE
, 'Replace', None, self
.search_replace
, None, 0)
144 tools
.insert_stock(g
.STOCK_FIND
, 'Search', None, self
.search
, None, 0)
145 tools
.insert_stock(g
.STOCK_SAVE
, 'Save', None, self
.save
, None, 0)
146 tools
.insert_stock(g
.STOCK_GO_UP
, 'Up', None, self
.up
, None, 0)
147 tools
.insert_stock(g
.STOCK_CLOSE
, 'Close', None, self
.close
, None, 0)
149 swin
= g
.ScrolledWindow()
150 swin
.set_policy(g
.POLICY_NEVER
, g
.POLICY_AUTOMATIC
)
151 vbox
.pack_start(swin
, True, True)
157 # Create the minibuffer
158 self
.mini_hbox
= g
.HBox(FALSE
)
160 info
.set_relief(g
.RELIEF_NONE
)
161 info
.unset_flags(g
.CAN_FOCUS
)
163 image
.set_from_stock(g
.STOCK_DIALOG_INFO
, size
= g
.ICON_SIZE_SMALL_TOOLBAR
)
166 info
.connect('clicked', self
.mini_show_info
)
168 self
.mini_hbox
.pack_start(info
, FALSE
, TRUE
, 0)
169 self
.mini_label
= g
.Label('')
170 self
.mini_hbox
.pack_start(self
.mini_label
, FALSE
, TRUE
, 0)
171 self
.mini_entry
= g
.Entry()
172 self
.mini_hbox
.pack_start(self
.mini_entry
, TRUE
, TRUE
, 0)
173 vbox
.pack_start(self
.mini_hbox
, FALSE
, TRUE
)
174 self
.mini_entry
.connect('key-press-event', self
.mini_key_press
)
175 self
.mini_entry
.connect('changed', self
.mini_changed
)
177 self
.connect('destroy', self
.destroyed
)
179 self
.connect('delete-event', self
.delete_event
)
180 self
.text
.grab_focus()
181 self
.text
.connect('key-press-event', self
.key_press
)
185 self
.uri
= os
.path
.abspath(filename
)
190 # Loading might take a while, so get something on the screen
196 self
.load_file(filename
)
198 self
.save_mode
= os
.stat(filename
).st_mode
203 self
.buffer.connect('modified-changed', self
.update_title
)
204 self
.buffer.set_modified(FALSE
)
206 self
.text
.connect('button-press-event', self
.button_press
)
208 menu
.attach(self
, self
)
209 self
.buffer.place_cursor(self
.buffer.get_start_iter())
210 self
.buffer.start_undo_history()
212 def key_press(self
, text
, kev
):
213 if kev
.keyval
!= g
.keysyms
.Return
and kev
.keyval
!= g
.keysyms
.KP_Enter
:
215 if not auto_indent
.int_value
:
217 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
219 start
.set_line_offset(0)
220 end
.forward_to_line_end()
221 line
= self
.buffer.get_text(start
, end
, False)
228 self
.buffer.begin_user_action()
229 self
.buffer.insert_at_cursor('\n' + indent
)
230 self
.buffer.end_user_action()
233 def destroyed(self
, widget
):
234 app_options
.remove_notify(self
.update_styles
)
236 def update_styles(self
):
239 font
= pango
.FontDescription(default_font
.value
)
240 bg
= g
.gdk
.color_parse(background_colour
.value
)
241 fg
= g
.gdk
.color_parse(foreground_colour
.value
)
243 self
.text
.set_left_margin(layout_left_margin
.int_value
)
244 self
.text
.set_right_margin(layout_right_margin
.int_value
)
246 self
.text
.set_pixels_above_lines(layout_before_para
.int_value
)
247 self
.text
.set_pixels_below_lines(layout_after_para
.int_value
)
248 self
.text
.set_pixels_inside_wrap(layout_inside_para
.int_value
)
249 self
.text
.set_indent(layout_indent_para
.int_value
)
251 rox
.report_exception()
253 self
.text
.modify_font(font
)
254 self
.text
.modify_base(g
.STATE_NORMAL
, bg
)
255 self
.text
.modify_text(g
.STATE_NORMAL
, fg
)
257 def cut(self
): self
.text
.emit('cut_clipboard')
258 def copy(self
): self
.text
.emit('copy_clipboard')
259 def paste(self
): self
.text
.emit('paste_clipboard')
261 def button_press(self
, text
, event
):
262 if event
.button
!= 3:
264 menu
.popup(self
, event
)
267 def delete_event(self
, window
, event
):
268 if self
.buffer.get_modified():
269 self
.save(discard
= 1)
273 def update_title(self
, *unused
):
274 title
= self
.uri
or '<Untitled>'
275 if self
.buffer.get_modified():
277 self
.set_title(title
)
279 def xds_load_from_stream(self
, name
, t
, stream
):
280 if t
== 'UTF8_STRING':
281 return # Gtk will handle it
283 self
.insert_data(stream
.read())
287 def get_encoding(self
, message
):
288 "Returns (encoding, errors), or raises Abort to cancel."
291 box
= g
.MessageDialog(self
, 0, g
.MESSAGE_QUESTION
, g
.BUTTONS_CANCEL
, message
)
292 box
.set_has_separator(FALSE
)
295 box
.vbox
.pack_start(frame
, TRUE
, TRUE
)
296 frame
.set_border_width(6)
298 hbox
= g
.HBox(FALSE
, 4)
299 hbox
.set_border_width(6)
301 hbox
.pack_start(g
.Label('Encoding:'), FALSE
, TRUE
, 0)
303 combo
.disable_activate()
304 combo
.entry
.connect('activate', lambda w
: box
.activate_default())
305 combo
.set_popdown_strings(known_codecs
)
306 hbox
.pack_start(combo
, TRUE
, TRUE
, 0)
307 ignore_errors
= g
.CheckButton('Ignore errors')
308 hbox
.pack_start(ignore_errors
, FALSE
, TRUE
)
313 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
314 box
.set_default_response(g
.RESPONSE_YES
)
317 combo
.entry
.grab_focus()
320 if resp
!= g
.RESPONSE_YES
:
324 if ignore_errors
.get_active():
328 encoding
= combo
.entry
.get_text()
330 codecs
.getdecoder(encoding
)
333 rox
.alert("Unknown encoding '%s'" % encoding
)
337 return encoding
, errors
339 def insert_data(self
, data
):
344 decoder
= codecs
.getdecoder(encoding
)
346 data
= decoder(data
, errors
)[0]
347 if errors
== 'strict':
348 assert '\0' not in data
351 data
= data
.replace('\0', '\\0')
356 encoding
, errors
= self
.get_encoding(
357 "Data is not valid %s. Please select the file's encoding."
358 "Turn on 'ignore errors' to try and load it anyway." % encoding
)
360 self
.buffer.begin_user_action()
361 self
.buffer.insert_at_cursor(data
)
362 self
.buffer.end_user_action()
365 def load_file(self
, path
):
370 file = open(path
, 'r')
371 contents
= file.read()
374 self
.insert_data(contents
)
378 rox
.report_exception()
381 def close(self
, button
= None):
382 if self
.buffer.get_modified():
383 self
.save(discard
= 1)
390 def up(self
, button
= None):
392 filer
.show_file(self
.uri
)
394 rox
.alert(_("File is not saved to disk yet"))
396 def has_selection(self
):
397 s
, e
= self
.get_selection_range()
398 return not e
.equal(s
)
400 def get_marked_range(self
):
401 s
= self
.buffer.get_iter_at_mark(self
.mark_start
)
402 e
= self
.buffer.get_iter_at_mark(self
.mark_end
)
407 def get_selection_range(self
):
408 s
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
409 e
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
414 def save(self
, widget
= None, discard
= 0):
415 from rox
.saving
import SaveBox
418 self
.savebox
.destroy()
420 if self
.has_selection() and not discard
:
421 saver
= SelectionSaver(self
)
422 self
.savebox
= SaveBox(saver
, 'Selection', 'text/plain')
423 self
.savebox
.connect('destroy', lambda w
: saver
.destroy())
425 uri
= self
.uri
or 'TextFile'
426 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
429 def help(self
, button
= None):
430 filer
.open_dir(rox
.app_dir
+ '/Help')
432 def save_to_stream(self
, stream
):
433 s
= self
.buffer.get_start_iter()
434 e
= self
.buffer.get_end_iter()
435 stream
.write(self
.buffer.get_text(s
, e
, TRUE
))
437 def set_uri(self
, uri
):
439 self
.buffer.set_modified(FALSE
)
445 def change_font(self
):
446 style
= self
.text
.get_style().copy()
447 style
.font
= load_font(options
.get('edit_font'))
448 self
.text
.set_style(style
)
450 def show_options(self
):
453 def set_marked(self
, start
= None, end
= None):
454 "Set the marked region (from the selection if no region is given)."
456 assert not self
.marked
463 start
, end
= self
.get_selection_range()
464 buffer.move_mark(self
.mark_start
, start
)
465 buffer.move_mark(self
.mark_end
, end
)
466 buffer.apply_tag_by_name('marked',
467 buffer.get_iter_at_mark(self
.mark_start
),
468 buffer.get_iter_at_mark(self
.mark_end
))
471 def clear_marked(self
):
476 buffer.remove_tag_by_name('marked',
477 buffer.get_iter_at_mark(self
.mark_start
),
478 buffer.get_iter_at_mark(self
.mark_end
))
480 def undo(self
, widget
= None):
483 def redo(self
, widget
= None):
486 def goto(self
, widget
= None):
487 from goto
import Goto
488 self
.set_minibuffer(Goto())
490 def search(self
, widget
= None):
491 from search
import Search
492 self
.set_minibuffer(Search())
494 def search_replace(self
, widget
= None):
495 from search
import Replace
498 def set_mini_label(self
, label
):
499 self
.mini_label
.set_text(label
)
501 def set_minibuffer(self
, minibuffer
):
502 assert minibuffer
is None or isinstance(minibuffer
, Minibuffer
)
504 self
.minibuffer
= None
507 self
.mini_entry
.set_text('')
508 self
.minibuffer
= minibuffer
509 minibuffer
.setup(self
)
510 self
.mini_entry
.grab_focus()
511 self
.mini_hbox
.show_all()
513 self
.mini_hbox
.hide()
514 self
.text
.grab_focus()
516 def mini_key_press(self
, entry
, kev
):
517 if kev
.keyval
== g
.keysyms
.Escape
:
518 self
.set_minibuffer(None)
520 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
521 self
.minibuffer
.activate()
524 return self
.minibuffer
.key_press(kev
)
526 def mini_changed(self
, entry
):
527 if not self
.minibuffer
:
529 self
.minibuffer
.changed()
531 def mini_show_info(self
, *unused
):
532 assert self
.minibuffer
534 self
.info_box
.destroy()
535 self
.info_box
= g
.MessageDialog(self
, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
,
536 self
.minibuffer
.info
)
537 self
.info_box
.set_title('Minibuffer help')
540 self
.info_box
.connect('destroy', destroy
)
542 self
.info_box
.connect('response', lambda w
, r
: w
.destroy())
544 def process_selected(self
, process
):
545 """Calls process(line) on each line in the selection, or each line in the file
546 if there is no selection. If the result is not None, the text is replaced."""
547 self
.buffer.begin_user_action()
548 self
._process
_selected
(process
)
549 self
.buffer.end_user_action()
551 def _process_selected(self
, process
):
552 if self
.has_selection():
554 start
, end
= self
.get_selection_range()
555 if start
.compare(end
) > 0:
558 start
, end
= self
.get_selection_range()
559 if start
.compare(end
) > 0:
563 return self
.buffer.get_end_iter()
564 start
= self
.buffer.get_start_iter()
567 while start
.compare(end
) <= 0:
568 line_end
= start
.copy()
569 line_end
.forward_to_line_end()
570 if line_end
.compare(end
) >= 0:
572 line
= self
.buffer.get_text(start
, line_end
, False)
575 self
.buffer.move_mark(self
.mark_tmp
, start
)
576 self
.buffer.insert(line_end
, new
)
577 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
578 line_end
= start
.copy()
579 line_end
.forward_chars(len(line
))
580 self
.buffer.delete(start
, line_end
)
582 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
584 if not start
.forward_line(): break
586 class SelectionSaver(Saveable
):
587 def __init__(self
, window
):
591 def save_to_stream(self
, stream
):
592 s
, e
= self
.window
.get_marked_range()
593 stream
.write(self
.window
.buffer.get_text(s
, e
, TRUE
))
596 # Called when savebox is remove. Get rid of the selection marker
597 self
.window
.clear_marked()