1
2
3 """
4 Graphical User Interface to PISI based on GTK - one implementation of user interaction
5
6 This file is part of Pisi.
7
8 It is a very basic GUI, which only allows for selection of two data sources and after initiation
9 of sync shows a progress bar. What else do we need?
10
11 Pisi is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
15
16 Pisi is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
20
21 You should have received a copy of the GNU General Public License
22 along with Pisi. If not, see <http://www.gnu.org/licenses/>.
23 """
24
25 import pygtk
26 pygtk.require('2.0')
27 import gtk
28
29 from pisiconstants import *
30 import pisiprogress
31 import pisi
32 import os
33 import os.path
34
35 -class Base(pisiprogress.AbstractCallback):
36 """
37 The one and only Main frame
38
39 Made up mainly by a notebook; one side for each type of PIM data, and by a progress bar and some buttons.
40 """
42 """
43 Constructor - the whole assembling of the window is performed in here
44 """
45 pisiprogress.AbstractCallback.__init__(self)
46 config = pisi.getConfiguration()
47 self.sources = {}
48 self.sourcesContacts = []
49 self.sourcesCalendar = []
50 for con in config.sections():
51 self.sources[config.get(con,'description')] = [con, config.get(con,'module') ]
52 if config.get(con,'module').startswith('calendar'):
53 self.sourcesCalendar.append(config.get(con,'description'))
54 elif config.get(con,'module').startswith('contacts'):
55 self.sourcesContacts.append(config.get(con,'description'))
56
57 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
58 self.window.connect("delete_event", self.delete_event)
59 self.window.connect("destroy", self.destroy)
60 self.window.set_border_width(10)
61
62 box = gtk.VBox(False, 5)
63
64 labelTitle= gtk.Label("PISI Synchronization")
65 box.pack_start(labelTitle, False, False, 0)
66 labelTitle.show()
67
68 contactsPanel = self._createContactsPanel(self.sourcesContacts)
69 paddingBox1 = gtk.HBox(False, 5)
70 paddingBox1.pack_start(contactsPanel, False, True, 5)
71 paddingBox1.show()
72 calendarPanel = self._createCalendarPanel(self.sourcesCalendar)
73 paddingBox2 = gtk.HBox(False, 5)
74 paddingBox2.pack_start(calendarPanel, False, True, 5)
75 paddingBox2.show()
76 self.notebook = gtk.Notebook()
77 self.notebook.append_page(paddingBox1, gtk.Label('Contacts'))
78 self.notebook.append_page(paddingBox2, gtk.Label('Calendar'))
79 self.notebook.show()
80
81 box.pack_start(self.notebook, False, True, 5)
82
83 separator = gtk.HSeparator()
84 box.pack_start(separator, False, True, 5)
85 separator.show()
86
87 labelProgress = gtk.Label("Progress")
88 labelProgress.set_alignment(0, 0)
89 box.pack_start(labelProgress, False, False, 0)
90 labelProgress.show()
91 self.progressbar = gtk.ProgressBar(adjustment=None)
92 self.progressbar.set_text("idle")
93 self.progressbar.set_fraction(0)
94 box.pack_start(self.progressbar, False, False, 0)
95 self.progressbar.show()
96
97 separator = gtk.HSeparator()
98 box.pack_start(separator, False, True, 5)
99 separator.show()
100
101 boxButtons = gtk.HBox(False, 5)
102 bAbout = gtk.Button('About')
103 bAbout.connect('clicked', self.showAbout)
104 boxButtons.pack_start(bAbout, True, False, 0)
105 bAbout.show()
106
107 bStart = gtk.Button('Start')
108 bStart.connect('clicked', self.startSync)
109 boxButtons.pack_start(bStart, True, False, 0)
110 bStart.show()
111
112 bQuit = gtk.Button('Quit')
113 bQuit.connect_object("clicked", gtk.Widget.destroy, self.window)
114 boxButtons.pack_start(bQuit, True, False, 0)
115 bQuit.show()
116
117 boxButtons.show()
118 box.pack_start(boxButtons, False, False, 0)
119
120 box.show()
121 self.window.add(box)
122 self.window.show()
123
168
170 """
171 Creates one side in the notebook - the one for setting up calendar synchronization
172 """
173 box = gtk.VBox(False, 5)
174 label = gtk.Label("Source A")
175 label.set_alignment(0, 0)
176 box.pack_start(label, False, False, 0)
177 label.show()
178 self.calendar_combobox1 = gtk.combo_box_new_text()
179 for sourceDesc in sources:
180 self.calendar_combobox1.append_text(sourceDesc)
181 self.calendar_combobox1.set_active(0)
182 box.pack_start(self.calendar_combobox1, False, False, 0)
183 self.calendar_combobox1.show()
184
185 label = gtk.Label("Source B")
186 label.set_alignment(0, 0)
187 box.pack_start(label, False, False, 0)
188 label.show()
189 self.calendar_combobox2 = gtk.combo_box_new_text()
190 for sourceDesc in sources:
191 self.calendar_combobox2.append_text(sourceDesc)
192 self.calendar_combobox2.set_active(1)
193 box.pack_start(self.calendar_combobox2, False, False, 0)
194 self.calendar_combobox2.show()
195
196 box.show()
197 return box
198
200 """
201 Starts up the application ('Main-Loop')
202 """
203 gtk.main()
204
205 - def destroy(self, widget, data=None):
206 """
207 Shuts down the application
208 """
209 gtk.main_quit()
210
212 """
213 Event handler
214 """
215 return False
216
217
238
240 """
241 Passes control on to PISI core and starts up synchronization process
242
243 Updates on the GUI are from now on only performed on request of the core by calling the callback functions.
244 """
245 self.verbose('Configuring')
246 self.progress.reset()
247 self.progress.setProgress(0)
248 self.update('Configuring')
249 self.verbose('Starting synchronization')
250 self.verbose('My configuration is:')
251 page = self.notebook.get_current_page()
252 modulesToLoad = []
253 modulesNamesCombined = ""
254 mergeMode = 0
255 if page == 0:
256 mode = 1
257 if len(self.sourcesContacts) < 2:
258 self.progress.setProgress(0)
259 self.update('Error')
260 self.message("You cannot synchronize this type of PIM information as you do not have enough data sources available / configured.")
261 return
262 source1 = self.contacts_combobox1.get_active()
263 source1 = self.sourcesContacts[source1]
264 source1 = self.sources[source1][0]
265 source2 = self.contacts_combobox2.get_active()
266 source2 = self.sourcesContacts[source2]
267 source2 = self.sources[source2][0]
268 mergeMode = self.contacts_combobox3.get_active()
269 elif page == 1:
270 mode = 0
271 if len(self.sourcesCalendar) < 2:
272 self.progress.setProgress(0)
273 self.update('Error')
274 self.message("You cannot synchronize this type of PIM information as you do not have enough data sources available / configured.")
275 return
276 source1 = self.calendar_combobox1.get_active()
277 source1 = self.sourcesCalendar[source1]
278 source1 = self.sources[source1][0]
279 source2 = self.calendar_combobox2.get_active()
280 source2 = self.sourcesCalendar[source2]
281 source2 = self.sources[source2][0]
282
283 if source1 == source2:
284 self.progress.setProgress(0)
285 self.update('Error')
286 self.error("You cannot choose one source for synchronization twice. Please make sure that two different sources are chosen for synchronization.")
287 return
288
289 self.verbose('\tMode is %d - %s' %(mode, MODE_STRINGS[mode]))
290 self.verbose( ('\tIn case of conflicts I use the following strategy: %s' %(MERGEMODE_STRINGS[mergeMode])))
291 modulesToLoad.append(source1)
292 modulesToLoad.append(source2)
293 modulesNamesCombined += source1
294 modulesNamesCombined += source2
295
296 config, configfolder = pisi.readConfiguration()
297 source = pisi.importModules(configfolder, config, modulesToLoad, modulesNamesCombined, False)
298
299 self.progress.push(8, 10)
300 self.update('Pre-Processing sources')
301 self.verbose('Pre-Processing sources')
302 self.verbose("\tSource 1")
303 source[0].preProcess()
304 self.verbose("\tSource 2")
305 source[1].preProcess()
306 self.verbose(" Pre-Processing Done")
307 self.progress.drop()
308
309 self.progress.push(10, 40)
310 self.update('Loading')
311 self.verbose("\n PHASE 1 - Loading ")
312 try:
313 self.progress.push(0, 50)
314 source[0].load()
315 self.progress.drop()
316 except BaseException, m:
317 if not self.promptGenericConfirmation("The following error occured when loading:\n%s\nContinue processing?" %(m.message)):
318 self.progress.reset()
319 self.update("Error")
320 return
321 self.progress.drop()
322 try:
323 self.progress.push(50, 100)
324 self.update('Loading')
325 source[1].load()
326 self.progress.drop()
327 except BaseException, m:
328 if not self.promptGenericConfirmation("The following error occured when loading:\n%s\nContinue processing?" %(m.message)):
329 self.progress.reset()
330 self.update("Error")
331 return
332 self.progress.drop()
333 self.progress.drop()
334
335 self.progress.push(40, 70)
336 self.update('Comparing')
337 self.verbose("\n PHASE 2 - Comparing ")
338 if mode == MODE_CALENDAR:
339 pisi.eventsSync.syncEvents(True, modulesToLoad, source)
340 elif mode == MODE_CONTACTS:
341 pisi.contactsSync.syncContacts(True, modulesToLoad, source, mergeMode)
342 self.progress.drop()
343
344 self.progress.push(70, 95)
345 self.update('Storing')
346 self.verbose ("\n PHASE 3 - Saving ")
347 try:
348 pisi.applyChanges(source)
349 self.verbose( " DONE ")
350 self.progress.drop()
351
352 self.progress.push(95, 100)
353 self.update('Post-Processing sources')
354 self.verbose('Post-Processing sources')
355 self.verbose("\tSource 1")
356 source[0].postProcess()
357 self.verbose("\tSource 2")
358 source[1].postProcess()
359 self.verbose(" Post-Processing Done")
360 self.progress.drop()
361
362 self.progress.setProgress(100)
363 self.update('Finished')
364 except BaseException, m:
365 self.error(str(m.message))
366 self.progress.reset()
367 self.update("Error")
368 return
369
370
371
372
373
374
375
376 - def message(self, st, messageType = gtk.MESSAGE_INFO):
377 """
378 Output a message to the user - a message dialog is popped up
379 """
380 dialog = gtk.MessageDialog(self.window, buttons=gtk.BUTTONS_OK, message_format = st, flags = gtk.DIALOG_MODAL, type = messageType)
381 ret = dialog.run()
382 dialog.destroy()
383
385 """
386 Output a message (of lower interest) to the user - output is directed to text based console.
387 """
388 print st
389
391 """
392 Redirect to L{message} with special dialog type
393 """
394 print "** Error: %s" %(str(st))
395 self.message(str(st), gtk.MESSAGE_ERROR)
396
398 """
399 Ask user for a single confirmation (OK / Cancel)
400 """
401 d = gtk.MessageDialog(self.window, type = gtk.MESSAGE_QUESTION, buttons = gtk.BUTTONS_YES_NO, message_format = prompt)
402 ret = d.run()
403 d.destroy()
404 return ret == gtk.RESPONSE_YES
405
407 """
408 Prompt the user for a single entry to type in - not implemented in GUI
409 """
410 raise ValueError("Prompt Generic not implemented for this GUI.")
411
413 """
414 Prompt the user for providing a file name - the standard file selection dialog of GTK is used for this
415 """
416 d = gtk.FileChooserDialog(prompt, self.window, buttons =(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK) )
417 d.set_select_multiple(False)
418 ret = d.run()
419 filename = d.get_filename()
420 d.destroy()
421 if ret == gtk.RESPONSE_CANCEL or ret == -4:
422 raise ValueError("File selection dialog - no file was chosen (%s)." %(prompt))
423 return filename
424
426 """
427 Use interaction for choosing which contact entry to keep in case of a conflict
428
429 An instance of L{ConflictsDialog} is started up with the information in question.
430
431 @return: A dictionary which contains the action for each conflict entry; the key is the id of the contact entry,
432 the value one out of a - keep entry from first source, b - keep value from second source and s - skip this entry (no change on either side)
433 """
434 if len(idList) == 0:
435 return {}
436 d = ConflictsDialog(self.window, source, idList)
437 ret = d.run()
438 reactions = d.getReactions()
439 d.destroy()
440 return reactions
441
443 """
444 This function should be called whenever new information has been made available and the UI should be updates somehow.
445
446 The progress bar is updated and the messages is put insight the progress bar. Finally, the GUI is forced to update all components.
447 """
448 prog = self.progress.calculateOverallProgress()
449 self.progressbar.set_text("%s (%d %%)" %(status, prog ))
450 self.progressbar.set_fraction(prog / 100.0)
451 while gtk.events_pending():
452 gtk.main_iteration(False)
453
455 """
456 GTK-Dialog to visualize a list of conflicting contact entries in a table view with options to select actions for each entry
457 """
458
459 - def __init__(self, parent, source, idList):
460 """
461 Contructor - all components are assembled in here
462 """
463 gtk.Dialog.__init__(self, "Please resolve conflicts", parent, gtk.DIALOG_MODAL , (gtk.STOCK_OK,gtk.RESPONSE_OK))
464 self._idList = idList
465 self._source= source
466
467 self.set_size_request(500, 300)
468 scrolled_window = gtk.ScrolledWindow()
469 scrolled_window.set_border_width(10)
470 scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
471 self.vbox.pack_start(scrolled_window, True, True, 0)
472 scrolled_window.show()
473
474 table = gtk.Table(3, len(idList), True)
475 table.set_row_spacings(10)
476 table.set_col_spacings(10)
477
478 scrolled_window.add_with_viewport(table)
479
480 y = 0
481 self.combos = []
482 self.entries = []
483 self.entriesForButton = {}
484 for entryID in idList:
485 entry1 = source[0].getContact(entryID)
486 entry2 = source[1].getContact(entryID)
487 self.entries.append([entry1, entry2])
488
489 l = gtk.Label("%s, %s" %(entry1.attributes['lastname'],entry1.attributes['firstname']))
490 l.show()
491 table.attach(l, 0, 1, y, y+1)
492
493 box = gtk.HBox(False, 5)
494
495 b = gtk.Button("Details")
496 b.connect('clicked', self.actionDetails)
497 b.show()
498 box.pack_start(b, False, False, 0)
499 self.entriesForButton[b] = [entry1, entry2]
500
501 self.combos.append(gtk.combo_box_new_text())
502 self.combos[y].append_text("SKIP")
503 self.combos[y].append_text("Keep <%s>" %(source[0].getDescription()))
504 self.combos[y].append_text("Keep <%s>" %(source[1].getDescription()))
505
506 self.combos[y].set_active(0)
507 self.combos[y].show()
508 box.pack_start(self.combos[y], False, False, 0)
509
510 box.show()
511 table.attach(box, 1, 3, y, y+1)
512
513 y+=1
514
515 table.show()
516 self.vbox.pack_start(table, True, True, 0)
517
519 """
520 Checks all drop down boxes for their selections and assembles a nice dictionary containing all the user selections
521 """
522 y = 0
523 ret = {}
524 for entryID in self._idList:
525 ret[entryID] = ['s', 'a', 'b'][self.combos[y].get_active()]
526 y+=1
527 return ret
528
530 """
531 Pops up an instance of L{ConflictDetailsDialog}
532 """
533 entry1 = self.entriesForButton[target][0]
534 entry2 = self.entriesForButton[target][1]
535 d = ConflictDetailsDialog(self, entry1, entry2, self._source[0].getDescription(), self._source[1].getDescription() )
536 ret = d.run()
537 d.destroy()
538
539
541 """
542 GTK-Dialog for visualizing the differences between two particular contact entries from two data sources (which are concerned as belonging to the same person)
543
544 All attributes, in which the two entries differ, are visualized in a table for the two sources.
545 """
546
547 - def __init__(self, parent, entry1, entry2, nameSource1, nameSource2):
548 """
549 Constructor - all components are assembled in here
550 """
551 gtk.Dialog.__init__(self, "Details for conflict", parent, gtk.DIALOG_MODAL , ("Close",gtk.RESPONSE_OK))
552
553 self.set_size_request(600, 300)
554 box = gtk.VBox(False, 5)
555
556 l = gtk.Label("Details for <%s, %s>" %(entry1.attributes['lastname'],entry1.attributes['firstname']))
557 l.show()
558 box.pack_start(l, False, False, 0)
559
560 separator = gtk.HSeparator()
561 box.pack_start(separator, False, True, 5)
562 separator.show()
563
564 diffList = pisi.determineConflictDetails(entry1, entry2)
565
566
567
568 scrolled_window = gtk.ScrolledWindow()
569 scrolled_window.set_border_width(10)
570 scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
571 box.pack_start(scrolled_window, True, True, 0)
572 scrolled_window.show()
573
574 table = gtk.Table(3, len(diffList.keys())+1, True)
575 table.set_row_spacings(10)
576 table.set_col_spacings(10)
577
578 l = gtk.Label('Attribute')
579 l.set_pattern('_' * len('Attribute'))
580 l.set_use_underline(True)
581 l.show()
582 table.attach(l, 0, 1, 0, 1)
583 l = gtk.Label(nameSource1)
584 l.set_pattern('_' * len(nameSource1))
585 l.set_use_underline(True)
586 l.show()
587 table.attach(l, 1, 2, 0, 1)
588 l = gtk.Label(nameSource2)
589 l.set_pattern('_' * len(nameSource2))
590 l.set_use_underline(True)
591 l.show()
592 table.attach(l, 2, 3, 0, 1)
593
594 y = 1
595 for key in diffList.keys():
596 l = gtk.Label(key)
597 l.show()
598 table.attach(l, 0, 1, y, y+1)
599 l = gtk.Label(self._getDetailValue(entry1.attributes, key))
600 l.show()
601 table.attach(l, 1, 2, y, y+1)
602 l = gtk.Label(self._getDetailValue(entry2.attributes, key))
603 l.show()
604 table.attach(l, 2, 3, y, y+1)
605 y+=1
606
607 table.show()
608 scrolled_window.add_with_viewport(table)
609
610
611
612 box.show()
613 self.vbox.pack_start(box, True, True, 0)
614
616 """
617 Gets around the problem if an attribute in one data source is not set in the other one at all
618 """
619 try:
620 return dict[key]
621 except KeyError:
622 return default
623
625 """
626 Checks, whether configuration can be loaded from PISI core.
627
628 If not possible, an error message is visualized in a GTK dialog.
629 @return: False, if an Error occurs when loading the configration from core; otherwise True
630 """
631 try:
632 pisi.getConfiguration()
633 return True
634 except ValueError:
635 dialog = gtk.MessageDialog(None, buttons=gtk.BUTTONS_OK, message_format = "PISI configuration not found", type = gtk.MESSAGE_ERROR)
636 configfolder = os.path.join(os.environ.get('HOME'), '.pisi')
637 configfile = os.path.join(configfolder, 'conf')
638 dialog.format_secondary_markup("For running PISI you must have a configuration file located at\n '%s'.\n\nWith the package a well-documented sample was placed at '/usr/share/doc/pisi/conf.example'. You may move this for a starting point - then edit this file in order to configure your PIM synchronization data sources." %(configfile))
639 ret = dialog.run()
640 dialog.destroy()
641 return False
642
643 """
644 This starts the GUI version of PISI
645 """
646 if __name__ == "__main__":
647 if testConfiguration():
648 base = Base()
649 pisiprogress.registerCallback(base)
650 base.main()
651