Fix warning message when opening minibuffer (reported by joehill).
[rox-edit.git] / EditWindow.py
blob6cb61b0644fbd96b3251e470a3bf6c01191a9a3e
1 import rox
2 from rox import g, filer, app_options
3 import sys
4 from rox.loading import XDSLoader
5 from rox.options import Option
6 from buffer import Buffer
7 from rox.saving import Saveable
9 FALSE = g.FALSE
10 TRUE = g.TRUE
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 set_save_name('Edit')
21 menu = Menu('main', [
22 ('/File', '', '<Branch>'),
23 ('/File/Save', 'save', ''),
24 ('/File/Open Parent', 'up', ''),
25 ('/File/Close', 'close', ''),
26 ('/File/', '', '<Separator>'),
27 ('/File/New', 'new', ''),
28 ('/Edit', '', '<Branch>'),
29 ('/Edit/Undo', 'undo', ''),
30 ('/Edit/Redo', 'redo', ''),
31 ('/Edit/', '', '<Separator>'),
32 ('/Edit/Search...', 'search', ''),
33 ('/Edit/Goto line...', 'goto', ''),
34 ('/Edit/', '', '<Separator>'),
35 ('/Edit/Process...', 'process', ''),
36 ('/Options', 'show_options', ''),
37 ('/Help', 'help', '', 'F1'),
40 known_codecs = (
41 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
42 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
43 "iso8859_13", "iso8859_14", "iso8859_15",
44 "ascii", "base64_codec", "charmap",
45 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
46 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
47 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
48 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
49 "cp869", "cp874", "cp875", "hex_codec",
50 "koi8_r",
51 "latin_1",
52 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
53 "mbcs", "quopri_codec", "raw_unicode_escape",
54 "rot_13",
55 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
56 "zlib_codec"
59 class Abort(Exception):
60 pass
62 class Minibuffer:
63 def setup(self):
64 """Called when the minibuffer is opened."""
66 def key_press(self, kev):
67 """A keypress event in the minibuffer text entry."""
69 def changed(self):
70 """The minibuffer text has changed."""
72 def activate(self):
73 """Return or Enter pressed."""
75 info = 'Press Escape to close the minibuffer.'
77 class EditWindow(g.Window, XDSLoader, Saveable):
78 def __init__(self, filename = None):
79 g.Window.__init__(self)
80 XDSLoader.__init__(self, ['text/plain', 'UTF8_STRING'])
81 self.set_default_size(g.gdk.screen_width() * 2 / 3,
82 g.gdk.screen_height() / 2)
84 self.savebox = None
85 self.info_box = None
87 app_options.add_notify(self.update_styles)
89 self.buffer = Buffer()
91 scrollbar = g.VScrollbar()
92 self.text = g.TextView()
93 self.text.set_property('left-margin', 4)
94 self.text.set_property('right-margin', 4)
95 self.text.set_buffer(self.buffer)
96 adj = scrollbar.get_adjustment()
97 self.text.set_scroll_adjustments(None, adj)
98 self.text.set_size_request(10, 10)
99 self.xds_proxy_for(self.text)
100 self.text.set_wrap_mode(g.WRAP_WORD)
101 self.update_styles()
103 self.insert_mark = self.buffer.get_mark('insert')
104 self.selection_bound_mark = self.buffer.get_mark('selection_bound')
105 start = self.buffer.get_start_iter()
106 self.mark_start = self.buffer.create_mark('mark_start', start, TRUE)
107 self.mark_end = self.buffer.create_mark('mark_end', start, FALSE)
108 tag = self.buffer.create_tag('marked')
109 tag.set_property('background', 'green')
110 self.marked = 0
112 # When searching, this is where the cursor was when the minibuffer
113 # was opened.
114 start = self.buffer.get_start_iter()
115 self.search_base = self.buffer.create_mark('search_base', start, TRUE)
117 vbox = g.VBox(FALSE)
118 self.add(vbox)
120 tools = g.Toolbar()
121 tools.set_style(g.TOOLBAR_ICONS)
122 vbox.pack_start(tools, FALSE, TRUE, 0)
123 tools.show()
125 tools.insert_stock(g.STOCK_HELP, 'Help', None, self.help, None, 0)
126 tools.insert_stock(g.STOCK_REDO, 'Redo', None, self.redo, None, 0)
127 tools.insert_stock(g.STOCK_UNDO, 'Undo', None, self.undo, None, 0)
128 tools.insert_stock(g.STOCK_FIND, 'Search', None, self.search, None, 0)
129 tools.insert_stock(g.STOCK_SAVE, 'Save', None, self.save, None, 0)
130 tools.insert_stock(g.STOCK_GO_UP, 'Up', None, self.up, None, 0)
131 tools.insert_stock(g.STOCK_CLOSE, 'Close', None, self.close, None, 0)
133 hbox = g.HBox(FALSE) # View + Minibuffer + Scrollbar
134 vbox.pack_start(hbox, TRUE, TRUE)
136 inner_vbox = g.VBox(FALSE) # View + Minibuffer
137 hbox.pack_start(inner_vbox, TRUE, TRUE)
138 inner_vbox.pack_start(self.text, TRUE, TRUE)
139 hbox.pack_start(scrollbar, FALSE, TRUE)
141 self.show_all()
143 # Create the minibuffer
144 self.mini_hbox = g.HBox(FALSE)
145 info = g.Button()
146 info.set_relief(g.RELIEF_NONE)
147 info.unset_flags(g.CAN_FOCUS)
148 image = g.Image()
149 image.set_from_stock(g.STOCK_DIALOG_INFO, size = g.ICON_SIZE_SMALL_TOOLBAR)
150 info.add(image)
151 info.show_all()
152 info.connect('clicked', self.mini_show_info)
154 self.mini_hbox.pack_start(info, FALSE, TRUE, 0)
155 self.mini_label = g.Label('')
156 self.mini_hbox.pack_start(self.mini_label, FALSE, TRUE, 0)
157 self.mini_entry = g.Entry()
158 self.mini_hbox.pack_start(self.mini_entry, TRUE, TRUE, 0)
159 inner_vbox.pack_start(self.mini_hbox, FALSE, TRUE)
160 self.mini_entry.connect('key-press-event', self.mini_key_press)
161 self.mini_entry.connect('changed', self.mini_changed)
163 rox.toplevel_ref()
164 self.connect('destroy', self.destroyed)
166 self.connect('delete-event', self.delete_event)
167 self.text.grab_focus()
169 if filename:
170 import os.path
171 self.uri = os.path.abspath(filename)
172 else:
173 self.uri = None
174 self.update_title()
176 # Loading might take a while, so get something on the screen
177 # now...
178 g.gdk.flush()
180 if filename:
181 try:
182 self.load_file(filename)
183 if filename != '-':
184 self.save_mode = os.stat(filename).st_mode
185 except Abort:
186 self.destroy()
187 raise
189 self.buffer.connect('modified-changed', self.update_title)
190 self.buffer.set_modified(FALSE)
192 self.text.connect('button-press-event', self.button_press)
194 menu.attach(self, self)
195 self.buffer.place_cursor(self.buffer.get_start_iter())
196 self.buffer.start_undo_history()
198 def destroyed(self, widget):
199 app_options.remove_notify(self.update_styles)
200 rox.toplevel_unref()
202 def update_styles(self):
203 try:
204 import pango
205 font = pango.FontDescription(default_font.value)
206 bg = g.gdk.color_parse(background_colour.value)
207 fg = g.gdk.color_parse(foreground_colour.value)
208 except:
209 rox.report_exception()
210 else:
211 self.text.modify_font(font)
212 self.text.modify_base(g.STATE_NORMAL, bg)
213 self.text.modify_text(g.STATE_NORMAL, fg)
215 def button_press(self, text, event):
216 if event.button != 3:
217 return 0
218 menu.popup(self, event)
219 return 1
221 def delete_event(self, window, event):
222 if self.buffer.get_modified():
223 self.save(discard = 1)
224 return 1
225 return 0
227 def update_title(self, *unused):
228 title = self.uri or '<Untitled>'
229 if self.buffer.get_modified():
230 title = title + " *"
231 self.set_title(title)
233 def xds_load_from_stream(self, name, t, stream):
234 if t == 'UTF8_STRING':
235 return # Gtk will handle it
236 try:
237 self.insert_data(stream.read())
238 except Abort:
239 pass
241 def get_encoding(self, message):
242 "Returns (encoding, errors), or raises Abort to cancel."
243 import codecs
245 box = g.MessageDialog(self, 0, g.MESSAGE_QUESTION, g.BUTTONS_CANCEL, message)
246 box.set_has_separator(FALSE)
248 frame = g.Frame()
249 box.vbox.pack_start(frame, TRUE, TRUE)
250 frame.set_border_width(6)
252 hbox = g.HBox(FALSE, 4)
253 hbox.set_border_width(6)
255 hbox.pack_start(g.Label('Encoding:'), FALSE, TRUE, 0)
256 combo = g.Combo()
257 combo.disable_activate()
258 combo.entry.connect('activate', lambda w: box.activate_default())
259 combo.set_popdown_strings(known_codecs)
260 hbox.pack_start(combo, TRUE, TRUE, 0)
261 ignore_errors = g.CheckButton('Ignore errors')
262 hbox.pack_start(ignore_errors, FALSE, TRUE)
264 frame.add(hbox)
266 box.vbox.show_all()
267 box.add_button(g.STOCK_CONVERT, g.RESPONSE_YES)
268 box.set_default_response(g.RESPONSE_YES)
270 while 1:
271 combo.entry.grab_focus()
273 resp = box.run()
274 if resp != g.RESPONSE_YES:
275 box.destroy()
276 raise Abort
278 if ignore_errors.get_active():
279 errors = 'replace'
280 else:
281 errors = 'strict'
282 encoding = combo.entry.get_text()
283 try:
284 codecs.getdecoder(encoding)
285 break
286 except:
287 rox.alert("Unknown encoding '%s'" % encoding)
289 box.destroy()
291 return encoding, errors
293 def insert_data(self, data):
294 import codecs
295 errors = 'strict'
296 encoding = 'utf-8'
297 while 1:
298 decoder = codecs.getdecoder(encoding)
299 try:
300 data = decoder(data, errors)[0]
301 assert '\0' not in data
302 break
303 except:
304 pass
306 encoding, errors = self.get_encoding(
307 "Data is not valid %s. Please select the file's encoding."
308 "Turn on 'ignore errors' to try and load it anyway." % encoding)
310 self.buffer.insert_at_cursor(data, -1)
311 return 1
313 def load_file(self, path):
314 try:
315 if path == '-':
316 file = sys.stdin
317 else:
318 file = open(path, 'r')
319 contents = file.read()
320 if path != '-':
321 file.close()
322 self.insert_data(contents)
323 except Abort:
324 raise
325 except:
326 rox.report_exception()
327 raise Abort
329 def close(self, button = None):
330 if self.buffer.get_modified():
331 self.save(discard = 1)
332 else:
333 self.destroy()
335 def discard(self):
336 self.destroy()
338 def up(self, button = None):
339 if self.uri:
340 filer.show_file(self.uri)
341 else:
342 rox.alert("File is not saved to disk yet")
344 def has_selection(self):
345 s, e = self.get_selection_range()
346 return not e.equal(s)
348 def get_marked_range(self):
349 s = self.buffer.get_iter_at_mark(self.mark_start)
350 e = self.buffer.get_iter_at_mark(self.mark_end)
351 return s, e
353 def get_selection_range(self):
354 s = self.buffer.get_iter_at_mark(self.insert_mark)
355 e = self.buffer.get_iter_at_mark(self.selection_bound_mark)
356 return s, e
358 def save(self, widget = None, discard = 0):
359 from rox.saving import SaveBox
361 if self.savebox:
362 self.savebox.destroy()
364 if self.has_selection() and not discard:
365 saver = SelectionSaver(self)
366 self.savebox = SaveBox(saver, 'Selection', 'text/plain')
367 self.savebox.connect('destroy', lambda w: saver.destroy())
368 else:
369 uri = self.uri or 'TextFile'
370 self.savebox = SaveBox(self, uri, 'text/plain', discard)
371 self.savebox.show()
373 def help(self, button = None):
374 filer.open_dir(rox.app_dir + '/Help')
376 def save_to_stream(self, stream):
377 s = self.buffer.get_start_iter()
378 e = self.buffer.get_end_iter()
379 stream.write(self.buffer.get_text(s, e, TRUE))
381 def set_uri(self, uri):
382 self.uri = uri
383 self.buffer.set_modified(FALSE)
384 self.update_title()
386 def new(self):
387 EditWindow()
389 def change_font(self):
390 style = self.text.get_style().copy()
391 style.font = load_font(options.get('edit_font'))
392 self.text.set_style(style)
394 def show_options(self):
395 rox.edit_options()
397 def set_marked(self, start = None, end = None):
398 "Set the marked region (from the selection if no region is given)."
399 self.clear_marked()
400 assert not self.marked
402 buffer = self.buffer
403 if start:
404 assert end
405 else:
406 assert not end
407 start, end = self.get_selection_range()
408 buffer.move_mark(self.mark_start, start)
409 buffer.move_mark(self.mark_end, end)
410 buffer.apply_tag_by_name('marked',
411 buffer.get_iter_at_mark(self.mark_start),
412 buffer.get_iter_at_mark(self.mark_end))
413 self.marked = 1
415 def clear_marked(self):
416 if not self.marked:
417 return
418 self.marked = 0
419 buffer = self.buffer
420 buffer.remove_tag_by_name('marked',
421 buffer.get_iter_at_mark(self.mark_start),
422 buffer.get_iter_at_mark(self.mark_end))
424 def undo(self, widget = None):
425 self.buffer.undo()
427 def redo(self, widget = None):
428 self.buffer.redo()
430 def goto(self, widget = None):
431 from goto import Goto
432 self.set_minibuffer(Goto())
434 def search(self, widget = None):
435 from search import Search
436 self.set_minibuffer(Search())
438 def process(self, widget = None):
439 from process import Process
440 self.set_minibuffer(Process())
442 def set_mini_label(self, label):
443 self.mini_label.set_text(label)
445 def set_minibuffer(self, minibuffer):
446 assert minibuffer is None or isinstance(minibuffer, Minibuffer)
448 self.minibuffer = None
450 if minibuffer:
451 self.mini_entry.set_text('')
452 self.minibuffer = minibuffer
453 minibuffer.setup(self)
454 self.mini_entry.grab_focus()
455 self.mini_hbox.show_all()
456 else:
457 self.mini_hbox.hide()
458 self.text.grab_focus()
460 def mini_key_press(self, entry, kev):
461 if kev.keyval == g.keysyms.Escape:
462 self.set_minibuffer(None)
463 return 1
464 if kev.keyval == g.keysyms.Return or kev.keyval == g.keysyms.KP_Enter:
465 self.minibuffer.activate()
466 return 1
468 return self.minibuffer.key_press(kev)
470 def mini_changed(self, entry):
471 if not self.minibuffer:
472 return
473 self.minibuffer.changed()
475 def mini_show_info(self, *unused):
476 assert self.minibuffer
477 if self.info_box:
478 self.info_box.destroy()
479 self.info_box = g.MessageDialog(self, 0, g.MESSAGE_INFO, g.BUTTONS_OK,
480 self.minibuffer.info)
481 self.info_box.set_title('Minibuffer help')
482 def destroy(box):
483 self.info_box = None
484 self.info_box.connect('destroy', destroy)
485 self.info_box.show()
486 self.info_box.connect('response', lambda w, r: w.destroy())
488 class SelectionSaver(Saveable):
489 def __init__(self, window):
490 self.window = window
491 window.set_marked()
493 def save_to_stream(self, stream):
494 s, e = self.window.get_marked_range()
495 stream.write(self.window.buffer.get_text(s, e, TRUE))
497 def destroy(self):
498 # Called when savebox is remove. Get rid of the selection marker
499 self.window.clear_marked()