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 ACTION_CANCEL = "cancel"
317 ACTION_SELECT = "select"
319 ACTION_SEND_SMS = "sms"
321 def __init__(self, widgetTree, gcBackend):
322 self._gcBackend = gcBackend
323 self._widgetTree = widgetTree
325 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
326 self._smsDialog = SmsEntryDialog(self._widgetTree, self._gcBackend)
328 self._smsButton = self._widgetTree.get_widget("sms_button")
329 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
331 self._dialButton = self._widgetTree.get_widget("dial_button")
332 self._dialButton.connect("clicked", self._on_phonetype_dial)
334 self._selectButton = self._widgetTree.get_widget("select_button")
335 self._selectButton.connect("clicked", self._on_phonetype_select)
337 self._cancelButton = self._widgetTree.get_widget("cancel_button")
338 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
340 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
341 self._typeviewselection = None
343 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
344 typeview = self._widgetTree.get_widget("phonetypes")
345 typeview.connect("row-activated", self._on_phonetype_select)
346 typeview.set_model(self._typemodel)
347 textrenderer = gtk.CellRendererText()
349 # Add the column to the treeview
350 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
351 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
353 typeview.append_column(column)
355 self._typeviewselection = typeview.get_selection()
356 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
358 self._action = self.ACTION_CANCEL
360 def run(self, contactDetails, message = ""):
361 self._typemodel.clear()
363 for phoneType, phoneNumber in contactDetails:
364 # @bug this isn't populating correctly for recent and messages but it is for contacts
365 print repr(phoneNumber), repr(phoneType)
366 self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
368 # @todo Need to decide how how to handle the single phone number case
370 self._message.set_markup(message)
373 self._message.set_markup("")
376 userResponse = self._dialog.run()
378 if userResponse == gtk.RESPONSE_OK:
379 phoneNumber = self._get_number()
383 self._action = self.ACTION_CANCEL
385 if self._action == self.ACTION_SEND_SMS:
386 smsMessage = self._smsDialog.run(phoneNumber, message)
391 self._action = self.ACTION_CANCEL
393 self._typeviewselection.unselect_all()
395 return self._action, phoneNumber, smsMessage
397 def _get_number(self):
398 model, itr = self._typeviewselection.get_selected()
402 phoneNumber = self._typemodel.get_value(itr, 0)
405 def _on_phonetype_dial(self, *args):
406 self._dialog.response(gtk.RESPONSE_OK)
407 self._action = self.ACTION_DIAL
409 def _on_phonetype_send_sms(self, *args):
410 self._dialog.response(gtk.RESPONSE_OK)
411 self._action = self.ACTION_SEND_SMS
413 def _on_phonetype_select(self, *args):
414 self._dialog.response(gtk.RESPONSE_OK)
415 self._action = self.ACTION_SELECT
417 def _on_phonetype_cancel(self, *args):
418 self._dialog.response(gtk.RESPONSE_CANCEL)
419 self._action = self.ACTION_CANCEL
422 class SmsEntryDialog(object):
426 def __init__(self, widgetTree, gcBackend):
427 self._gcBackend = gcBackend
428 self._widgetTree = widgetTree
429 self._dialog = self._widgetTree.get_widget("smsDialog")
431 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
432 self._smsButton.connect("clicked", self._on_send)
434 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
435 self._cancelButton.connect("clicked", self._on_cancel)
437 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
438 self._message = self._widgetTree.get_widget("smsMessage")
439 self._smsEntry = self._widgetTree.get_widget("smsEntry")
440 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
442 def run(self, number, message = ""):
444 self._message.set_markup(message)
447 self._message.set_markup("")
449 self._smsEntry.get_buffer().set_text("")
450 self._update_letter_count()
452 userResponse = self._dialog.run()
453 if userResponse == gtk.RESPONSE_OK:
454 entryBuffer = self._smsEntry.get_buffer()
455 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
456 enteredMessage = enteredMessage[0:self.MAX_CHAR]
461 return enteredMessage
463 def _update_letter_count(self, *args):
464 entryLength = self._smsEntry.get_buffer().get_char_count()
465 self._letterCountLabel.set_text(str(self.MAX_CHAR - entryLength))
467 def _on_entry_changed(self, *args):
468 self._update_letter_count()
470 def _on_send(self, *args):
471 self._dialog.response(gtk.RESPONSE_OK)
473 def _on_cancel(self, *args):
474 self._dialog.response(gtk.RESPONSE_CANCEL)
477 class Dialpad(object):
479 def __init__(self, widgetTree, errorDisplay):
480 self._errorDisplay = errorDisplay
481 self._numberdisplay = widgetTree.get_widget("numberdisplay")
482 self._dialButton = widgetTree.get_widget("dial")
483 self._phonenumber = ""
484 self._prettynumber = ""
485 self._clearall_id = None
488 "on_dial_clicked": self._on_dial_clicked,
489 "on_digit_clicked": self._on_digit_clicked,
490 "on_clear_number": self._on_clear_number,
491 "on_back_clicked": self._on_backspace,
492 "on_back_pressed": self._on_back_pressed,
493 "on_back_released": self._on_back_released,
495 widgetTree.signal_autoconnect(callbackMapping)
498 self._dialButton.grab_focus()
503 def dial(self, number):
505 @note Actual dial function is patched in later
507 raise NotImplementedError
509 def get_number(self):
510 return self._phonenumber
512 def set_number(self, number):
514 Set the callback phonenumber
517 self._phonenumber = make_ugly(number)
518 self._prettynumber = make_pretty(self._phonenumber)
519 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
521 self._errorDisplay.push_exception(e)
526 def _on_dial_clicked(self, widget):
527 self.dial(self.get_number())
529 def _on_clear_number(self, *args):
532 def _on_digit_clicked(self, widget):
533 self.set_number(self._phonenumber + widget.get_name()[-1])
535 def _on_backspace(self, widget):
536 self.set_number(self._phonenumber[:-1])
538 def _on_clearall(self):
542 def _on_back_pressed(self, widget):
543 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
545 def _on_back_released(self, widget):
546 if self._clearall_id is not None:
547 gobject.source_remove(self._clearall_id)
548 self._clearall_id = None
551 class AccountInfo(object):
553 def __init__(self, widgetTree, backend, errorDisplay):
554 self._errorDisplay = errorDisplay
555 self._backend = backend
557 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
558 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
559 self._callbackCombo = widgetTree.get_widget("callbackcombo")
560 self._onCallbackentryChangedId = 0
563 assert self._backend.is_authed()
564 self._accountViewNumberDisplay.set_use_markup(True)
565 self.set_account_number("")
566 self._callbackList.clear()
568 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
571 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
573 self._callbackList.clear()
575 def get_selected_callback_number(self):
576 return make_ugly(self._callbackCombo.get_child().get_text())
578 def set_account_number(self, number):
580 Displays current account number
582 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
585 self.populate_callback_combo()
586 self.set_account_number(self._backend.get_account_number())
589 self._callbackCombo.get_child().set_text("")
590 self.set_account_number("")
592 def populate_callback_combo(self):
593 self._callbackList.clear()
595 callbackNumbers = self._backend.get_callback_numbers()
596 except RuntimeError, e:
597 self._errorDisplay.push_exception(e)
600 for number, description in callbackNumbers.iteritems():
601 self._callbackList.append((make_pretty(number),))
603 self._callbackCombo.set_model(self._callbackList)
604 self._callbackCombo.set_text_column(0)
606 callbackNumber = self._backend.get_callback_number()
607 except RuntimeError, e:
608 self._errorDisplay.push_exception(e)
610 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
612 def _on_callbackentry_changed(self, *args):
614 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
617 text = self.get_selected_callback_number()
618 if not self._backend.is_valid_syntax(text):
619 self._errorDisplay.push_message("%s is not a valid callback number" % text)
620 elif text == self._backend.get_callback_number():
621 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
623 self._backend.set_callback_number(text)
624 except RuntimeError, e:
625 self._errorDisplay.push_exception(e)
628 class RecentCallsView(object):
630 def __init__(self, widgetTree, backend, errorDisplay):
631 self._errorDisplay = errorDisplay
632 self._backend = backend
634 self._recenttime = 0.0
635 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
636 self._recentview = widgetTree.get_widget("recentview")
637 self._recentviewselection = None
638 self._onRecentviewRowActivatedId = 0
640 textrenderer = gtk.CellRendererText()
641 # @todo Make seperate columns for each item in recent item payload
642 self._recentviewColumn = gtk.TreeViewColumn("Calls")
643 self._recentviewColumn.pack_start(textrenderer, expand=True)
644 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
645 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
647 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
650 assert self._backend.is_authed()
651 self._recentview.set_model(self._recentmodel)
653 self._recentview.append_column(self._recentviewColumn)
654 self._recentviewselection = self._recentview.get_selection()
655 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
657 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
660 self._recentview.disconnect(self._onRecentviewRowActivatedId)
661 self._recentview.remove_column(self._recentviewColumn)
662 self._recentview.set_model(None)
664 def number_selected(self, action, number, message):
666 @note Actual dial function is patched in later
668 raise NotImplementedError
671 if (time.time() - self._recenttime) < 300:
673 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
674 backgroundPopulate.setDaemon(True)
675 backgroundPopulate.start()
678 self._recenttime = 0.0
679 self._recentmodel.clear()
681 def _idly_populate_recentview(self):
682 self._recenttime = time.time()
683 self._recentmodel.clear()
686 recentItems = self._backend.get_recent()
687 except RuntimeError, e:
688 self._errorDisplay.push_exception_with_lock(e)
689 self._recenttime = 0.0
692 for personsName, phoneNumber, date, action in recentItems:
693 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
694 item = (phoneNumber, description)
695 with gtk_toolbox.gtk_lock():
696 self._recentmodel.append(item)
700 def _on_recentview_row_activated(self, treeview, path, view_column):
701 model, itr = self._recentviewselection.get_selected()
705 number = self._recentmodel.get_value(itr, 0)
706 number = make_ugly(number)
707 contactPhoneNumbers = [("Phone", number)]
708 description = self._recentmodel.get_value(itr, 1)
709 print "Activated Recent Row:", repr(contactPhoneNumbers), repr(description)
711 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
712 if action == PhoneTypeSelector.ACTION_CANCEL:
716 self.number_selected(action, phoneNumber, message)
717 self._recentviewselection.unselect_all()
720 class MessagesView(object):
722 def __init__(self, widgetTree, backend, errorDisplay):
723 self._errorDisplay = errorDisplay
724 self._backend = backend
726 self._messagetime = 0.0
727 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
728 self._messageview = widgetTree.get_widget("messages_view")
729 self._messageviewselection = None
730 self._onMessageviewRowActivatedId = 0
732 textrenderer = gtk.CellRendererText()
733 # @todo Make seperate columns for each item in message payload
734 self._messageviewColumn = gtk.TreeViewColumn("Messages")
735 self._messageviewColumn.pack_start(textrenderer, expand=True)
736 self._messageviewColumn.add_attribute(textrenderer, "markup", 1)
737 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
739 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
742 assert self._backend.is_authed()
743 self._messageview.set_model(self._messagemodel)
745 self._messageview.append_column(self._messageviewColumn)
746 self._messageviewselection = self._messageview.get_selection()
747 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
749 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
752 self._messageview.disconnect(self._onMessageviewRowActivatedId)
753 self._messageview.remove_column(self._messageviewColumn)
754 self._messageview.set_model(None)
756 def number_selected(self, action, number, message):
758 @note Actual dial function is patched in later
760 raise NotImplementedError
763 if (time.time() - self._messagetime) < 300:
765 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
766 backgroundPopulate.setDaemon(True)
767 backgroundPopulate.start()
770 self._messagetime = 0.0
771 self._messagemodel.clear()
773 def _idly_populate_messageview(self):
774 self._messagetime = time.time()
775 self._messagemodel.clear()
778 messageItems = self._backend.get_messages()
779 except RuntimeError, e:
780 self._errorDisplay.push_exception_with_lock(e)
781 self._messagetime = 0.0
784 for header, number, relativeDate, message in messageItems:
785 number = make_ugly(number)
786 print "Discarding", header, relativeDate
787 item = (number, message)
788 with gtk_toolbox.gtk_lock():
789 self._messagemodel.append(item)
793 def _on_messageview_row_activated(self, treeview, path, view_column):
794 model, itr = self._messageviewselection.get_selected()
798 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
799 description = self._messagemodel.get_value(itr, 1)
800 print repr(contactPhoneNumbers), repr(description)
802 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
803 if action == PhoneTypeSelector.ACTION_CANCEL:
807 self.number_selected(action, phoneNumber, message)
808 self._messageviewselection.unselect_all()
811 class ContactsView(object):
813 def __init__(self, widgetTree, backend, errorDisplay):
814 self._errorDisplay = errorDisplay
815 self._backend = backend
817 self._addressBook = None
818 self._addressBookFactories = [DummyAddressBook()]
820 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
821 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
823 self._contactstime = 0.0
824 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
825 self._contactsviewselection = None
826 self._contactsview = widgetTree.get_widget("contactsview")
828 self._contactColumn = gtk.TreeViewColumn("Contact")
829 displayContactSource = False
830 if displayContactSource:
831 textrenderer = gtk.CellRendererText()
832 self._contactColumn.pack_start(textrenderer, expand=False)
833 self._contactColumn.add_attribute(textrenderer, 'text', 0)
834 textrenderer = gtk.CellRendererText()
835 self._contactColumn.pack_start(textrenderer, expand=True)
836 self._contactColumn.add_attribute(textrenderer, 'text', 1)
837 textrenderer = gtk.CellRendererText()
838 self._contactColumn.pack_start(textrenderer, expand=True)
839 self._contactColumn.add_attribute(textrenderer, 'text', 4)
840 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
841 self._contactColumn.set_sort_column_id(1)
842 self._contactColumn.set_visible(True)
844 self._onContactsviewRowActivatedId = 0
845 self._onAddressbookComboChangedId = 0
846 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
849 assert self._backend.is_authed()
851 self._contactsview.set_model(self._contactsmodel)
852 self._contactsview.append_column(self._contactColumn)
853 self._contactsviewselection = self._contactsview.get_selection()
854 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
856 self._booksList.clear()
857 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
858 if factoryName and bookName:
859 entryName = "%s: %s" % (factoryName, bookName)
861 entryName = factoryName
865 entryName = "Bad name (%d)" % factoryId
866 row = (str(factoryId), bookId, entryName)
867 self._booksList.append(row)
869 self._booksSelectionBox.set_model(self._booksList)
870 cell = gtk.CellRendererText()
871 self._booksSelectionBox.pack_start(cell, True)
872 self._booksSelectionBox.add_attribute(cell, 'text', 2)
873 self._booksSelectionBox.set_active(0)
875 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
876 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
879 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
880 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
882 self._booksSelectionBox.clear()
883 self._booksSelectionBox.set_model(None)
884 self._contactsview.set_model(None)
885 self._contactsview.remove_column(self._contactColumn)
887 def number_selected(self, action, number, message):
889 @note Actual dial function is patched in later
891 raise NotImplementedError
893 def get_addressbooks(self):
895 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
897 for i, factory in enumerate(self._addressBookFactories):
898 for bookFactory, bookId, bookName in factory.get_addressbooks():
899 yield (i, bookId), (factory.factory_name(), bookName)
901 def open_addressbook(self, bookFactoryId, bookId):
902 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
903 self._contactstime = 0
904 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
905 backgroundPopulate.setDaemon(True)
906 backgroundPopulate.start()
909 if (time.time() - self._contactstime) < 300:
911 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
912 backgroundPopulate.setDaemon(True)
913 backgroundPopulate.start()
916 self._contactstime = 0.0
917 self._contactsmodel.clear()
919 def clear_caches(self):
920 for factory in self._addressBookFactories:
921 factory.clear_caches()
922 self._addressBook.clear_caches()
924 def append(self, book):
925 self._addressBookFactories.append(book)
927 def extend(self, books):
928 self._addressBookFactories.extend(books)
930 def _idly_populate_contactsview(self):
931 #@todo Add a lock so only one code path can be in here at a time
934 # completely disable updating the treeview while we populate the data
935 self._contactsview.freeze_child_notify()
936 self._contactsview.set_model(None)
938 addressBook = self._addressBook
940 contacts = addressBook.get_contacts()
941 except RuntimeError, e:
943 self._contactstime = 0.0
944 self._errorDisplay.push_exception_with_lock(e)
945 for contactId, contactName in contacts:
946 contactType = (addressBook.contact_source_short_name(contactId), )
947 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
949 # restart the treeview data rendering
950 self._contactsview.set_model(self._contactsmodel)
951 self._contactsview.thaw_child_notify()
954 def _on_addressbook_combo_changed(self, *args, **kwds):
955 itr = self._booksSelectionBox.get_active_iter()
958 factoryId = int(self._booksList.get_value(itr, 0))
959 bookId = self._booksList.get_value(itr, 1)
960 self.open_addressbook(factoryId, bookId)
962 def _on_contactsview_row_activated(self, treeview, path, view_column):
963 model, itr = self._contactsviewselection.get_selected()
967 contactId = self._contactsmodel.get_value(itr, 3)
968 contactName = self._contactsmodel.get_value(itr, 1)
970 contactDetails = self._addressBook.get_contact_details(contactId)
971 except RuntimeError, e:
973 self._contactstime = 0.0
974 self._errorDisplay.push_exception(e)
975 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
977 if len(contactPhoneNumbers) == 0:
980 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
981 if action == PhoneTypeSelector.ACTION_CANCEL:
985 self.number_selected(action, phoneNumber, message)
986 self._contactsviewselection.unselect_all()