Module pisigui
[hide private]
[frames] | no frames]

Source Code for Module pisigui

  1  #!/usr/bin/env python 
  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 """
41 - def __init__(self):
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
124 - def _createContactsPanel(self, sources):
125 """ 126 Creates one side in the notebook - the one for setting up contacts synchronization 127 """ 128 box = gtk.VBox(False, 5) 129 label = gtk.Label("Source A") 130 label.set_alignment(0, 0) 131 box.pack_start(label, False, False, 0) 132 label.show() 133 self.contacts_combobox1 = gtk.combo_box_new_text() 134 for sourceDesc in sources: 135 self.contacts_combobox1.append_text(sourceDesc) 136 self.contacts_combobox1.set_active(0) 137 box.pack_start(self.contacts_combobox1, False, False, 0) 138 self.contacts_combobox1.show() 139 140 label = gtk.Label("Source B") 141 label.set_alignment(0, 0) 142 box.pack_start(label, False, False, 0) 143 label.show() 144 self.contacts_combobox2 = gtk.combo_box_new_text() 145 for sourceDesc in sources: 146 self.contacts_combobox2.append_text(sourceDesc) 147 self.contacts_combobox2.set_active(1) 148 box.pack_start(self.contacts_combobox2, False, False, 0) 149 self.contacts_combobox2.show() 150 151 separator = gtk.HSeparator() 152 box.pack_start(separator, False, True, 5) 153 separator.show() 154 155 label = gtk.Label("Conflict Mode") 156 label.set_alignment(0, 0) 157 box.pack_start(label, False, False, 0) 158 label.show() 159 self.contacts_combobox3 = gtk.combo_box_new_text() 160 for mode in MERGEMODE_STRINGS: 161 self.contacts_combobox3.append_text(mode) 162 self.contacts_combobox3.set_active(0) 163 box.pack_start(self.contacts_combobox3, False, False, 0) 164 self.contacts_combobox3.show() 165 166 box.show() 167 return box
168
169 - def _createCalendarPanel(self, sources):
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
199 - def main(self):
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
211 - def delete_event(self, widget, event, data=None):
212 """ 213 Event handler 214 """ 215 return False
216 217
218 - def showAbout(self, target):
219 """ 220 Pops up an 'About'-dialog, which displays all the application meta information from module pisiconstants. 221 """ 222 d = gtk.AboutDialog() 223 d.set_name(PISI_NAME) 224 d.set_version(PISI_VERSION) 225 f = open(FILEPATH_COPYING, "r") 226 content = f.read() 227 f.close() 228 d.set_license(content) 229 d.set_authors(PISI_AUTHORS) 230 d.set_comments(PISI_COMMENTS) 231 d.set_website(PISI_HOMEPAGE) 232 if PISI_TRANSLATOR_CREDITS: 233 d.set_translator_credits(PISI_TRANSLATOR_CREDITS) 234 if PISI_DOCUMENTERS: 235 d.set_documenters(PISI_DOCUMENTERS) 236 ret = d.run() 237 d.destroy()
238
239 - def startSync(self, target):
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: # sync contacts 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: # sync calendar 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: # events mode 339 pisi.eventsSync.syncEvents(True, modulesToLoad, source) 340 elif mode == MODE_CONTACTS: # contacts mode 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 ## Callbacks for PISI interaction 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
384 - def verbose(self, st):
385 """ 386 Output a message (of lower interest) to the user - output is directed to text based console. 387 """ 388 print st
389
390 - def error(self, st):
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
397 - def promptGenericConfirmation(self, prompt):
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
406 - def promptGeneric(self, prompt, default):
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
412 - def promptFilename(self, prompt, default):
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
425 - def askConfirmation(self, source, idList):
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
442 - def update(self, status):
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(): # see http://faq.pygtk.org/index.py?req=show&file=faq03.007.htp 452 gtk.main_iteration(False)
453
454 -class ConflictsDialog(gtk.Dialog):
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
518 - def getReactions(self):
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
529 - def actionDetails(self, target):
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
540 -class ConflictDetailsDialog(gtk.Dialog):
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 # boxH = gtk.HBox(False, 20) 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 # boxH.pack_start(table, False, True, 20) 610 # boxH.show() 611 # box.pack_start(boxH, False, True, 5) 612 box.show() 613 self.vbox.pack_start(box, True, True, 0)
614
615 - def _getDetailValue(self, dict, key, default = '<n.a.>'):
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
624 -def testConfiguration():
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