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
21 @todo Look into a messages view
22 @li https://www.google.com/voice/inbox/recent/voicemail/
23 @li https://www.google.com/voice/inbox/recent/sms/
24 Would need to either use both json and html or just html
27 from __future__ import with_statement
39 def make_ugly(prettynumber):
41 function to take a phone number and strip out all non-numeric
44 >>> make_ugly("+012-(345)-678-90")
48 uglynumber = re.sub('\D', '', prettynumber)
52 def make_pretty(phonenumber):
54 Function to take a phone number and return the pretty version
56 if phonenumber begins with 0:
58 if phonenumber begins with 1: ( for gizmo callback numbers )
60 if phonenumber is 13 digits:
62 if phonenumber is 10 digits:
66 >>> make_pretty("1234567")
68 >>> make_pretty("2345678901")
70 >>> make_pretty("12345678901")
72 >>> make_pretty("01234567890")
75 if phonenumber is None or phonenumber is "":
78 phonenumber = make_ugly(phonenumber)
80 if len(phonenumber) < 3:
83 if phonenumber[0] == "0":
85 prettynumber += "+%s" % phonenumber[0:3]
86 if 3 < len(phonenumber):
87 prettynumber += "-(%s)" % phonenumber[3:6]
88 if 6 < len(phonenumber):
89 prettynumber += "-%s" % phonenumber[6:9]
90 if 9 < len(phonenumber):
91 prettynumber += "-%s" % phonenumber[9:]
93 elif len(phonenumber) <= 7:
94 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
95 elif len(phonenumber) > 8 and phonenumber[0] == "1":
96 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
97 elif len(phonenumber) > 7:
98 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
102 def make_idler(func):
104 Decorator that makes a generator-function into a function that will continue execution on next call
108 def decorated_func(*args, **kwds):
110 a.append(func(*args, **kwds))
114 except StopIteration:
118 decorated_func.__name__ = func.__name__
119 decorated_func.__doc__ = func.__doc__
120 decorated_func.__dict__.update(func.__dict__)
122 return decorated_func
125 class DummyAddressBook(object):
127 Minimal example of both an addressbook factory and an addressbook
130 def clear_caches(self):
133 def get_addressbooks(self):
135 @returns Iterable of (Address Book Factory, Book Id, Book Name)
137 yield self, "", "None"
139 def open_addressbook(self, bookId):
143 def contact_source_short_name(contactId):
153 @returns Iterable of (contact id, contact name)
158 def get_contact_details(contactId):
160 @returns Iterable of (Phone Type, Phone Number)
165 class MergedAddressBook(object):
167 Merger of all addressbooks
170 def __init__(self, addressbookFactories, sorter = None):
171 self.__addressbookFactories = addressbookFactories
172 self.__addressbooks = None
173 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
175 def clear_caches(self):
176 self.__addressbooks = None
177 for factory in self.__addressbookFactories:
178 factory.clear_caches()
180 def get_addressbooks(self):
182 @returns Iterable of (Address Book Factory, Book Id, Book Name)
186 def open_addressbook(self, bookId):
189 def contact_source_short_name(self, contactId):
190 if self.__addressbooks is None:
192 bookIndex, originalId = contactId.split("-", 1)
193 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
197 return "All Contacts"
199 def get_contacts(self):
201 @returns Iterable of (contact id, contact name)
203 if self.__addressbooks is None:
204 self.__addressbooks = list(
205 factory.open_addressbook(id)
206 for factory in self.__addressbookFactories
207 for (f, id, name) in factory.get_addressbooks()
210 ("-".join([str(bookIndex), contactId]), contactName)
211 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
212 for (contactId, contactName) in addressbook.get_contacts()
214 sortedContacts = self.__sort_contacts(contacts)
215 return sortedContacts
217 def get_contact_details(self, contactId):
219 @returns Iterable of (Phone Type, Phone Number)
221 if self.__addressbooks is None:
223 bookIndex, originalId = contactId.split("-", 1)
224 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
227 def null_sorter(contacts):
229 Good for speed/low memory
234 def basic_firtname_sorter(contacts):
236 Expects names in "First Last" format
239 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
240 for (contactId, contactName) in contacts
242 contactsWithKey.sort()
243 return (contactData for (lastName, contactData) in contactsWithKey)
246 def basic_lastname_sorter(contacts):
248 Expects names in "First Last" format
251 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
252 for (contactId, contactName) in contacts
254 contactsWithKey.sort()
255 return (contactData for (lastName, contactData) in contactsWithKey)
258 def reversed_firtname_sorter(contacts):
260 Expects names in "Last, First" format
263 (contactName.split(", ", 1)[-1], (contactId, contactName))
264 for (contactId, contactName) in contacts
266 contactsWithKey.sort()
267 return (contactData for (lastName, contactData) in contactsWithKey)
270 def reversed_lastname_sorter(contacts):
272 Expects names in "Last, First" format
275 (contactName.split(", ", 1)[0], (contactId, contactName))
276 for (contactId, contactName) in contacts
278 contactsWithKey.sort()
279 return (contactData for (lastName, contactData) in contactsWithKey)
282 def guess_firstname(name):
284 return name.split(", ", 1)[-1]
286 return name.rsplit(" ", 1)[0]
289 def guess_lastname(name):
291 return name.split(", ", 1)[0]
293 return name.rsplit(" ", 1)[-1]
296 def advanced_firstname_sorter(cls, contacts):
298 (cls.guess_firstname(contactName), (contactId, contactName))
299 for (contactId, contactName) in contacts
301 contactsWithKey.sort()
302 return (contactData for (lastName, contactData) in contactsWithKey)
305 def advanced_lastname_sorter(cls, contacts):
307 (cls.guess_lastname(contactName), (contactId, contactName))
308 for (contactId, contactName) in contacts
310 contactsWithKey.sort()
311 return (contactData for (lastName, contactData) in contactsWithKey)
314 class PhoneTypeSelector(object):
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 def run(self, contactDetails, message = ""):
354 self._typemodel.clear()
356 for phoneType, phoneNumber in contactDetails:
357 self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
361 self._message.set_text(message)
365 userResponse = self._dialog.run()
367 if userResponse == gtk.RESPONSE_OK:
368 phoneNumber = self._get_number()
372 self._typeviewselection.unselect_all()
376 def _get_number(self):
377 model, itr = self._typeviewselection.get_selected()
381 phoneNumber = self._typemodel.get_value(itr, 0)
384 def _on_phonetype_dial(self, *args):
385 self._gcBackend.dial(self._get_number())
386 self._dialog.response(gtk.RESPONSE_CANCEL)
388 def _on_phonetype_send_sms(self, *args):
389 self._dialog.response(gtk.RESPONSE_CANCEL)
390 idly_run = gtk_toolbox.asynchronous_gtk_message(self._smsDialog.run)
391 idly_run(self._get_number(), self._message.get_text())
393 def _on_phonetype_select(self, *args):
394 self._dialog.response(gtk.RESPONSE_OK)
396 def _on_phonetype_cancel(self, *args):
397 self._dialog.response(gtk.RESPONSE_CANCEL)
400 class SmsEntryDialog(object):
404 def __init__(self, widgetTree, gcBackend):
405 self._gcBackend = gcBackend
406 self._widgetTree = widgetTree
407 self._dialog = self._widgetTree.get_widget("smsDialog")
409 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
410 self._smsButton.connect("clicked", self._on_send)
412 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
413 self._cancelButton.connect("clicked", self._on_cancel)
415 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
416 self._message = self._widgetTree.get_widget("smsMessage")
417 self._smsEntry = self._widgetTree.get_widget("smsEntry")
418 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
420 def run(self, number, message = ""):
423 self._message.set_text(message)
426 self._smsEntry.get_buffer().set_text("")
427 self._update_letter_count()
429 userResponse = self._dialog.run()
430 if userResponse == gtk.RESPONSE_OK:
431 entryBuffer = self._smsEntry.get_buffer()
432 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
433 enteredMessage = enteredMessage[0:self.MAX_CHAR]
434 self._gcBackend.send_sms(number, enteredMessage)
438 def _update_letter_count(self, *args):
439 entryLength = self._smsEntry.get_buffer().get_char_count()
440 self._letterCountLabel.set_text(str(self.MAX_CHAR - entryLength))
442 def _on_entry_changed(self, *args):
443 self._update_letter_count()
445 def _on_send(self, *args):
446 self._dialog.response(gtk.RESPONSE_OK)
448 def _on_cancel(self, *args):
449 self._dialog.response(gtk.RESPONSE_CANCEL)
452 class Dialpad(object):
454 def __init__(self, widgetTree, errorDisplay):
455 self._errorDisplay = errorDisplay
456 self._numberdisplay = widgetTree.get_widget("numberdisplay")
457 self._dialButton = widgetTree.get_widget("dial")
458 self._phonenumber = ""
459 self._prettynumber = ""
460 self._clearall_id = None
463 "on_dial_clicked": self._on_dial_clicked,
464 "on_digit_clicked": self._on_digit_clicked,
465 "on_clear_number": self._on_clear_number,
466 "on_back_clicked": self._on_backspace,
467 "on_back_pressed": self._on_back_pressed,
468 "on_back_released": self._on_back_released,
470 widgetTree.signal_autoconnect(callbackMapping)
473 self._dialButton.grab_focus()
478 def dial(self, number):
480 @note Actual dial function is patched in later
482 raise NotImplementedError
484 def get_number(self):
485 return self._phonenumber
487 def set_number(self, number):
489 Set the callback phonenumber
492 self._phonenumber = make_ugly(number)
493 self._prettynumber = make_pretty(self._phonenumber)
494 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
496 self._errorDisplay.push_exception(e)
501 def _on_dial_clicked(self, widget):
502 self.dial(self.get_number())
504 def _on_clear_number(self, *args):
507 def _on_digit_clicked(self, widget):
508 self.set_number(self._phonenumber + widget.get_name()[-1])
510 def _on_backspace(self, widget):
511 self.set_number(self._phonenumber[:-1])
513 def _on_clearall(self):
517 def _on_back_pressed(self, widget):
518 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
520 def _on_back_released(self, widget):
521 if self._clearall_id is not None:
522 gobject.source_remove(self._clearall_id)
523 self._clearall_id = None
526 class AccountInfo(object):
528 def __init__(self, widgetTree, backend, errorDisplay):
529 self._errorDisplay = errorDisplay
530 self._backend = backend
532 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
533 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
534 self._callbackCombo = widgetTree.get_widget("callbackcombo")
535 self._onCallbackentryChangedId = 0
538 assert self._backend.is_authed()
539 self._accountViewNumberDisplay.set_use_markup(True)
540 self.set_account_number("")
541 self._callbackList.clear()
543 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
546 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
548 self._callbackList.clear()
550 def get_selected_callback_number(self):
551 return make_ugly(self._callbackCombo.get_child().get_text())
553 def set_account_number(self, number):
555 Displays current account number
557 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
560 self.populate_callback_combo()
561 self.set_account_number(self._backend.get_account_number())
564 self._callbackCombo.get_child().set_text("")
565 self.set_account_number("")
567 def populate_callback_combo(self):
568 self._callbackList.clear()
570 callbackNumbers = self._backend.get_callback_numbers()
571 except RuntimeError, e:
572 self._errorDisplay.push_exception(e)
575 for number, description in callbackNumbers.iteritems():
576 self._callbackList.append((make_pretty(number),))
578 self._callbackCombo.set_model(self._callbackList)
579 self._callbackCombo.set_text_column(0)
581 callbackNumber = self._backend.get_callback_number()
582 except RuntimeError, e:
583 self._errorDisplay.push_exception(e)
585 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
587 def _on_callbackentry_changed(self, *args):
589 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
592 text = self.get_selected_callback_number()
593 if not self._backend.is_valid_syntax(text):
594 self._errorDisplay.push_message("%s is not a valid callback number" % text)
595 elif text == self._backend.get_callback_number():
596 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
598 self._backend.set_callback_number(text)
599 except RuntimeError, e:
600 self._errorDisplay.push_exception(e)
603 class RecentCallsView(object):
605 def __init__(self, widgetTree, backend, errorDisplay):
606 self._errorDisplay = errorDisplay
607 self._backend = backend
609 self._recenttime = 0.0
610 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
611 self._recentview = widgetTree.get_widget("recentview")
612 self._recentviewselection = None
613 self._onRecentviewRowActivatedId = 0
615 textrenderer = gtk.CellRendererText()
616 self._recentviewColumn = gtk.TreeViewColumn("Calls")
617 self._recentviewColumn.pack_start(textrenderer, expand=True)
618 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
619 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
621 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
624 assert self._backend.is_authed()
625 self._recentview.set_model(self._recentmodel)
627 self._recentview.append_column(self._recentviewColumn)
628 self._recentviewselection = self._recentview.get_selection()
629 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
631 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
634 self._recentview.disconnect(self._onRecentviewRowActivatedId)
635 self._recentview.remove_column(self._recentviewColumn)
636 self._recentview.set_model(None)
638 def number_selected(self, number):
640 @note Actual dial function is patched in later
642 raise NotImplementedError
645 if (time.time() - self._recenttime) < 300:
647 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
648 backgroundPopulate.setDaemon(True)
649 backgroundPopulate.start()
652 self._recenttime = 0.0
653 self._recentmodel.clear()
655 def _idly_populate_recentview(self):
656 self._recenttime = time.time()
657 self._recentmodel.clear()
660 recentItems = self._backend.get_recent()
661 except RuntimeError, e:
662 self._errorDisplay.push_exception_with_lock(e)
663 self._recenttime = 0.0
666 for personsName, phoneNumber, date, action in recentItems:
667 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
668 item = (phoneNumber, description)
669 with gtk_toolbox.gtk_lock():
670 self._recentmodel.append(item)
674 def _on_recentview_row_activated(self, treeview, path, view_column):
675 model, itr = self._recentviewselection.get_selected()
679 contactPhoneNumbers = [("Phone", self._recentmodel.get_value(itr, 0))]
680 description = self._recentmodel.get_value(itr, 1)
681 print repr(contactPhoneNumbers), repr(description)
683 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
684 if 0 == len(phoneNumber):
687 self.number_selected(phoneNumber)
688 self._recentviewselection.unselect_all()
691 class MessagesView(object):
693 def __init__(self, widgetTree, backend, errorDisplay):
694 self._errorDisplay = errorDisplay
695 self._backend = backend
697 self._messagetime = 0.0
698 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
699 self._messageview = widgetTree.get_widget("messages_view")
700 self._messageviewselection = None
701 self._onMessageviewRowActivatedId = 0
703 textrenderer = gtk.CellRendererText()
704 self._messageviewColumn = gtk.TreeViewColumn("Messages")
705 self._messageviewColumn.pack_start(textrenderer, expand=True)
706 self._messageviewColumn.add_attribute(textrenderer, "text", 1)
707 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
709 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
712 assert self._backend.is_authed()
713 self._messageview.set_model(self._messagemodel)
715 self._messageview.append_column(self._messageviewColumn)
716 self._messageviewselection = self._messageview.get_selection()
717 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
719 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
722 self._messageview.disconnect(self._onMessageviewRowActivatedId)
723 self._messageview.remove_column(self._messageviewColumn)
724 self._messageview.set_model(None)
726 def number_selected(self, number):
728 @note Actual dial function is patched in later
730 raise NotImplementedError
733 if (time.time() - self._messagetime) < 300:
735 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
736 backgroundPopulate.setDaemon(True)
737 backgroundPopulate.start()
740 self._messagetime = 0.0
741 self._messagemodel.clear()
743 def _idly_populate_messageview(self):
744 self._messagetime = time.time()
745 self._messagemodel.clear()
748 messageItems = self._backend.get_messages()
749 except RuntimeError, e:
750 self._errorDisplay.push_exception_with_lock(e)
751 self._messagetime = 0.0
754 for phoneNumber, data in messageItems:
755 item = (phoneNumber, data)
756 with gtk_toolbox.gtk_lock():
757 self._messagemodel.append(item)
761 def _on_messageview_row_activated(self, treeview, path, view_column):
762 model, itr = self._messageviewselection.get_selected()
766 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
767 description = self._messagemodel.get_value(itr, 1)
768 print repr(contactPhoneNumbers), repr(description)
770 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
771 if 0 == len(phoneNumber):
774 self.number_selected(phoneNumber)
775 self._messageviewselection.unselect_all()
778 class ContactsView(object):
780 def __init__(self, widgetTree, backend, errorDisplay):
781 self._errorDisplay = errorDisplay
782 self._backend = backend
784 self._addressBook = None
785 self._addressBookFactories = [DummyAddressBook()]
787 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
788 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
790 self._contactstime = 0.0
791 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
792 self._contactsviewselection = None
793 self._contactsview = widgetTree.get_widget("contactsview")
795 self._contactColumn = gtk.TreeViewColumn("Contact")
796 displayContactSource = False
797 if displayContactSource:
798 textrenderer = gtk.CellRendererText()
799 self._contactColumn.pack_start(textrenderer, expand=False)
800 self._contactColumn.add_attribute(textrenderer, 'text', 0)
801 textrenderer = gtk.CellRendererText()
802 self._contactColumn.pack_start(textrenderer, expand=True)
803 self._contactColumn.add_attribute(textrenderer, 'text', 1)
804 textrenderer = gtk.CellRendererText()
805 self._contactColumn.pack_start(textrenderer, expand=True)
806 self._contactColumn.add_attribute(textrenderer, 'text', 4)
807 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
808 self._contactColumn.set_sort_column_id(1)
809 self._contactColumn.set_visible(True)
811 self._onContactsviewRowActivatedId = 0
812 self._onAddressbookComboChangedId = 0
813 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
816 assert self._backend.is_authed()
818 self._contactsview.set_model(self._contactsmodel)
819 self._contactsview.append_column(self._contactColumn)
820 self._contactsviewselection = self._contactsview.get_selection()
821 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
823 self._booksList.clear()
824 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
825 if factoryName and bookName:
826 entryName = "%s: %s" % (factoryName, bookName)
828 entryName = factoryName
832 entryName = "Bad name (%d)" % factoryId
833 row = (str(factoryId), bookId, entryName)
834 self._booksList.append(row)
836 self._booksSelectionBox.set_model(self._booksList)
837 cell = gtk.CellRendererText()
838 self._booksSelectionBox.pack_start(cell, True)
839 self._booksSelectionBox.add_attribute(cell, 'text', 2)
840 self._booksSelectionBox.set_active(0)
842 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
843 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
846 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
847 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
849 self._booksSelectionBox.clear()
850 self._booksSelectionBox.set_model(None)
851 self._contactsview.set_model(None)
852 self._contactsview.remove_column(self._contactColumn)
854 def number_selected(self, number):
856 @note Actual dial function is patched in later
858 raise NotImplementedError
860 def get_addressbooks(self):
862 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
864 for i, factory in enumerate(self._addressBookFactories):
865 for bookFactory, bookId, bookName in factory.get_addressbooks():
866 yield (i, bookId), (factory.factory_name(), bookName)
868 def open_addressbook(self, bookFactoryId, bookId):
869 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
870 self._contactstime = 0
871 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
872 backgroundPopulate.setDaemon(True)
873 backgroundPopulate.start()
876 if (time.time() - self._contactstime) < 300:
878 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
879 backgroundPopulate.setDaemon(True)
880 backgroundPopulate.start()
883 self._contactstime = 0.0
884 self._contactsmodel.clear()
886 def clear_caches(self):
887 for factory in self._addressBookFactories:
888 factory.clear_caches()
889 self._addressBook.clear_caches()
891 def append(self, book):
892 self._addressBookFactories.append(book)
894 def extend(self, books):
895 self._addressBookFactories.extend(books)
897 def _idly_populate_contactsview(self):
898 #@todo Add a lock so only one code path can be in here at a time
901 # completely disable updating the treeview while we populate the data
902 self._contactsview.freeze_child_notify()
903 self._contactsview.set_model(None)
905 addressBook = self._addressBook
907 contacts = addressBook.get_contacts()
908 except RuntimeError, e:
910 self._contactstime = 0.0
911 self._errorDisplay.push_exception_with_lock(e)
912 for contactId, contactName in contacts:
913 contactType = (addressBook.contact_source_short_name(contactId), )
914 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
916 # restart the treeview data rendering
917 self._contactsview.set_model(self._contactsmodel)
918 self._contactsview.thaw_child_notify()
921 def _on_addressbook_combo_changed(self, *args, **kwds):
922 itr = self._booksSelectionBox.get_active_iter()
925 factoryId = int(self._booksList.get_value(itr, 0))
926 bookId = self._booksList.get_value(itr, 1)
927 self.open_addressbook(factoryId, bookId)
929 def _on_contactsview_row_activated(self, treeview, path, view_column):
930 model, itr = self._contactsviewselection.get_selected()
934 contactId = self._contactsmodel.get_value(itr, 3)
935 contactName = self._contactsmodel.get_value(itr, 1)
937 contactDetails = self._addressBook.get_contact_details(contactId)
938 except RuntimeError, e:
940 self._contactstime = 0.0
941 self._errorDisplay.push_exception(e)
942 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
944 if len(contactPhoneNumbers) == 0:
946 elif len(contactPhoneNumbers) == 1:
947 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
949 if 0 == len(phoneNumber):
952 self.number_selected(phoneNumber)
953 self._contactsviewselection.unselect_all()