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(g
.Window
, XDSLoader
, Saveable
):
97 def __init__(self
, filename
= None):
98 g
.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 scrollbar
= g
.VScrollbar()
111 self
.v_adj
= scrollbar
.get_adjustment()
112 self
.text
= g
.TextView()
113 self
.text
.set_buffer(self
.buffer)
114 self
.text
.set_scroll_adjustments(None, self
.v_adj
)
115 self
.text
.set_size_request(10, 10)
116 self
.xds_proxy_for(self
.text
)
117 self
.text
.set_wrap_mode(g
.WRAP_WORD
)
120 self
.insert_mark
= self
.buffer.get_mark('insert')
121 self
.selection_bound_mark
= self
.buffer.get_mark('selection_bound')
122 start
= self
.buffer.get_start_iter()
123 self
.mark_start
= self
.buffer.create_mark('mark_start', start
, TRUE
)
124 self
.mark_end
= self
.buffer.create_mark('mark_end', start
, FALSE
)
125 self
.mark_tmp
= self
.buffer.create_mark('mark_tmp', start
, FALSE
)
126 tag
= self
.buffer.create_tag('marked')
127 tag
.set_property('background', 'green')
130 # When searching, this is where the cursor was when the minibuffer
132 start
= self
.buffer.get_start_iter()
133 self
.search_base
= self
.buffer.create_mark('search_base', start
, TRUE
)
139 tools
.set_style(g
.TOOLBAR_ICONS
)
140 vbox
.pack_start(tools
, FALSE
, TRUE
, 0)
143 tools
.insert_stock(g
.STOCK_HELP
, 'Help', None, self
.help, None, 0)
144 tools
.insert_stock(g
.STOCK_REDO
, 'Redo', None, self
.redo
, None, 0)
145 tools
.insert_stock(g
.STOCK_UNDO
, 'Undo', None, self
.undo
, None, 0)
146 tools
.insert_stock(g
.STOCK_FIND_AND_REPLACE
, 'Replace', None, self
.search_replace
, None, 0)
147 tools
.insert_stock(g
.STOCK_FIND
, 'Search', None, self
.search
, None, 0)
148 tools
.insert_stock(g
.STOCK_SAVE
, 'Save', None, self
.save
, None, 0)
149 tools
.insert_stock(g
.STOCK_GO_UP
, 'Up', None, self
.up
, None, 0)
150 tools
.insert_stock(g
.STOCK_CLOSE
, 'Close', None, self
.close
, None, 0)
152 hbox
= g
.HBox(FALSE
) # View + Minibuffer + Scrollbar
153 vbox
.pack_start(hbox
, TRUE
, TRUE
)
155 inner_vbox
= g
.VBox(FALSE
) # View + Minibuffer
156 hbox
.pack_start(inner_vbox
, TRUE
, TRUE
)
157 inner_vbox
.pack_start(self
.text
, TRUE
, TRUE
)
158 hbox
.pack_start(scrollbar
, FALSE
, TRUE
)
162 # Create the minibuffer
163 self
.mini_hbox
= g
.HBox(FALSE
)
165 info
.set_relief(g
.RELIEF_NONE
)
166 info
.unset_flags(g
.CAN_FOCUS
)
168 image
.set_from_stock(g
.STOCK_DIALOG_INFO
, size
= g
.ICON_SIZE_SMALL_TOOLBAR
)
171 info
.connect('clicked', self
.mini_show_info
)
173 self
.mini_hbox
.pack_start(info
, FALSE
, TRUE
, 0)
174 self
.mini_label
= g
.Label('')
175 self
.mini_hbox
.pack_start(self
.mini_label
, FALSE
, TRUE
, 0)
176 self
.mini_entry
= g
.Entry()
177 self
.mini_hbox
.pack_start(self
.mini_entry
, TRUE
, TRUE
, 0)
178 inner_vbox
.pack_start(self
.mini_hbox
, FALSE
, TRUE
)
179 self
.mini_entry
.connect('key-press-event', self
.mini_key_press
)
180 self
.mini_entry
.connect('changed', self
.mini_changed
)
183 self
.connect('destroy', self
.destroyed
)
185 self
.connect('delete-event', self
.delete_event
)
186 self
.text
.grab_focus()
187 self
.text
.connect('key-press-event', self
.key_press
)
191 self
.uri
= os
.path
.abspath(filename
)
196 # Loading might take a while, so get something on the screen
202 self
.load_file(filename
)
204 self
.save_mode
= os
.stat(filename
).st_mode
209 self
.buffer.connect('modified-changed', self
.update_title
)
210 self
.buffer.set_modified(FALSE
)
212 self
.text
.connect('button-press-event', self
.button_press
)
213 self
.text
.connect('scroll-event', self
.scroll_event
)
215 menu
.attach(self
, self
)
216 self
.buffer.place_cursor(self
.buffer.get_start_iter())
217 self
.buffer.start_undo_history()
219 def key_press(self
, text
, kev
):
220 if kev
.keyval
!= g
.keysyms
.Return
and kev
.keyval
!= g
.keysyms
.KP_Enter
:
222 if not auto_indent
.int_value
:
224 start
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
226 start
.set_line_offset(0)
227 end
.forward_to_line_end()
228 line
= self
.buffer.get_text(start
, end
, False)
235 self
.buffer.begin_user_action()
236 self
.buffer.insert_at_cursor('\n' + indent
)
237 self
.buffer.end_user_action()
240 def destroyed(self
, widget
):
241 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 button_press(self
, text
, event
):
270 if event
.button
!= 3:
272 menu
.popup(self
, event
)
275 def scroll_event(self
, text
, event
):
276 dir = event
.direction
277 new
= self
.v_adj
.value
278 if dir == g
.gdk
.SCROLL_UP
:
280 elif dir == g
.gdk
.SCROLL_DOWN
:
282 self
.v_adj
.set_value(new
)
284 def delete_event(self
, window
, event
):
285 if self
.buffer.get_modified():
286 self
.save(discard
= 1)
290 def update_title(self
, *unused
):
291 title
= self
.uri
or '<Untitled>'
292 if self
.buffer.get_modified():
294 self
.set_title(title
)
296 def xds_load_from_stream(self
, name
, t
, stream
):
297 if t
== 'UTF8_STRING':
298 return # Gtk will handle it
300 self
.insert_data(stream
.read())
304 def get_encoding(self
, message
):
305 "Returns (encoding, errors), or raises Abort to cancel."
308 box
= g
.MessageDialog(self
, 0, g
.MESSAGE_QUESTION
, g
.BUTTONS_CANCEL
, message
)
309 box
.set_has_separator(FALSE
)
312 box
.vbox
.pack_start(frame
, TRUE
, TRUE
)
313 frame
.set_border_width(6)
315 hbox
= g
.HBox(FALSE
, 4)
316 hbox
.set_border_width(6)
318 hbox
.pack_start(g
.Label('Encoding:'), FALSE
, TRUE
, 0)
320 combo
.disable_activate()
321 combo
.entry
.connect('activate', lambda w
: box
.activate_default())
322 combo
.set_popdown_strings(known_codecs
)
323 hbox
.pack_start(combo
, TRUE
, TRUE
, 0)
324 ignore_errors
= g
.CheckButton('Ignore errors')
325 hbox
.pack_start(ignore_errors
, FALSE
, TRUE
)
330 box
.add_button(g
.STOCK_CONVERT
, g
.RESPONSE_YES
)
331 box
.set_default_response(g
.RESPONSE_YES
)
334 combo
.entry
.grab_focus()
337 if resp
!= g
.RESPONSE_YES
:
341 if ignore_errors
.get_active():
345 encoding
= combo
.entry
.get_text()
347 codecs
.getdecoder(encoding
)
350 rox
.alert("Unknown encoding '%s'" % encoding
)
354 return encoding
, errors
356 def insert_data(self
, data
):
361 decoder
= codecs
.getdecoder(encoding
)
363 data
= decoder(data
, errors
)[0]
364 if errors
== 'strict':
365 assert '\0' not in data
368 data
= data
.replace('\0', '\\0')
373 encoding
, errors
= self
.get_encoding(
374 "Data is not valid %s. Please select the file's encoding."
375 "Turn on 'ignore errors' to try and load it anyway." % encoding
)
377 self
.buffer.begin_user_action()
378 self
.buffer.insert_at_cursor(data
)
379 self
.buffer.end_user_action()
382 def load_file(self
, path
):
387 file = open(path
, 'r')
388 contents
= file.read()
391 self
.insert_data(contents
)
395 rox
.report_exception()
398 def close(self
, button
= None):
399 if self
.buffer.get_modified():
400 self
.save(discard
= 1)
407 def up(self
, button
= None):
409 filer
.show_file(self
.uri
)
411 rox
.alert("File is not saved to disk yet")
413 def has_selection(self
):
414 s
, e
= self
.get_selection_range()
415 return not e
.equal(s
)
417 def get_marked_range(self
):
418 s
= self
.buffer.get_iter_at_mark(self
.mark_start
)
419 e
= self
.buffer.get_iter_at_mark(self
.mark_end
)
424 def get_selection_range(self
):
425 s
= self
.buffer.get_iter_at_mark(self
.insert_mark
)
426 e
= self
.buffer.get_iter_at_mark(self
.selection_bound_mark
)
431 def save(self
, widget
= None, discard
= 0):
432 from rox
.saving
import SaveBox
435 self
.savebox
.destroy()
437 if self
.has_selection() and not discard
:
438 saver
= SelectionSaver(self
)
439 self
.savebox
= SaveBox(saver
, 'Selection', 'text/plain')
440 self
.savebox
.connect('destroy', lambda w
: saver
.destroy())
442 uri
= self
.uri
or 'TextFile'
443 self
.savebox
= SaveBox(self
, uri
, 'text/plain', discard
)
446 def help(self
, button
= None):
447 filer
.open_dir(rox
.app_dir
+ '/Help')
449 def save_to_stream(self
, stream
):
450 s
= self
.buffer.get_start_iter()
451 e
= self
.buffer.get_end_iter()
452 stream
.write(self
.buffer.get_text(s
, e
, TRUE
))
454 def set_uri(self
, uri
):
456 self
.buffer.set_modified(FALSE
)
462 def change_font(self
):
463 style
= self
.text
.get_style().copy()
464 style
.font
= load_font(options
.get('edit_font'))
465 self
.text
.set_style(style
)
467 def show_options(self
):
470 def set_marked(self
, start
= None, end
= None):
471 "Set the marked region (from the selection if no region is given)."
473 assert not self
.marked
480 start
, end
= self
.get_selection_range()
481 buffer.move_mark(self
.mark_start
, start
)
482 buffer.move_mark(self
.mark_end
, end
)
483 buffer.apply_tag_by_name('marked',
484 buffer.get_iter_at_mark(self
.mark_start
),
485 buffer.get_iter_at_mark(self
.mark_end
))
488 def clear_marked(self
):
493 buffer.remove_tag_by_name('marked',
494 buffer.get_iter_at_mark(self
.mark_start
),
495 buffer.get_iter_at_mark(self
.mark_end
))
497 def undo(self
, widget
= None):
500 def redo(self
, widget
= None):
503 def goto(self
, widget
= None):
504 from goto
import Goto
505 self
.set_minibuffer(Goto())
507 def search(self
, widget
= None):
508 from search
import Search
509 self
.set_minibuffer(Search())
511 def search_replace(self
, widget
= None):
512 from search
import Replace
515 def set_mini_label(self
, label
):
516 self
.mini_label
.set_text(label
)
518 def set_minibuffer(self
, minibuffer
):
519 assert minibuffer
is None or isinstance(minibuffer
, Minibuffer
)
521 self
.minibuffer
= None
524 self
.mini_entry
.set_text('')
525 self
.minibuffer
= minibuffer
526 minibuffer
.setup(self
)
527 self
.mini_entry
.grab_focus()
528 self
.mini_hbox
.show_all()
530 self
.mini_hbox
.hide()
531 self
.text
.grab_focus()
533 def mini_key_press(self
, entry
, kev
):
534 if kev
.keyval
== g
.keysyms
.Escape
:
535 self
.set_minibuffer(None)
537 if kev
.keyval
== g
.keysyms
.Return
or kev
.keyval
== g
.keysyms
.KP_Enter
:
538 self
.minibuffer
.activate()
541 return self
.minibuffer
.key_press(kev
)
543 def mini_changed(self
, entry
):
544 if not self
.minibuffer
:
546 self
.minibuffer
.changed()
548 def mini_show_info(self
, *unused
):
549 assert self
.minibuffer
551 self
.info_box
.destroy()
552 self
.info_box
= g
.MessageDialog(self
, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
,
553 self
.minibuffer
.info
)
554 self
.info_box
.set_title('Minibuffer help')
557 self
.info_box
.connect('destroy', destroy
)
559 self
.info_box
.connect('response', lambda w
, r
: w
.destroy())
561 def process_selected(self
, process
):
562 """Calls process(line) on each line in the selection, or each line in the file
563 if there is no selection. If the result is not None, the text is replaced."""
564 self
.buffer.begin_user_action()
565 self
._process
_selected
(process
)
566 self
.buffer.end_user_action()
568 def _process_selected(self
, process
):
569 if self
.has_selection():
571 start
, end
= self
.get_selection_range()
572 if start
.compare(end
) > 0:
575 start
, end
= self
.get_selection_range()
576 if start
.compare(end
) > 0:
580 return self
.buffer.get_end_iter()
581 start
= self
.buffer.get_start_iter()
584 while start
.compare(end
) <= 0:
585 line_end
= start
.copy()
586 line_end
.forward_to_line_end()
587 if line_end
.compare(end
) >= 0:
589 line
= self
.buffer.get_text(start
, line_end
, False)
592 self
.buffer.move_mark(self
.mark_tmp
, start
)
593 self
.buffer.insert(line_end
, new
)
594 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
595 line_end
= start
.copy()
596 line_end
.forward_chars(len(line
))
597 self
.buffer.delete(start
, line_end
)
599 start
= self
.buffer.get_iter_at_mark(self
.mark_tmp
)
601 if not start
.forward_line(): break
603 class SelectionSaver(Saveable
):
604 def __init__(self
, window
):
608 def save_to_stream(self
, stream
):
609 s
, e
= self
.window
.get_marked_range()
610 stream
.write(self
.window
.buffer.get_text(s
, e
, TRUE
))
613 # Called when savebox is remove. Get rid of the selection marker
614 self
.window
.clear_marked()