Update for new ROX-Lib.
[rox-edit.git] / EditWindow.py
blob94580bce5c71b038a8bea875cfde20cbb5b746cb
1 import rox
2 from rox import g, filer, app_options
3 import sys
4 from rox.XDSLoader 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_name = Option('default_name', 'TextFile')
15 default_font = Option('default_font', 'serif')
17 background_colour = Option('background', '#fff')
18 foreground_colour = Option('foreground', '#000')
20 set_save_name('Edit')
22 menu = Menu('main', [
23 ('/File', '', '<Branch>'),
24 ('/File/Save', 'save', ''),
25 ('/File/Open Parent', 'up', ''),
26 ('/File/Close', 'close', ''),
27 ('/File/', '', '<Separator>'),
28 ('/File/New', 'new', ''),
29 ('/Edit', '', '<Branch>'),
30 ('/Edit/Undo', 'undo', ''),
31 ('/Edit/Redo', 'redo', ''),
32 ('/Edit/', '', '<Separator>'),
33 ('/Edit/Search...', 'search', ''),
34 ('/Edit/Goto line...', 'goto', ''),
35 ('/Edit/', '', '<Separator>'),
36 ('/Edit/Process...', 'process', ''),
37 ('/Options', 'show_options', ''),
38 ('/Help', 'help', '', 'F1'),
41 known_codecs = (
42 "iso8859_1", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5",
43 "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", "iso8859_10",
44 "iso8859_13", "iso8859_14", "iso8859_15",
45 "ascii", "base64_codec", "charmap",
46 "cp037", "cp1006", "cp1026", "cp1140", "cp1250", "cp1251", "cp1252",
47 "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp424",
48 "cp437", "cp500", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856",
49 "cp857", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866",
50 "cp869", "cp874", "cp875", "hex_codec",
51 "koi8_r",
52 "latin_1",
53 "mac_cyrillic", "mac_greek", "mac_iceland", "mac_latin2", "mac_roman", "mac_turkish",
54 "mbcs", "quopri_codec", "raw_unicode_escape",
55 "rot_13",
56 "utf_16_be", "utf_16_le", "utf_16", "utf_7", "utf_8", "uu_codec",
57 "zlib_codec"
60 class Abort(Exception):
61 pass
63 class EditWindow(g.Window, XDSLoader, Saveable):
64 def __init__(self, filename = None):
65 g.Window.__init__(self)
66 XDSLoader.__init__(self, ['text/plain', 'UTF8_STRING'])
67 self.set_default_size(g.gdk.screen_width() * 2 / 3,
68 g.gdk.screen_height() / 2)
70 self.savebox = None
72 app_options.add_notify(self.update_styles)
74 self.buffer = Buffer()
76 scrollbar = g.VScrollbar()
77 self.text = g.TextView()
78 self.text.set_property('left-margin', 4)
79 self.text.set_property('right-margin', 4)
80 self.text.set_buffer(self.buffer)
81 adj = scrollbar.get_adjustment()
82 self.text.set_scroll_adjustments(None, adj)
83 self.text.set_size_request(10, 10)
84 self.xds_proxy_for(self.text)
85 self.text.set_wrap_mode(g.WRAP_WORD)
86 self.update_styles()
88 self.insert_mark = self.buffer.get_mark('insert')
89 self.selection_bound_mark = self.buffer.get_mark('selection_bound')
90 start = self.buffer.get_start_iter()
91 self.mark_start = self.buffer.create_mark('mark_start', start, TRUE)
92 self.mark_end = self.buffer.create_mark('mark_end', start, FALSE)
93 tag = self.buffer.create_tag('marked')
94 tag.set_property('background', 'green')
95 self.marked = 0
97 # When searching, this is where the cursor was when the minibuffer
98 # was opened.
99 start = self.buffer.get_start_iter()
100 self.search_base = self.buffer.create_mark('search_base', start, TRUE)
102 vbox = g.VBox(FALSE)
103 self.add(vbox)
105 tools = g.Toolbar()
106 tools.set_style(g.TOOLBAR_ICONS)
107 vbox.pack_start(tools, FALSE, TRUE, 0)
108 tools.show()
110 tools.insert_stock(g.STOCK_HELP, 'Help', None, self.help, None, 0)
111 tools.insert_stock(g.STOCK_REDO, 'Redo', None, self.redo, None, 0)
112 tools.insert_stock(g.STOCK_UNDO, 'Undo', None, self.undo, None, 0)
113 tools.insert_stock(g.STOCK_FIND, 'Search', None, self.search, None, 0)
114 tools.insert_stock(g.STOCK_SAVE, 'Save', None, self.save, None, 0)
115 tools.insert_stock(g.STOCK_GO_UP, 'Up', None, self.up, None, 0)
116 tools.insert_stock(g.STOCK_CLOSE, 'Close', None, self.close, None, 0)
118 hbox = g.HBox(FALSE) # View + Minibuffer + Scrollbar
119 vbox.pack_start(hbox, TRUE, TRUE)
121 inner_vbox = g.VBox(FALSE) # View + Minibuffer
122 hbox.pack_start(inner_vbox, TRUE, TRUE)
123 inner_vbox.pack_start(self.text, TRUE, TRUE)
124 hbox.pack_start(scrollbar, FALSE, TRUE)
126 self.show_all()
128 # Create the minibuffer
129 self.mini_hbox = g.HBox(FALSE)
130 self.mini_label = g.Label('')
131 self.mini_hbox.pack_start(self.mini_label, FALSE, TRUE, 0)
132 self.mini_entry = g.Entry()
133 self.mini_hbox.pack_start(self.mini_entry, TRUE, TRUE, 0)
134 inner_vbox.pack_start(self.mini_hbox, FALSE, TRUE)
135 self.mini_entry.connect('key-press-event', self.mini_key_press)
136 self.mini_entry.connect('changed', self.mini_changed)
138 rox.toplevel_ref()
139 self.connect('destroy', self.destroyed)
141 self.connect('delete-event', self.delete_event)
142 self.text.grab_focus()
144 if filename:
145 import os.path
146 self.uri = os.path.abspath(filename)
147 else:
148 self.uri = None
149 self.update_title()
151 # Loading might take a while, so get something on the screen
152 # now...
153 g.gdk.flush()
155 if filename:
156 try:
157 self.load_file(filename)
158 if filename != '-':
159 self.save_mode = os.stat(filename).st_mode
160 except Abort:
161 self.destroy()
162 raise
164 self.buffer.connect('modified-changed', self.update_title)
165 self.buffer.set_modified(FALSE)
167 self.text.connect('button-press-event', self.button_press)
169 menu.attach(self, self)
170 self.buffer.place_cursor(self.buffer.get_start_iter())
171 self.buffer.start_undo_history()
173 def destroyed(self, widget):
174 app_options.remove_notify(self.update_styles)
175 rox.toplevel_unref()
177 def update_styles(self):
178 try:
179 import pango
180 font = pango.FontDescription(default_font.value)
181 bg = g.gdk.color_parse(background_colour.value)
182 fg = g.gdk.color_parse(foreground_colour.value)
183 except:
184 rox.report_exception()
185 else:
186 self.text.modify_font(font)
187 self.text.modify_base(g.STATE_NORMAL, bg)
188 self.text.modify_text(g.STATE_NORMAL, fg)
190 def button_press(self, text, event):
191 if event.button != 3:
192 return 0
193 menu.popup(self, event)
194 return 1
196 def delete_event(self, window, event):
197 if self.buffer.get_modified():
198 self.save(discard = 1)
199 return 1
200 return 0
202 def update_title(self, *unused):
203 title = self.uri or '<Untitled>'
204 if self.buffer.get_modified():
205 title = title + " *"
206 self.set_title(title)
208 def xds_load_from_stream(self, name, t, stream):
209 if t== 'UTF8_STRING':
210 return # Gtk will handle it
211 try:
212 self.insert_data(stream.read())
213 except Abort:
214 pass
216 def get_encoding(self, message):
217 "Returns (encoding, errors), or raises Abort to cancel."
218 import codecs
220 box = g.MessageDialog(self, 0, g.MESSAGE_QUESTION, g.BUTTONS_CANCEL, message)
221 box.set_has_separator(FALSE)
223 frame = g.Frame()
224 box.vbox.pack_start(frame, TRUE, TRUE)
225 frame.set_border_width(6)
227 hbox = g.HBox(FALSE, 4)
228 hbox.set_border_width(6)
230 hbox.pack_start(g.Label('Encoding:'), FALSE, TRUE, 0)
231 combo = g.Combo()
232 combo.disable_activate()
233 combo.entry.connect('activate', lambda w: box.activate_default())
234 combo.set_popdown_strings(known_codecs)
235 hbox.pack_start(combo, TRUE, TRUE, 0)
236 ignore_errors = g.CheckButton('Ignore errors')
237 hbox.pack_start(ignore_errors, FALSE, TRUE)
239 frame.add(hbox)
241 box.vbox.show_all()
242 box.add_button(g.STOCK_CONVERT, g.RESPONSE_YES)
243 box.set_default_response(g.RESPONSE_YES)
245 while 1:
246 combo.entry.grab_focus()
248 resp = box.run()
249 if resp != g.RESPONSE_YES:
250 box.destroy()
251 raise Abort
253 if ignore_errors.get_active():
254 errors = 'replace'
255 else:
256 errors = 'strict'
257 encoding = combo.entry.get_text()
258 try:
259 codecs.getdecoder(encoding)
260 break
261 except:
262 rox.alert("Unknown encoding '%s'" % encoding)
264 box.destroy()
266 return encoding, errors
268 def insert_data(self, data):
269 import codecs
270 errors = 'strict'
271 encoding = 'utf-8'
272 while 1:
273 decoder = codecs.getdecoder(encoding)
274 try:
275 data = decoder(data, errors)[0]
276 assert '\0' not in data
277 break
278 except:
279 pass
281 encoding, errors = self.get_encoding(
282 "Data is not valid %s. Please select the file's encoding."
283 "Turn on 'ignore errors' to try and load it anyway." % encoding)
285 self.buffer.insert_at_cursor(data, -1)
286 return 1
288 def load_file(self, path):
289 try:
290 if path == '-':
291 file = sys.stdin
292 else:
293 file = open(path, 'r')
294 contents = file.read()
295 if path != '-':
296 file.close()
297 self.insert_data(contents)
298 except Abort:
299 raise
300 except:
301 rox.report_exception()
302 raise Abort
304 def close(self, button = None):
305 if self.buffer.get_modified():
306 self.save(discard = 1)
307 else:
308 self.destroy()
310 def discard(self):
311 self.destroy()
313 def up(self, button = None):
314 if self.uri:
315 filer.show_file(self.uri)
316 else:
317 rox.alert("File is not saved to disk yet")
319 def has_selection(self):
320 s, e = self.get_selection_range()
321 return not e.equal(s)
323 def get_marked_range(self):
324 s = self.buffer.get_iter_at_mark(self.mark_start)
325 e = self.buffer.get_iter_at_mark(self.mark_end)
326 return s, e
328 def get_selection_range(self):
329 s = self.buffer.get_iter_at_mark(self.insert_mark)
330 e = self.buffer.get_iter_at_mark(self.selection_bound_mark)
331 return s, e
333 def save(self, widget = None, discard = 0):
334 from rox.saving import SaveBox
336 if self.savebox:
337 self.savebox.destroy()
339 if self.has_selection() and not discard:
340 saver = SelectionSaver(self)
341 self.savebox = SaveBox(saver, 'Selection', 'text/plain')
342 self.savebox.connect('destroy', lambda w: saver.destroy())
343 else:
344 uri = self.uri or default_name.value
345 self.savebox = SaveBox(self, uri, 'text/plain', discard)
346 self.savebox.show()
348 def help(self, button = None):
349 filer.open_dir(rox.app_dir + '/Help')
351 def save_to_stream(self, stream):
352 s = self.buffer.get_start_iter()
353 e = self.buffer.get_end_iter()
354 stream.write(self.buffer.get_text(s, e, TRUE))
356 def set_uri(self, uri):
357 self.uri = uri
358 self.buffer.set_modified(FALSE)
359 self.update_title()
361 def new(self):
362 EditWindow()
364 def change_font(self):
365 style = self.text.get_style().copy()
366 style.font = load_font(options.get('edit_font'))
367 self.text.set_style(style)
369 def show_options(self):
370 rox.edit_options()
372 def set_marked(self, start = None, end = None):
373 "Set the marked region (from the selection if no region is given)."
374 self.clear_marked()
375 assert not self.marked
377 buffer = self.buffer
378 if start:
379 assert end
380 else:
381 assert not end
382 start, end = self.get_selection_range()
383 buffer.move_mark(self.mark_start, start)
384 buffer.move_mark(self.mark_end, end)
385 buffer.apply_tag_by_name('marked',
386 buffer.get_iter_at_mark(self.mark_start),
387 buffer.get_iter_at_mark(self.mark_end))
388 self.marked = 1
390 def clear_marked(self):
391 if not self.marked:
392 return
393 self.marked = 0
394 buffer = self.buffer
395 buffer.remove_tag_by_name('marked',
396 buffer.get_iter_at_mark(self.mark_start),
397 buffer.get_iter_at_mark(self.mark_end))
399 def undo(self, widget = None):
400 self.buffer.undo()
402 def redo(self, widget = None):
403 self.buffer.redo()
405 def goto(self, widget = None):
406 from goto import Goto
407 self.set_minibuffer(Goto())
409 def search(self, widget = None):
410 from search import Search
411 self.set_minibuffer(Search())
413 def set_mini_label(self, label):
414 self.mini_label.set_text(label)
416 def set_minibuffer(self, minibuffer):
417 self.minibuffer = minibuffer
419 if minibuffer:
420 minibuffer.setup(self)
421 self.mini_entry.set_text('')
422 self.mini_entry.grab_focus()
423 self.mini_hbox.show_all()
424 else:
425 self.mini_hbox.hide()
426 self.text.grab_focus()
428 def mini_key_press(self, entry, kev):
429 if kev.keyval == g.keysyms.Escape:
430 self.set_minibuffer(None)
431 return 1
433 return self.minibuffer.key_press(kev)
435 def mini_changed(self, entry):
436 self.minibuffer.changed()
438 class SelectionSaver(Saveable):
439 def __init__(self, window):
440 self.window = window
441 window.set_marked()
443 def save_to_stream(self, stream):
444 s, e = self.window.get_marked_range()
445 stream.write(self.window.buffer.get_text(s, e, TRUE))
447 def destroy(self):
448 # Called when savebox is remove. Get rid of the selection marker
449 self.window.clear_marked()