Merge commit 'origin/master'
[reinteract/rox.git] / lib / reinteract / main.py
blob9bdfe6a2db44339f9f64704b8014cc7256f8cdee
1 import gtk
2 import pango
4 import logging
5 import os
6 import stdout_capture
7 import sys
9 from notebook import Notebook
10 from shell_buffer import ShellBuffer
11 from shell_view import ShellView
13 from format_escaped import format_escaped
14 from optparse import OptionParser
16 stdout_capture.init()
18 usage = "usage: %prog [options]"
19 op = OptionParser(usage=usage)
20 op.add_option("-u", "--ui", type="choice", choices=("standard", "hildon", "rox"),
21 default="standard", help=("which user interface to use (standard, rox or "
22 "hildon), default=%default"))
23 op.add_option("-d", "--debug", action="store_true",
24 help=("enable internal debug messages"))
26 options, args = op.parse_args()
27 use_hildon = False
28 use_rox = False
30 if options.debug:
31 logging.basicConfig(level=logging.DEBUG)
33 if options.ui == "hildon":
34 try:
35 import hildon
36 use_hildon = True
37 except ImportError, e:
38 print >>sys.stderr, "Error importing hildon. Falling back to standard ui."
39 elif options.ui == "rox":
40 import rox_ui
41 use_rox = True
43 notebook = Notebook()
45 if use_hildon:
46 w = hildon.Window()
47 else:
48 w = gtk.Window()
50 v = gtk.VBox()
51 w.add(v)
53 buf = ShellBuffer(notebook)
54 view = ShellView(buf)
55 view.modify_font(pango.FontDescription("monospace"))
56 buf = view.get_buffer()
58 ui_manager = gtk.UIManager()
59 w.add_accel_group(ui_manager.get_accel_group())
61 def quit():
62 if not confirm_discard('Save the unchanged changes to worksheet "%s" before quitting?', '_Quit without saving'):
63 return
64 gtk.main_quit()
66 def on_quit(action):
67 quit()
69 def on_cut(action):
70 view.emit('cut-clipboard')
72 def on_copy(action):
73 view.emit('copy-clipboard')
75 def on_copy_as_doctests(action):
76 view.get_buffer().copy_as_doctests(view.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD))
78 def on_paste(action):
79 view.emit('paste-clipboard')
81 def on_delete(action):
82 buf.delete_selection(True, view.get_editable())
84 def confirm_discard(message_format, continue_button_text):
85 if not buf.code_modified:
86 return True
88 if buf.filename == None:
89 save_button_text = gtk.STOCK_SAVE_AS
90 else:
91 save_button_text = gtk.STOCK_SAVE
93 if buf.filename == None:
94 name = "Unsaved Worksheet"
95 else:
96 name = buf.filename
98 message = format_escaped("<big><b>" + message_format + "</b></big>", name)
100 dialog = gtk.MessageDialog(parent=w, buttons=gtk.BUTTONS_NONE,
101 type=gtk.MESSAGE_WARNING)
102 dialog.set_markup(message)
104 dialog.add_buttons(continue_button_text, gtk.RESPONSE_OK,
105 gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
106 save_button_text, 1)
107 dialog.set_default_response(1)
108 response = dialog.run()
109 dialog.destroy()
111 if response == gtk.RESPONSE_OK:
112 return True
113 elif response == 1:
114 if buf.filename == None:
115 save_as()
116 else:
117 buf.save()
119 if buf.code_modified:
120 return False
121 else:
122 return True
123 else:
124 return False
126 def on_new(action):
127 if not confirm_discard('Discard unsaved changes to worksheet "%s"?', '_Discard'):
128 return
130 buf.clear()
132 def load(filename):
133 notebook.set_path([os.path.dirname(os.path.abspath(filename))])
134 if not os.path.exists(filename):
135 buf.filename = filename
136 update_title()
137 else:
138 buf.load(filename)
139 calculate()
141 def on_open(action):
142 if not confirm_discard('Discard unsaved changes to worksheet "%s"?', '_Discard'):
143 return
145 if use_hildon:
146 chooser = hildon.FileChooserDialog(w, gtk.FILE_CHOOSER_ACTION_OPEN)
147 else:
148 chooser = gtk.FileChooserDialog("Open Worksheet...", w, gtk.FILE_CHOOSER_ACTION_OPEN,
149 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
150 gtk.STOCK_OPEN, gtk.RESPONSE_OK))
151 chooser.set_default_response(gtk.RESPONSE_OK)
152 response = chooser.run()
153 filename = None
154 if response == gtk.RESPONSE_OK:
155 filename = chooser.get_filename()
157 if filename != None:
158 load(filename)
160 chooser.destroy()
162 def on_save(action):
163 if buf.filename == None:
164 on_save_as(action)
165 else:
166 buf.save()
168 def save_as():
169 if use_rox:
170 rox_ui.save_with_rox_savebox(buf, notebook)
171 return
173 if use_hildon:
174 chooser = hildon.FileChooserDialog(w, gtk.FILE_CHOOSER_ACTION_SAVE)
175 else:
176 chooser = gtk.FileChooserDialog("Save As...", w, gtk.FILE_CHOOSER_ACTION_SAVE,
177 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
178 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
179 chooser.set_default_response(gtk.RESPONSE_OK)
180 response = chooser.run()
181 filename = None
182 if response == gtk.RESPONSE_OK:
183 filename = chooser.get_filename()
185 if filename != None:
186 buf.save(filename)
187 notebook.set_path([os.path.dirname(os.path.abspath(filename))])
189 chooser.destroy()
191 def on_save_as(action):
192 save_as()
194 def find_program_in_path(progname):
195 try:
196 path = os.environ['PATH']
197 except KeyError:
198 path = os.defpath
200 for dir in path.split(os.pathsep):
201 p = os.path.join(dir, progname)
202 if os.path.exists(p):
203 return p
205 return None
207 def find_url_open_program():
208 for progname in ['xdg-open', 'htmlview', 'gnome-open']:
209 path = find_program_in_path(progname)
210 if path != None:
211 return path
212 return None
214 def open_url(dialog, url):
215 prog = find_url_open_program()
216 os.spawnl(os.P_NOWAIT, prog, prog, url)
218 def on_about(action):
219 if find_url_open_program() != None:
220 gtk.about_dialog_set_url_hook(open_url)
222 dialog = gtk.AboutDialog()
223 dialog.set_transient_for(w)
224 dialog.set_name("Reinteract")
225 dialog.set_copyright("Copyright \302\251 2007 Owen Taylor, Red Hat, Inc., and others")
226 dialog.set_website("http://www.reinteract.org")
227 dialog.connect("response", lambda d, r: d.destroy())
228 dialog.run()
230 def calculate():
231 buf.calculate()
233 # This is a hack to work around the fact that scroll_mark_onscreen()
234 # doesn't wait for a size-allocate cycle, so doesn't properly handle
235 # embedded request widgets
236 view.size_request()
237 view.size_allocate((view.allocation.x, view.allocation.y,
238 view.allocation.width, view.allocation.height))
240 view.scroll_mark_onscreen(buf.get_insert())
242 def on_calculate(action):
243 calculate()
245 action_group = gtk.ActionGroup("main")
246 action_group.add_actions([
247 ('file', None, "_File"),
248 ('edit', None, "_Edit"),
249 ('help', None, "_Help"),
250 ('new', gtk.STOCK_NEW, None, None, None, on_new),
251 ('open', gtk.STOCK_OPEN, None, None, None, on_open),
252 ('save', gtk.STOCK_SAVE, None, None, None, on_save),
253 ('save-as', gtk.STOCK_SAVE_AS, None, None, None, on_save_as),
254 ('quit', gtk.STOCK_QUIT, None, None, None, on_quit),
255 ('cut', gtk.STOCK_CUT, None, None, None, on_cut),
256 ('copy', gtk.STOCK_COPY, None, None, None, on_copy),
257 ('copy-as-doctests',
258 gtk.STOCK_COPY,
259 "Copy as _Doctests",
260 "<control><shift>c",
261 None,
262 on_copy_as_doctests),
263 ('paste', gtk.STOCK_PASTE, None, None, None, on_paste),
264 ('delete', gtk.STOCK_DELETE, None, None, None, on_delete),
265 ('about', gtk.STOCK_ABOUT, None, None, None, on_about),
266 ('calculate', gtk.STOCK_REFRESH, "_Calculate", '<control>Return', None, on_calculate),
269 ui_manager.insert_action_group(action_group, 0)
271 if use_hildon:
272 menu_element = 'popup'
273 else:
274 menu_element = 'menubar'
276 ui_string="""
277 <ui>
278 <%(menu_element)s name="TopMenu">
279 <menu action="file">
280 <menuitem action="new"/>
281 <menuitem action="open"/>
282 <separator/>
283 <menuitem action="save"/>
284 <menuitem action="save-as"/>
285 <separator/>
286 <menuitem action="quit"/>
287 </menu>
288 <menu action="edit">
289 <menuitem action="cut"/>
290 <menuitem action="copy"/>
291 <menuitem action="copy-as-doctests"/>
292 <menuitem action="paste"/>
293 <menuitem action="delete"/>
294 <separator/>
295 <menuitem action="calculate"/>
296 </menu>
297 <menu action="help">
298 <menuitem action="about"/>
299 </menu>
300 </%(menu_element)s>
301 <toolbar name="ToolBar">
302 <toolitem action="calculate"/>
303 </toolbar>
304 </ui>
305 """ % { 'menu_element': menu_element }
307 ui_manager.add_ui_from_string(ui_string)
308 ui_manager.ensure_update()
310 menu = ui_manager.get_widget("/TopMenu")
311 toolbar = ui_manager.get_widget("/ToolBar")
313 if use_hildon:
314 w.set_menu(menu)
315 w.add_toolbar(toolbar)
316 else:
317 v.pack_start(menu, expand=False, fill=False)
318 v.pack_start(toolbar, expand=False, fill=False)
320 sw = gtk.ScrolledWindow()
321 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
322 v.pack_start(sw, expand=True, fill=True)
324 sw.add(view)
326 w.set_default_size(700, 800)
328 v.show_all()
329 view.grab_focus()
331 def update_title(*args):
332 if buf.code_modified:
333 title = "*"
334 else:
335 title = ""
337 if buf.filename == None:
338 title += "Unsaved Worksheet"
339 else:
340 title += os.path.basename(buf.filename)
342 title += " - Reinteract"
344 w.set_title(title)
346 buf.connect('filename-changed', update_title)
347 buf.connect('code-modified-changed', update_title)
349 update_title()
351 # We have a <Control>Return accelerator, but this hooks up <Control>KP_Enter as well;
352 # maybe someone wants that
353 def on_key_press_event(window, event):
354 if (event.keyval == 0xff0d or event.keyval == 0xff8d) and (event.state & gtk.gdk.CONTROL_MASK != 0):
355 calculate()
356 return True
357 return False
359 w.connect('key-press-event', on_key_press_event)
361 if len(args) > 0:
362 load(args[0])
363 else:
364 # If you run reinteract from the command line, you'd expect to be able to
365 # create a worksheet, test it, then save it in the current directory, and
366 # have that act the same as loading the worksheet to start with. This is
367 # less obviously right when run from a menu item.
368 notebook.set_path([os.getcwd()])
370 if use_hildon:
371 settings = w.get_settings()
372 settings.set_property("gtk-button-images", False)
373 settings.set_property("gtk-menu-images", False)
375 w.show()
377 def on_delete_event(window, event):
378 quit()
379 return True
381 w.connect('delete-event', on_delete_event)
383 gtk.main()