4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22 from __future__ import with_statement
34 def make_ugly(prettynumber):
36 function to take a phone number and strip out all non-numeric
39 >>> make_ugly("+012-(345)-678-90")
43 uglynumber = re.sub('\D', '', prettynumber)
47 def make_pretty(phonenumber):
49 Function to take a phone number and return the pretty version
51 if phonenumber begins with 0:
53 if phonenumber begins with 1: ( for gizmo callback numbers )
55 if phonenumber is 13 digits:
57 if phonenumber is 10 digits:
61 >>> make_pretty("1234567")
63 >>> make_pretty("2345678901")
65 >>> make_pretty("12345678901")
67 >>> make_pretty("01234567890")
70 if phonenumber is None or phonenumber is "":
73 phonenumber = make_ugly(phonenumber)
75 if len(phonenumber) < 3:
78 if phonenumber[0] == "0":
80 prettynumber += "+%s" % phonenumber[0:3]
81 if 3 < len(phonenumber):
82 prettynumber += "-(%s)" % phonenumber[3:6]
83 if 6 < len(phonenumber):
84 prettynumber += "-%s" % phonenumber[6:9]
85 if 9 < len(phonenumber):
86 prettynumber += "-%s" % phonenumber[9:]
88 elif len(phonenumber) <= 7:
89 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
90 elif len(phonenumber) > 8 and phonenumber[0] == "1":
91 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
92 elif len(phonenumber) > 7:
93 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
99 Decorator that makes a generator-function into a function that will continue execution on next call
103 def decorated_func(*args, **kwds):
105 a.append(func(*args, **kwds))
109 except StopIteration:
113 decorated_func.__name__ = func.__name__
114 decorated_func.__doc__ = func.__doc__
115 decorated_func.__dict__.update(func.__dict__)
117 return decorated_func
120 class DummyAddressBook(object):
122 Minimal example of both an addressbook factory and an addressbook
125 def clear_caches(self):
128 def get_addressbooks(self):
130 @returns Iterable of (Address Book Factory, Book Id, Book Name)
132 yield self, "", "None"
134 def open_addressbook(self, bookId):
138 def contact_source_short_name(contactId):
148 @returns Iterable of (contact id, contact name)
153 def get_contact_details(contactId):
155 @returns Iterable of (Phone Type, Phone Number)
160 class MergedAddressBook(object):
162 Merger of all addressbooks
165 def __init__(self, addressbookFactories, sorter = None):
166 self.__addressbookFactories = addressbookFactories
167 self.__addressbooks = None
168 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
170 def clear_caches(self):
171 self.__addressbooks = None
172 for factory in self.__addressbookFactories:
173 factory.clear_caches()
175 def get_addressbooks(self):
177 @returns Iterable of (Address Book Factory, Book Id, Book Name)
181 def open_addressbook(self, bookId):
184 def contact_source_short_name(self, contactId):
185 if self.__addressbooks is None:
187 bookIndex, originalId = contactId.split("-", 1)
188 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
192 return "All Contacts"
194 def get_contacts(self):
196 @returns Iterable of (contact id, contact name)
198 if self.__addressbooks is None:
199 self.__addressbooks = list(
200 factory.open_addressbook(id)
201 for factory in self.__addressbookFactories
202 for (f, id, name) in factory.get_addressbooks()
205 ("-".join([str(bookIndex), contactId]), contactName)
206 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
207 for (contactId, contactName) in addressbook.get_contacts()
209 sortedContacts = self.__sort_contacts(contacts)
210 return sortedContacts
212 def get_contact_details(self, contactId):
214 @returns Iterable of (Phone Type, Phone Number)
216 if self.__addressbooks is None:
218 bookIndex, originalId = contactId.split("-", 1)
219 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
222 def null_sorter(contacts):
224 Good for speed/low memory
229 def basic_firtname_sorter(contacts):
231 Expects names in "First Last" format
234 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
235 for (contactId, contactName) in contacts
237 contactsWithKey.sort()
238 return (contactData for (lastName, contactData) in contactsWithKey)
241 def basic_lastname_sorter(contacts):
243 Expects names in "First Last" format
246 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
247 for (contactId, contactName) in contacts
249 contactsWithKey.sort()
250 return (contactData for (lastName, contactData) in contactsWithKey)
253 def reversed_firtname_sorter(contacts):
255 Expects names in "Last, First" format
258 (contactName.split(", ", 1)[-1], (contactId, contactName))
259 for (contactId, contactName) in contacts
261 contactsWithKey.sort()
262 return (contactData for (lastName, contactData) in contactsWithKey)
265 def reversed_lastname_sorter(contacts):
267 Expects names in "Last, First" format
270 (contactName.split(", ", 1)[0], (contactId, contactName))
271 for (contactId, contactName) in contacts
273 contactsWithKey.sort()
274 return (contactData for (lastName, contactData) in contactsWithKey)
277 def guess_firstname(name):
279 return name.split(", ", 1)[-1]
281 return name.rsplit(" ", 1)[0]
284 def guess_lastname(name):
286 return name.split(", ", 1)[0]
288 return name.rsplit(" ", 1)[-1]
291 def advanced_firstname_sorter(cls, contacts):
293 (cls.guess_firstname(contactName), (contactId, contactName))
294 for (contactId, contactName) in contacts
296 contactsWithKey.sort()
297 return (contactData for (lastName, contactData) in contactsWithKey)
300 def advanced_lastname_sorter(cls, contacts):
302 (cls.guess_lastname(contactName), (contactId, contactName))
303 for (contactId, contactName) in contacts
305 contactsWithKey.sort()
306 return (contactData for (lastName, contactData) in contactsWithKey)
309 class PhoneTypeSelector(object):
311 ACTION_CANCEL = "cancel"
312 ACTION_SELECT = "select"
314 ACTION_SEND_SMS = "sms"
316 def __init__(self, widgetTree, gcBackend):
317 self._gcBackend = gcBackend
318 self._widgetTree = widgetTree
320 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
321 self._smsDialog = SmsEntryDialog(self._widgetTree, self._gcBackend)
323 self._smsButton = self._widgetTree.get_widget("sms_button")
324 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
326 self._dialButton = self._widgetTree.get_widget("dial_button")
327 self._dialButton.connect("clicked", self._on_phonetype_dial)
329 self._selectButton = self._widgetTree.get_widget("select_button")
330 self._selectButton.connect("clicked", self._on_phonetype_select)
332 self._cancelButton = self._widgetTree.get_widget("cancel_button")
333 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
335 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
336 self._typeviewselection = None
338 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
339 typeview = self._widgetTree.get_widget("phonetypes")
340 typeview.connect("row-activated", self._on_phonetype_select)
341 typeview.set_model(self._typemodel)
342 textrenderer = gtk.CellRendererText()
344 # Add the column to the treeview
345 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
346 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
348 typeview.append_column(column)
350 self._typeviewselection = typeview.get_selection()
351 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
353 self._action = self.ACTION_CANCEL
355 def run(self, contactDetails, message = ""):
356 self._typemodel.clear()
358 for phoneType, phoneNumber in contactDetails:
359 # @bug this isn't populating correctly for recent and messages but it is for contacts
360 print repr(phoneNumber), repr(phoneType)
361 self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
363 # @todo Need to decide how how to handle the single phone number case
365 self._message.set_markup(message)
368 self._message.set_markup("")
371 userResponse = self._dialog.run()
373 if userResponse == gtk.RESPONSE_OK:
374 phoneNumber = self._get_number()
378 self._action = self.ACTION_CANCEL
380 if self._action == self.ACTION_SEND_SMS:
381 smsMessage = self._smsDialog.run(phoneNumber, message)
386 self._action = self.ACTION_CANCEL
388 self._typeviewselection.unselect_all()
390 return self._action, phoneNumber, smsMessage
392 def _get_number(self):
393 model, itr = self._typeviewselection.get_selected()
397 phoneNumber = self._typemodel.get_value(itr, 0)
400 def _on_phonetype_dial(self, *args):
401 self._dialog.response(gtk.RESPONSE_OK)
402 self._action = self.ACTION_DIAL
404 def _on_phonetype_send_sms(self, *args):
405 self._dialog.response(gtk.RESPONSE_OK)
406 self._action = self.ACTION_SEND_SMS
408 def _on_phonetype_select(self, *args):
409 self._dialog.response(gtk.RESPONSE_OK)
410 self._action = self.ACTION_SELECT
412 def _on_phonetype_cancel(self, *args):
413 self._dialog.response(gtk.RESPONSE_CANCEL)
414 self._action = self.ACTION_CANCEL
417 class SmsEntryDialog(object):
421 def __init__(self, widgetTree, gcBackend):
422 self._gcBackend = gcBackend
423 self._widgetTree = widgetTree
424 self._dialog = self._widgetTree.get_widget("smsDialog")
426 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
427 self._smsButton.connect("clicked", self._on_send)
429 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
430 self._cancelButton.connect("clicked", self._on_cancel)
432 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
433 self._message = self._widgetTree.get_widget("smsMessage")
434 self._smsEntry = self._widgetTree.get_widget("smsEntry")
435 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
437 def run(self, number, message = ""):
439 self._message.set_markup(message)
442 self._message.set_markup("")
444 self._smsEntry.get_buffer().set_text("")
445 self._update_letter_count()
447 userResponse = self._dialog.run()
448 if userResponse == gtk.RESPONSE_OK:
449 entryBuffer = self._smsEntry.get_buffer()
450 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
451 enteredMessage = enteredMessage[0:self.MAX_CHAR]
456 return enteredMessage
458 def _update_letter_count(self, *args):
459 entryLength = self._smsEntry.get_buffer().get_char_count()
460 self._letterCountLabel.set_text(str(self.MAX_CHAR - entryLength))
462 def _on_entry_changed(self, *args):
463 self._update_letter_count()
465 def _on_send(self, *args):
466 self._dialog.response(gtk.RESPONSE_OK)
468 def _on_cancel(self, *args):
469 self._dialog.response(gtk.RESPONSE_CANCEL)
472 class Dialpad(object):
474 def __init__(self, widgetTree, errorDisplay):
475 self._errorDisplay = errorDisplay
476 self._numberdisplay = widgetTree.get_widget("numberdisplay")
477 self._dialButton = widgetTree.get_widget("dial")
478 self._phonenumber = ""
479 self._prettynumber = ""
480 self._clearall_id = None
483 "on_dial_clicked": self._on_dial_clicked,
484 "on_digit_clicked": self._on_digit_clicked,
485 "on_clear_number": self._on_clear_number,
486 "on_back_clicked": self._on_backspace,
487 "on_back_pressed": self._on_back_pressed,
488 "on_back_released": self._on_back_released,
490 widgetTree.signal_autoconnect(callbackMapping)
493 self._dialButton.grab_focus()
498 def dial(self, number):
500 @note Actual dial function is patched in later
502 raise NotImplementedError
504 def get_number(self):
505 return self._phonenumber
507 def set_number(self, number):
509 Set the callback phonenumber
512 self._phonenumber = make_ugly(number)
513 self._prettynumber = make_pretty(self._phonenumber)
514 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
516 self._errorDisplay.push_exception(e)
521 def _on_dial_clicked(self, widget):
522 self.dial(self.get_number())
524 def _on_clear_number(self, *args):
527 def _on_digit_clicked(self, widget):
528 self.set_number(self._phonenumber + widget.get_name()[-1])
530 def _on_backspace(self, widget):
531 self.set_number(self._phonenumber[:-1])
533 def _on_clearall(self):
537 def _on_back_pressed(self, widget):
538 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
540 def _on_back_released(self, widget):
541 if self._clearall_id is not None:
542 gobject.source_remove(self._clearall_id)
543 self._clearall_id = None
546 class AccountInfo(object):
548 def __init__(self, widgetTree, backend, errorDisplay):
549 self._errorDisplay = errorDisplay
550 self._backend = backend
552 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
553 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
554 self._callbackCombo = widgetTree.get_widget("callbackcombo")
555 self._onCallbackentryChangedId = 0
558 assert self._backend.is_authed()
559 self._accountViewNumberDisplay.set_use_markup(True)
560 self.set_account_number("")
561 self._callbackList.clear()
563 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
566 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
568 self._callbackList.clear()
570 def get_selected_callback_number(self):
571 return make_ugly(self._callbackCombo.get_child().get_text())
573 def set_account_number(self, number):
575 Displays current account number
577 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
580 self.populate_callback_combo()
581 self.set_account_number(self._backend.get_account_number())
584 self._callbackCombo.get_child().set_text("")
585 self.set_account_number("")
587 def populate_callback_combo(self):
588 self._callbackList.clear()
590 callbackNumbers = self._backend.get_callback_numbers()
591 except RuntimeError, e:
592 self._errorDisplay.push_exception(e)
595 for number, description in callbackNumbers.iteritems():
596 self._callbackList.append((make_pretty(number),))
598 self._callbackCombo.set_model(self._callbackList)
599 self._callbackCombo.set_text_column(0)
601 callbackNumber = self._backend.get_callback_number()
602 except RuntimeError, e:
603 self._errorDisplay.push_exception(e)
605 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
607 def _on_callbackentry_changed(self, *args):
609 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
612 text = self.get_selected_callback_number()
613 if not self._backend.is_valid_syntax(text):
614 self._errorDisplay.push_message("%s is not a valid callback number" % text)
615 elif text == self._backend.get_callback_number():
616 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
618 self._backend.set_callback_number(text)
619 except RuntimeError, e:
620 self._errorDisplay.push_exception(e)
623 class RecentCallsView(object):
625 def __init__(self, widgetTree, backend, errorDisplay):
626 self._errorDisplay = errorDisplay
627 self._backend = backend
629 self._recenttime = 0.0
630 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
631 self._recentview = widgetTree.get_widget("recentview")
632 self._recentviewselection = None
633 self._onRecentviewRowActivatedId = 0
635 textrenderer = gtk.CellRendererText()
636 # @todo Make seperate columns for each item in recent item payload
637 self._recentviewColumn = gtk.TreeViewColumn("Calls")
638 self._recentviewColumn.pack_start(textrenderer, expand=True)
639 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
640 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
642 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
645 assert self._backend.is_authed()
646 self._recentview.set_model(self._recentmodel)
648 self._recentview.append_column(self._recentviewColumn)
649 self._recentviewselection = self._recentview.get_selection()
650 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
652 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
655 self._recentview.disconnect(self._onRecentviewRowActivatedId)
656 self._recentview.remove_column(self._recentviewColumn)
657 self._recentview.set_model(None)
659 def number_selected(self, action, number, message):
661 @note Actual dial function is patched in later
663 raise NotImplementedError
666 if (time.time() - self._recenttime) < 300:
668 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
669 backgroundPopulate.setDaemon(True)
670 backgroundPopulate.start()
673 self._recenttime = 0.0
674 self._recentmodel.clear()
676 def _idly_populate_recentview(self):
677 self._recenttime = time.time()
678 self._recentmodel.clear()
681 recentItems = self._backend.get_recent()
682 except RuntimeError, e:
683 self._errorDisplay.push_exception_with_lock(e)
684 self._recenttime = 0.0
687 for personsName, phoneNumber, date, action in recentItems:
688 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
689 item = (phoneNumber, description)
690 with gtk_toolbox.gtk_lock():
691 self._recentmodel.append(item)
695 def _on_recentview_row_activated(self, treeview, path, view_column):
696 model, itr = self._recentviewselection.get_selected()
700 number = self._recentmodel.get_value(itr, 0)
701 number = make_ugly(number)
702 contactPhoneNumbers = [("Phone", number)]
703 description = self._recentmodel.get_value(itr, 1)
704 print "Activated Recent Row:", repr(contactPhoneNumbers), repr(description)
706 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
707 if action == PhoneTypeSelector.ACTION_CANCEL:
711 self.number_selected(action, phoneNumber, message)
712 self._recentviewselection.unselect_all()
715 class MessagesView(object):
717 def __init__(self, widgetTree, backend, errorDisplay):
718 self._errorDisplay = errorDisplay
719 self._backend = backend
721 self._messagetime = 0.0
722 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
723 self._messageview = widgetTree.get_widget("messages_view")
724 self._messageviewselection = None
725 self._onMessageviewRowActivatedId = 0
727 textrenderer = gtk.CellRendererText()
728 # @todo Make seperate columns for each item in message payload
729 self._messageviewColumn = gtk.TreeViewColumn("Messages")
730 self._messageviewColumn.pack_start(textrenderer, expand=True)
731 self._messageviewColumn.add_attribute(textrenderer, "markup", 1)
732 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
734 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
737 assert self._backend.is_authed()
738 self._messageview.set_model(self._messagemodel)
740 self._messageview.append_column(self._messageviewColumn)
741 self._messageviewselection = self._messageview.get_selection()
742 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
744 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
747 self._messageview.disconnect(self._onMessageviewRowActivatedId)
748 self._messageview.remove_column(self._messageviewColumn)
749 self._messageview.set_model(None)
751 def number_selected(self, action, number, message):
753 @note Actual dial function is patched in later
755 raise NotImplementedError
758 if (time.time() - self._messagetime) < 300:
760 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
761 backgroundPopulate.setDaemon(True)
762 backgroundPopulate.start()
765 self._messagetime = 0.0
766 self._messagemodel.clear()
768 def _idly_populate_messageview(self):
769 self._messagetime = time.time()
770 self._messagemodel.clear()
773 messageItems = self._backend.get_messages()
774 except RuntimeError, e:
775 self._errorDisplay.push_exception_with_lock(e)
776 self._messagetime = 0.0
779 for header, number, relativeDate, message in messageItems:
780 number = make_ugly(number)
781 print "Discarding", header, relativeDate
782 item = (number, message)
783 with gtk_toolbox.gtk_lock():
784 self._messagemodel.append(item)
788 def _on_messageview_row_activated(self, treeview, path, view_column):
789 model, itr = self._messageviewselection.get_selected()
793 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
794 description = self._messagemodel.get_value(itr, 1)
795 print repr(contactPhoneNumbers), repr(description)
797 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
798 if action == PhoneTypeSelector.ACTION_CANCEL:
802 self.number_selected(action, phoneNumber, message)
803 self._messageviewselection.unselect_all()
806 class ContactsView(object):
808 def __init__(self, widgetTree, backend, errorDisplay):
809 self._errorDisplay = errorDisplay
810 self._backend = backend
812 self._addressBook = None
813 self._addressBookFactories = [DummyAddressBook()]
815 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
816 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
818 self._contactstime = 0.0
819 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
820 self._contactsviewselection = None
821 self._contactsview = widgetTree.get_widget("contactsview")
823 self._contactColumn = gtk.TreeViewColumn("Contact")
824 displayContactSource = False
825 if displayContactSource:
826 textrenderer = gtk.CellRendererText()
827 self._contactColumn.pack_start(textrenderer, expand=False)
828 self._contactColumn.add_attribute(textrenderer, 'text', 0)
829 textrenderer = gtk.CellRendererText()
830 self._contactColumn.pack_start(textrenderer, expand=True)
831 self._contactColumn.add_attribute(textrenderer, 'text', 1)
832 textrenderer = gtk.CellRendererText()
833 self._contactColumn.pack_start(textrenderer, expand=True)
834 self._contactColumn.add_attribute(textrenderer, 'text', 4)
835 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
836 self._contactColumn.set_sort_column_id(1)
837 self._contactColumn.set_visible(True)
839 self._onContactsviewRowActivatedId = 0
840 self._onAddressbookComboChangedId = 0
841 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
844 assert self._backend.is_authed()
846 self._contactsview.set_model(self._contactsmodel)
847 self._contactsview.append_column(self._contactColumn)
848 self._contactsviewselection = self._contactsview.get_selection()
849 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
851 self._booksList.clear()
852 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
853 if factoryName and bookName:
854 entryName = "%s: %s" % (factoryName, bookName)
856 entryName = factoryName
860 entryName = "Bad name (%d)" % factoryId
861 row = (str(factoryId), bookId, entryName)
862 self._booksList.append(row)
864 self._booksSelectionBox.set_model(self._booksList)
865 cell = gtk.CellRendererText()
866 self._booksSelectionBox.pack_start(cell, True)
867 self._booksSelectionBox.add_attribute(cell, 'text', 2)
868 self._booksSelectionBox.set_active(0)
870 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
871 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
874 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
875 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
877 self._booksSelectionBox.clear()
878 self._booksSelectionBox.set_model(None)
879 self._contactsview.set_model(None)
880 self._contactsview.remove_column(self._contactColumn)
882 def number_selected(self, action, number, message):
884 @note Actual dial function is patched in later
886 raise NotImplementedError
888 def get_addressbooks(self):
890 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
892 for i, factory in enumerate(self._addressBookFactories):
893 for bookFactory, bookId, bookName in factory.get_addressbooks():
894 yield (i, bookId), (factory.factory_name(), bookName)
896 def open_addressbook(self, bookFactoryId, bookId):
897 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
898 self._contactstime = 0
899 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
900 backgroundPopulate.setDaemon(True)
901 backgroundPopulate.start()
904 if (time.time() - self._contactstime) < 300:
906 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
907 backgroundPopulate.setDaemon(True)
908 backgroundPopulate.start()
911 self._contactstime = 0.0
912 self._contactsmodel.clear()
914 def clear_caches(self):
915 for factory in self._addressBookFactories:
916 factory.clear_caches()
917 self._addressBook.clear_caches()
919 def append(self, book):
920 self._addressBookFactories.append(book)
922 def extend(self, books):
923 self._addressBookFactories.extend(books)
925 def _idly_populate_contactsview(self):
926 #@todo Add a lock so only one code path can be in here at a time
929 # completely disable updating the treeview while we populate the data
930 self._contactsview.freeze_child_notify()
931 self._contactsview.set_model(None)
933 addressBook = self._addressBook
935 contacts = addressBook.get_contacts()
936 except RuntimeError, e:
938 self._contactstime = 0.0
939 self._errorDisplay.push_exception_with_lock(e)
940 for contactId, contactName in contacts:
941 contactType = (addressBook.contact_source_short_name(contactId), )
942 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
944 # restart the treeview data rendering
945 self._contactsview.set_model(self._contactsmodel)
946 self._contactsview.thaw_child_notify()
949 def _on_addressbook_combo_changed(self, *args, **kwds):
950 itr = self._booksSelectionBox.get_active_iter()
953 factoryId = int(self._booksList.get_value(itr, 0))
954 bookId = self._booksList.get_value(itr, 1)
955 self.open_addressbook(factoryId, bookId)
957 def _on_contactsview_row_activated(self, treeview, path, view_column):
958 model, itr = self._contactsviewselection.get_selected()
962 contactId = self._contactsmodel.get_value(itr, 3)
963 contactName = self._contactsmodel.get_value(itr, 1)
965 contactDetails = self._addressBook.get_contact_details(contactId)
966 except RuntimeError, e:
968 self._contactstime = 0.0
969 self._errorDisplay.push_exception(e)
970 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
972 if len(contactPhoneNumbers) == 0:
975 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
976 if action == PhoneTypeSelector.ACTION_CANCEL:
980 self.number_selected(action, phoneNumber, message)
981 self._contactsviewselection.unselect_all()