1 """
2 Draws the applications main window
3
4 Also manages the callbacks from menu and GUI items
5
6 Copyright: John Stowers, 2006
7 License: GPLv2
8 """
9 import gobject
10 import gtk, gtk.glade
11 import gnome.ui
12 import os.path
13 import gettext
14 import threading
15 from gettext import gettext as _
16 import logging
17 log = logging.getLogger("gtkui.UI")
18
19 import conduit
20 import conduit.Web as Web
21 import conduit.Conduit as Conduit
22 import conduit.gtkui.Canvas as Canvas
23 import conduit.gtkui.MsgArea as MsgArea
24 import conduit.gtkui.Tree as Tree
25 import conduit.gtkui.ConflictResolver as ConflictResolver
26 import conduit.gtkui.Database as Database
27
28
29 DEFAULT_CONDUIT_BROWSER = "gtkmozembed"
30 DEVELOPER_WEB_LINKS = (
31
32 ("Introduction", "http://www.conduit-project.org/wiki/Development"),
33 ("Writing a Data Provider", "http://www.conduit-project.org/wiki/WritingADataProvider"),
34 ("API Documentation", "http://doc.conduit-project.org/conduit/"),
35 ("Test Results", "http://tests.conduit-project.org/")
36 )
37
38
39 for module in gtk.glade, gettext:
40 module.bindtextdomain('conduit', conduit.LOCALE_DIR)
41 module.textdomain('conduit')
42
44 """
45 The main conduit window.
46 """
47 - def __init__(self, conduitApplication, moduleManager, typeConverter, syncManager):
48 """
49 Constructs the mainwindow. Throws up a splash screen to cover
50 the most time consuming pieces
51 """
52 gnome.ui.authentication_manager_init()
53
54 self.conduitApplication = conduitApplication
55
56
57
58 icon_dirs = [
59 conduit.SHARED_DATA_DIR,
60 conduit.SHARED_MODULE_DIR,
61 os.path.join(conduit.SHARED_DATA_DIR,"icons"),
62 os.path.join(conduit.USER_DIR, "modules")
63 ]
64 for i in icon_dirs:
65 gtk.icon_theme_get_default().prepend_search_path(i)
66
67 self.gladeFile = os.path.join(conduit.SHARED_DATA_DIR, "conduit.glade")
68 self.widgets = gtk.glade.XML(self.gladeFile, "MainWindow")
69
70 dic = { "on_mainwindow_delete" : self.on_window_closed,
71 "on_mainwindow_state_event" : self.on_window_state_event,
72 "on_synchronize_activate" : self.on_synchronize_all_clicked,
73 "on_cancel_activate" : self.on_cancel_all_clicked,
74 "on_quit_activate" : self.on_window_closed,
75 "on_clear_canvas_activate" : self.on_clear_canvas,
76 "on_preferences_activate" : self.on_conduit_preferences,
77 "on_about_activate" : self.on_about_conduit,
78 "on_contents_activate" : self.on_help,
79 "on_save1_activate" : self.save_settings,
80 None : None
81 }
82 self.widgets.signal_autoconnect(dic)
83
84
85 self.type_converter = typeConverter
86 self.sync_manager = syncManager
87
88
89 self.mainWindow = self.widgets.get_widget("MainWindow")
90
91 if conduit.GLOBALS.settings.get("gui_use_rgba_colormap") == True:
92 screen = self.mainWindow.get_screen()
93 colormap = screen.get_rgba_colormap()
94 if colormap:
95 gtk.widget_set_default_colormap(colormap)
96 self.mainWindow.set_position(gtk.WIN_POS_CENTER)
97 self.mainWindow.set_icon_name("conduit")
98 title = "Conduit"
99 if conduit.IS_DEVELOPMENT_VERSION:
100 title = title + " - %s (Development Version)" % conduit.VERSION
101 if not conduit.IS_INSTALLED:
102 title = title + " - Running Uninstalled"
103 self.mainWindow.set_title(title)
104
105
106 self.canvasSW = self.widgets.get_widget("canvasScrolledWindow")
107 self.hpane = self.widgets.get_widget("hpaned1")
108
109
110 msg = MsgArea.MsgAreaController()
111 self.widgets.get_widget("mainVbox").pack_start(msg, False, False)
112 self.canvas = Canvas.Canvas(
113 parentWindow=self.mainWindow,
114 typeConverter=self.type_converter,
115 syncManager=self.sync_manager,
116 dataproviderMenu=gtk.glade.XML(self.gladeFile, "DataProviderMenu"),
117 conduitMenu=gtk.glade.XML(self.gladeFile, "ConduitMenu"),
118 msg=msg
119 )
120 self.canvasSW.add(self.canvas)
121 self.canvas.connect('drag-drop', self.drop_cb)
122 self.canvas.connect("drag-data-received", self.drag_data_received_data)
123
124
125 self.dataproviderTreeModel = Tree.DataProviderTreeModel()
126 dataproviderScrolledWindow = self.widgets.get_widget("scrolledwindow2")
127 self.dataproviderTreeView = Tree.DataProviderTreeView(self.dataproviderTreeModel)
128 dataproviderScrolledWindow.add(self.dataproviderTreeView)
129
130
131 self.conflictResolver = ConflictResolver.ConflictResolver(self.widgets)
132
133
134 self.cancelSyncButton = self.widgets.get_widget('cancel')
135 self.hpane.set_position(conduit.GLOBALS.settings.get("gui_hpane_postion"))
136 self.dataproviderTreeView.set_expand_rows()
137 self.window_state = 0
138
139
140
141 if conduit.IS_DEVELOPMENT_VERSION:
142 helpMenu = self.widgets.get_widget("help_menu")
143 developersMenuItem = gtk.ImageMenuItem("Developers")
144 developersMenuItem.set_image(
145 gtk.image_new_from_icon_name(
146 "applications-development",
147 gtk.ICON_SIZE_MENU))
148 developersMenu = gtk.Menu()
149 developersMenuItem.set_submenu(developersMenu)
150 helpMenu.prepend(developersMenuItem)
151 for name,url in DEVELOPER_WEB_LINKS:
152 item = gtk.ImageMenuItem(name)
153 item.set_image(
154 gtk.image_new_from_icon_name(
155 "applications-internet",
156 gtk.ICON_SIZE_MENU))
157 item.connect("activate",self.on_developer_menu_item_clicked,name,url)
158 developersMenu.append(item)
159
160
161 - def on_developer_menu_item_clicked(self, menuitem, name, url):
162 threading.Thread(
163 target=Web.LoginMagic,
164 args=(name, url),
165 kwargs={"login_function":lambda: True}
166 ).start()
167
168 - def on_conduit_added(self, syncset, cond):
169 cond.connect("sync-started", self.on_sync_started)
170 cond.connect("sync-completed", self.on_sync_completed)
171 cond.connect("sync-conflict", self.conflictResolver.on_conflict)
172
173 - def set_model(self, syncSet):
174 self.syncSet = syncSet
175 self.syncSet.connect("conduit-added", self.on_conduit_added)
176 self.canvas.set_sync_set(syncSet)
177
179 """
180 Present the main window. Enjoy your window
181 """
182 log.debug("Presenting GUI")
183 self.mainWindow.show_all()
184 self.mainWindow.present()
185
187 """
188 Iconifies the main window
189 """
190 log.debug("Iconifying GUI")
191 self.mainWindow.hide()
192
193 - def is_visible(self):
194 """
195 Returns True if mainWindow is visible
196 (not minimized or withdrawn)
197 """
198 minimized = self.window_state & gtk.gdk.WINDOW_STATE_ICONIFIED
199 return (not minimized) and self.mainWindow.get_property('visible')
200
201 - def on_sync_started(self, thread):
202 self.cancelSyncButton.set_property("sensitive", True)
203
204 - def on_sync_completed(self, thread, aborted, error, conflict):
205 self.cancelSyncButton.set_property(
206 "sensitive",
207 conduit.GLOBALS.syncManager.is_busy()
208 )
209
211 """
212 Synchronize all valid conduits on the canvas
213 """
214 self.conduitApplication.Synchronize()
215
216 - def on_cancel_all_clicked(self, widget):
217 """
218 Cancels all currently runnings syncs
219 """
220 self.conduitApplication.Cancel()
221
222 - def on_clear_canvas(self, widget):
223 """
224 Clear the canvas and start a new sync set
225 """
226 self.canvas.clear_canvas()
227
228 - def on_conduit_preferences(self, widget):
229 """
230 Show the properties of the current sync set (status, conflicts, etc
231 Edit the sync specific properties
232 """
233 def on_clear_button_clicked(sender, treeview, sqliteListStore):
234 treeview.set_model(None)
235 conduit.GLOBALS.mappingDB.delete()
236 treeview.set_model(sqliteListStore)
237
238
239 CONVERT_FROM_MESSAGE = _("Convert from")
240 CONVERT_INTO_MESSAGE = _("into")
241
242 convertables = self.type_converter.get_convertables_list()
243 converterListStore = gtk.ListStore( str )
244 for froms,tos in convertables:
245 string = "%s %s %s %s" % (CONVERT_FROM_MESSAGE, froms, CONVERT_INTO_MESSAGE, tos)
246 converterListStore.append( (string,) )
247 dataProviderListStore = gtk.ListStore( str, bool )
248
249 for i in conduit.GLOBALS.moduleManager.get_modules_by_type("sink","source","twoway"):
250 dataProviderListStore.append(("Name: %s\nDescription: %s)" % (i.name, i.description), True))
251
252 for f in conduit.GLOBALS.moduleManager.invalidFiles:
253 dataProviderListStore.append(("Error loading file: %s" % f, False))
254
255
256 tree = gtk.glade.XML(self.gladeFile, "PreferencesDialog")
257 notebook = tree.get_widget("prop_notebook")
258
259
260 if conduit.IS_DEVELOPMENT_VERSION:
261 vbox = gtk.VBox(False,5)
262
263
264
265 treeview = gtk.TreeView()
266 treeview.set_headers_visible(True)
267 treeview.set_fixed_height_mode(True)
268 index = 1
269 db = conduit.GLOBALS.mappingDB._db
270 for name in db.get_fields("mappings"):
271 column = gtk.TreeViewColumn(
272 name,
273 gtk.CellRendererText(),
274 text=index)
275 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
276 column.set_fixed_width(250)
277 treeview.append_column(column)
278 index = index + 1
279
280 store = Database.GenericDBListStore("mappings", db)
281 treeview.set_model(store)
282
283 sw = gtk.ScrolledWindow()
284 sw.add(treeview)
285 vbox.pack_start(sw,True,True)
286
287 clear = gtk.Button(None,gtk.STOCK_CLEAR)
288 clear.connect("clicked", on_clear_button_clicked, treeview, store)
289 vbox.pack_start(clear, False, False)
290
291 notebook.append_page(vbox,gtk.Label(_('Relationship Database')))
292
293 converterTreeView = tree.get_widget("dataConversionsTreeView")
294 converterTreeView.set_model(converterListStore)
295 converterTreeView.append_column(gtk.TreeViewColumn(_("Conversions Available"),
296 gtk.CellRendererText(),
297 text=0)
298 )
299 dataproviderTreeView = tree.get_widget("dataProvidersTreeView")
300 dataproviderTreeView.set_model(dataProviderListStore)
301 dataproviderTreeView.append_column(gtk.TreeViewColumn(_("Name"),
302 gtk.CellRendererText(),
303 text=0)
304 )
305 dataproviderTreeView.append_column(gtk.TreeViewColumn(_("Loaded"),
306 gtk.CellRendererToggle(),
307 active=1)
308 )
309
310
311 save_settings_check = tree.get_widget("save_settings_check")
312 save_settings_check.set_active(conduit.GLOBALS.settings.get("save_on_exit"))
313 status_icon_check = tree.get_widget("status_icon_check")
314 status_icon_check.set_active(conduit.GLOBALS.settings.get("show_status_icon"))
315 minimize_to_tray_check = tree.get_widget("minimize_to_tray_check")
316 minimize_to_tray_check.set_active(conduit.GLOBALS.settings.get("gui_minimize_to_tray"))
317 web_browser_check = tree.get_widget("web_check")
318 web_browser_check.set_active(conduit.GLOBALS.settings.get("web_login_browser") != "system")
319 show_hints_check = tree.get_widget("show_hints_check")
320 show_hints_check.set_active(conduit.GLOBALS.settings.get("gui_show_hints"))
321
322
323
324 for policyName in Conduit.CONFLICT_POLICY_NAMES:
325 currentValue = conduit.GLOBALS.settings.get("default_policy_%s" % policyName)
326 for policyValue in Conduit.CONFLICT_POLICY_VALUES:
327 name = "%s_%s" % (policyName,policyValue)
328 widget = tree.get_widget(name+"_radio")
329 widget.set_image(
330 gtk.image_new_from_icon_name(
331 Conduit.CONFLICT_POLICY_VALUE_ICONS[name],
332 gtk.ICON_SIZE_MENU))
333 if currentValue == policyValue:
334 widget.set_active(True)
335
336
337
338 for i in conduit.GLOBALS.moduleManager.dataproviderFactories:
339 widget = i.setup_configuration_widget()
340 if widget:
341 notebook.append_page(
342 widget,
343 gtk.Label(i.get_name()))
344
345 notebook.show_all()
346
347
348 dialog = tree.get_widget("PreferencesDialog")
349 dialog.set_transient_for(self.mainWindow)
350 response = dialog.run()
351 if response == gtk.RESPONSE_OK:
352 conduit.GLOBALS.settings.set("save_on_exit", save_settings_check.get_active())
353 conduit.GLOBALS.settings.set("show_status_icon", status_icon_check.get_active())
354 conduit.GLOBALS.settings.set("gui_minimize_to_tray", minimize_to_tray_check.get_active())
355 if web_browser_check.get_active():
356 conduit.GLOBALS.settings.set("web_login_browser", DEFAULT_CONDUIT_BROWSER)
357 else:
358 conduit.GLOBALS.settings.set("web_login_browser", "system")
359 conduit.GLOBALS.settings.set("gui_show_hints", show_hints_check.get_active())
360
361 for policyName in Conduit.CONFLICT_POLICY_NAMES:
362 for policyValue in Conduit.CONFLICT_POLICY_VALUES:
363 name = "%s_%s" % (policyName,policyValue)
364 if tree.get_widget(name+"_radio").get_active() == True:
365 conduit.GLOBALS.settings.set(
366 "default_policy_%s" % policyName,
367 policyValue)
368
369
370 for factory in conduit.GLOBALS.moduleManager.dataproviderFactories:
371 factory.save_configuration(response == gtk.RESPONSE_OK)
372
373 dialog.destroy()
374
375
376 - def on_about_conduit(self, widget):
377 """
378 Display about dialog
379 """
380 dialog = AboutDialog()
381 dialog.set_transient_for(self.mainWindow)
382 dialog.run()
383 dialog.destroy()
384
385 - def on_help(self, widget):
386 """
387 Display help
388 """
389 if conduit.IS_INSTALLED:
390 uri = "ghelp:conduit"
391 else:
392
393 uri = "ghelp:%s" % os.path.join(conduit.DIRECTORY,"help","C","conduit.xml")
394 log.debug("Launching help: %s" % uri)
395 gobject.spawn_async(
396 argv=("xdg-open",uri),
397 flags=gobject.SPAWN_SEARCH_PATH | gobject.SPAWN_STDOUT_TO_DEV_NULL | gobject.SPAWN_STDERR_TO_DEV_NULL
398 )
399
400 - def on_window_state_event(self, widget, event):
401 visible = self.is_visible()
402 self.window_state = event.new_window_state
403 if self.window_state & gtk.gdk.WINDOW_STATE_ICONIFIED and visible:
404 if conduit.GLOBALS.settings.get("gui_minimize_to_tray"):
405 self.minimize_to_tray()
406
407 - def on_window_closed(self, widget, event=None):
408 """
409 Check if there are any synchronizations currently in progress and
410 ask the user if they wish to cancel them
411 """
412 busy = False
413 quit = False
414 for c in self.syncSet.get_all_conduits():
415 if c.is_busy():
416 busy = True
417
418 if busy:
419 dialog = gtk.MessageDialog(
420 self.mainWindow,
421 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
422 gtk.MESSAGE_QUESTION,
423 gtk.BUTTONS_YES_NO,_("Synchronization in progress. Do you want to cancel it?")
424 )
425 response = dialog.run()
426 if response == gtk.RESPONSE_YES:
427 quit = True
428 else:
429
430 dialog.destroy()
431 return True
432 else:
433 quit = True
434
435
436
437
438 if quit:
439 self.conduitApplication.Quit()
440
441
442 - def drop_cb(self, wid, context, x, y, time):
443 """
444 drop cb
445 """
446 self.canvas.drag_get_data(context, context.targets[0], time)
447 return True
448
449 - def drag_data_received_data(self, treeview, context, x, y, selection, info, etime):
450 """
451 DND
452 """
453 dataproviderKey = selection.data
454
455
456 if dataproviderKey != "":
457
458 scroll = self.canvasSW.get_vadjustment().get_value()
459
460
461 new = conduit.GLOBALS.moduleManager.get_module_wrapper_with_instance(dataproviderKey)
462 self.canvas.add_dataprovider_to_canvas(
463 dataproviderKey,
464 new,
465 x,
466 int(scroll) + y
467 )
468
469 context.finish(True, True, etime)
470 return
471
472 - def save_settings(self, widget):
473 """
474 Saves the application settings to an XML document.
475 Saves the GUI settings (window state, position, etc to gconf)
476 """
477
478 self.syncSet.save_to_xml()
479
480
481 conduit.GLOBALS.settings.set(
482 "gui_hpane_postion",
483 self.hpane.get_position())
484 conduit.GLOBALS.settings.set(
485 "gui_window_size",
486 self.mainWindow.get_size())
487 conduit.GLOBALS.settings.set(
488 "gui_expanded_rows",
489 self.dataproviderTreeView.get_expanded_rows())
490
492 """
493 Simple splash screen class which shows an image for a predetermined period
494 of time or until L{SplashScreen.destroy} is called.
495
496 Code adapted from banshee
497 """
498 DELAY = 1500
500 """
501 Constructor
502 """
503
504 self.destroyed = True
505
507 """
508 Builds the splashscreen and connects the splash window to be destroyed
509 via a timeout callback in L{SplashScreen.DELAY}msec time.
510
511 The splash can also be destroyed manually by the application
512 """
513 self.wSplash = gtk.Window(gtk.WINDOW_POPUP)
514 self.wSplash.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_SPLASHSCREEN)
515 self.wSplash.set_decorated(False)
516
517 wSplashScreen = gtk.Image()
518 wSplashScreen.set_from_file(os.path.join(conduit.SHARED_DATA_DIR,"conduit-splash.png"))
519
520
521 wSplashFrame = gtk.Frame()
522 wSplashFrame.set_shadow_type(gtk.SHADOW_OUT)
523 wSplashFrame.add(wSplashScreen)
524 self.wSplash.add(wSplashFrame)
525
526
527 self.wSplash.set_position(gtk.WIN_POS_CENTER)
528
529
530
531 self.destroyed = False
532
533 self.wSplash.show_all()
534
535 while gtk.events_pending():
536 gtk.main_iteration()
537
538 gobject.timeout_add(SplashScreen.DELAY,self.destroy)
539
541 """
542 Destroys the splashscreen. Can be safely called manually (prior to)
543 or via the timer callback
544 """
545 if not self.destroyed:
546 self.wSplash.destroy()
547 self.destroyed = True
548
551 gtk.AboutDialog.__init__(self)
552 self.set_name("Conduit")
553 self.set_version(conduit.VERSION)
554 self.set_comments("Synchronisation for GNOME")
555 self.set_website("http://www.conduit-project.org")
556 self.set_authors([
557 "John Stowers",
558 "John Carr",
559 "Thomas Van Machelen",
560 "Jonny Lamb"])
561 self.set_artists([
562 "John Stowers",
563 "mejogid",
564 "The Tango Project (http://tango.freedesktop.org)"])
565 self.set_logo_icon_name("conduit")
566
568 - def __init__(self, conduitApplication):
569 gtk.StatusIcon.__init__(self)
570
571
572 gtk.icon_theme_get_default().prepend_search_path(conduit.SHARED_DATA_DIR)
573
574 self.conduitApplication = conduitApplication
575 menu = '''
576 <ui>
577 <menubar name="Menubar">
578 <menu action="Menu">
579 <menuitem action="Sync"/>
580 <menuitem action="Cancel"/>
581 <menuitem action="Quit"/>
582 <separator/>
583 <menuitem action="About"/>
584 </menu>
585 </menubar>
586 </ui>
587 '''
588 actions = [
589 ('Menu', None, 'Menu'),
590 ('Sync', gtk.STOCK_EXECUTE, _("_Synchronize All"), None, _("Synchronizes All Groups"), self.on_synchronize),
591 ('Cancel', gtk.STOCK_CANCEL, _("_Cancel Synchronization"), None, _("Cancels Currently Synchronizing Groups"), self.on_cancel),
592 ('Quit', gtk.STOCK_QUIT, _("_Quit"), None, _("Close Conduit"), self.on_quit),
593 ('About', gtk.STOCK_ABOUT, _("_About"), None, _("About Conduit"), self.on_about)]
594 ag = gtk.ActionGroup('Actions')
595 ag.add_actions(actions)
596 self.manager = gtk.UIManager()
597 self.manager.insert_action_group(ag, 0)
598 self.manager.add_ui_from_string(menu)
599 self.menu = self.manager.get_widget('/Menubar/Menu/About').props.parent
600 self.cancelButton = self.manager.get_widget('/Menubar/Menu/Cancel')
601 self.connect('popup-menu', self.on_popup_menu)
602 self.connect('activate', self.on_click)
603
604 self.animating = False
605 self.check_animate = False
606 self.conflict = False
607 self.animated_idx = 0
608 self.animated_icons = range(1,8)
609
610
611 self.cancelButton.set_property("sensitive", False)
612 self.set_from_icon_name("conduit")
613 self.set_tooltip("Conduit")
614 self.set_visible(True)
615
617
618 if self.animating:
619 if self.animated_idx == self.animated_icons[-1]:
620 self.animated_idx = 1
621 else:
622 self.animated_idx += 1
623 self.set_from_icon_name("conduit-progress-%d" % self.animated_idx)
624
625 if self.check_animate:
626 self.animating = conduit.GLOBALS.syncManager.is_busy()
627 self.check_animate = False
628 return True
629
630 else:
631 if self.conflict:
632 self.set_from_icon_name("dialog-error")
633 self.set_tooltip(_("Synchronization Error"))
634 else:
635 self.set_from_icon_name("conduit")
636 self.set_tooltip(_("Synchronization Complete"))
637 self.conflict = False
638 self.cancelButton.set_property("sensitive", False)
639 return False
640
645
648
650 if not self.animating:
651 self.animating = True
652 self.set_tooltip(_("Synchronizing"))
653 self.cancelButton.set_property("sensitive", True)
654 gobject.timeout_add(100, self._animate_icon_timeout)
655
657
658 self.check_animate = True
659
662
665
667 self.conduitApplication.Cancel()
668
670 self.menu.popup(None, None, None, button, time)
671
673 self.conduitApplication.Quit()
674
679
681 if self.conduitApplication.HasGUI():
682 if self.conduitApplication.gui.is_visible():
683 self.conduitApplication.gui.minimize_to_tray()
684 else:
685 self.conduitApplication.gui.present()
686 else:
687 self.conduitApplication.BuildGUI()
688 self.conduitApplication.ShowGUI()
689
692
695