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 self._typeview = self._widgetTree.get_widget("phonetypes")
340 self._typeview.connect("row-activated", self._on_phonetype_select)
342 self._action = self.ACTION_CANCEL
344 def run(self, contactDetails, message = ""):
345 self._action = self.ACTION_CANCEL
346 self._typemodel.clear()
347 self._typeview.set_model(self._typemodel)
349 # Add the column to the treeview
350 textrenderer = gtk.CellRendererText()
351 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
352 self._typeview.append_column(numberColumn)
354 textrenderer = gtk.CellRendererText()
355 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
356 self._typeview.append_column(typeColumn)
358 self._typeviewselection = self._typeview.get_selection()
359 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
361 for phoneType, phoneNumber in contactDetails:
362 display = " - ".join((phoneNumber, phoneType))
364 row = (phoneNumber, display)
365 self._typemodel.append(row)
367 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
369 self._message.set_markup(message)
372 self._message.set_markup("")
375 userResponse = self._dialog.run()
377 if userResponse == gtk.RESPONSE_OK:
378 phoneNumber = self._get_number()
382 self._action = self.ACTION_CANCEL
384 if self._action == self.ACTION_SEND_SMS:
385 smsMessage = self._smsDialog.run(phoneNumber, message)
390 self._action = self.ACTION_CANCEL
392 self._typeviewselection.unselect_all()
393 self._typeview.remove_column(numberColumn)
394 self._typeview.remove_column(typeColumn)
395 self._typeview.set_model(None)
397 return self._action, phoneNumber, smsMessage
399 def _get_number(self):
400 model, itr = self._typeviewselection.get_selected()
404 phoneNumber = self._typemodel.get_value(itr, 0)
407 def _on_phonetype_dial(self, *args):
408 self._dialog.response(gtk.RESPONSE_OK)
409 self._action = self.ACTION_DIAL
411 def _on_phonetype_send_sms(self, *args):
412 self._dialog.response(gtk.RESPONSE_OK)
413 self._action = self.ACTION_SEND_SMS
415 def _on_phonetype_select(self, *args):
416 self._dialog.response(gtk.RESPONSE_OK)
417 self._action = self.ACTION_SELECT
419 def _on_phonetype_cancel(self, *args):
420 self._dialog.response(gtk.RESPONSE_CANCEL)
421 self._action = self.ACTION_CANCEL
424 class SmsEntryDialog(object):
428 def __init__(self, widgetTree, gcBackend):
429 self._gcBackend = gcBackend
430 self._widgetTree = widgetTree
431 self._dialog = self._widgetTree.get_widget("smsDialog")
433 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
434 self._smsButton.connect("clicked", self._on_send)
436 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
437 self._cancelButton.connect("clicked", self._on_cancel)
439 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
440 self._message = self._widgetTree.get_widget("smsMessage")
441 self._smsEntry = self._widgetTree.get_widget("smsEntry")
442 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
444 def run(self, number, message = ""):
446 self._message.set_markup(message)
449 self._message.set_markup("")
451 self._smsEntry.get_buffer().set_text("")
452 self._update_letter_count()
454 userResponse = self._dialog.run()
455 if userResponse == gtk.RESPONSE_OK:
456 entryBuffer = self._smsEntry.get_buffer()
457 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
458 enteredMessage = enteredMessage[0:self.MAX_CHAR]
463 return enteredMessage
465 def _update_letter_count(self, *args):
466 entryLength = self._smsEntry.get_buffer().get_char_count()
467 charsLeft = self.MAX_CHAR - entryLength
468 self._letterCountLabel.set_text(str(charsLeft))
470 self._smsButton.set_sensitive(False)
472 self._smsButton.set_sensitive(True)
474 def _on_entry_changed(self, *args):
475 self._update_letter_count()
477 def _on_send(self, *args):
478 self._dialog.response(gtk.RESPONSE_OK)
480 def _on_cancel(self, *args):
481 self._dialog.response(gtk.RESPONSE_CANCEL)
484 class Dialpad(object):
486 def __init__(self, widgetTree, errorDisplay):
487 self._errorDisplay = errorDisplay
488 self._numberdisplay = widgetTree.get_widget("numberdisplay")
489 self._dialButton = widgetTree.get_widget("dial")
490 self._phonenumber = ""
491 self._prettynumber = ""
492 self._clearall_id = None
495 "on_dial_clicked": self._on_dial_clicked,
496 "on_digit_clicked": self._on_digit_clicked,
497 "on_clear_number": self._on_clear_number,
498 "on_back_clicked": self._on_backspace,
499 "on_back_pressed": self._on_back_pressed,
500 "on_back_released": self._on_back_released,
502 widgetTree.signal_autoconnect(callbackMapping)
505 self._dialButton.grab_focus()
510 def dial(self, number):
512 @note Actual dial function is patched in later
514 raise NotImplementedError
516 def get_number(self):
517 return self._phonenumber
519 def set_number(self, number):
521 Set the callback phonenumber
524 self._phonenumber = make_ugly(number)
525 self._prettynumber = make_pretty(self._phonenumber)
526 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
528 self._errorDisplay.push_exception(e)
533 def _on_dial_clicked(self, widget):
534 self.dial(self.get_number())
536 def _on_clear_number(self, *args):
539 def _on_digit_clicked(self, widget):
540 self.set_number(self._phonenumber + widget.get_name()[-1])
542 def _on_backspace(self, widget):
543 self.set_number(self._phonenumber[:-1])
545 def _on_clearall(self):
549 def _on_back_pressed(self, widget):
550 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
552 def _on_back_released(self, widget):
553 if self._clearall_id is not None:
554 gobject.source_remove(self._clearall_id)
555 self._clearall_id = None
558 class AccountInfo(object):
560 def __init__(self, widgetTree, backend, errorDisplay):
561 self._errorDisplay = errorDisplay
562 self._backend = backend
564 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
565 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
566 self._callbackCombo = widgetTree.get_widget("callbackcombo")
567 self._onCallbackentryChangedId = 0
570 assert self._backend.is_authed()
571 self._accountViewNumberDisplay.set_use_markup(True)
572 self.set_account_number("")
573 self._callbackList.clear()
575 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
578 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
580 self._callbackList.clear()
582 def get_selected_callback_number(self):
583 return make_ugly(self._callbackCombo.get_child().get_text())
585 def set_account_number(self, number):
587 Displays current account number
589 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
592 self.populate_callback_combo()
593 self.set_account_number(self._backend.get_account_number())
596 self._callbackCombo.get_child().set_text("")
597 self.set_account_number("")
599 def populate_callback_combo(self):
600 self._callbackList.clear()
602 callbackNumbers = self._backend.get_callback_numbers()
603 except RuntimeError, e:
604 self._errorDisplay.push_exception(e)
607 for number, description in callbackNumbers.iteritems():
608 self._callbackList.append((make_pretty(number),))
610 self._callbackCombo.set_model(self._callbackList)
611 self._callbackCombo.set_text_column(0)
613 callbackNumber = self._backend.get_callback_number()
614 except RuntimeError, e:
615 self._errorDisplay.push_exception(e)
617 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
619 def _on_callbackentry_changed(self, *args):
621 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
624 text = self.get_selected_callback_number()
625 if not self._backend.is_valid_syntax(text):
626 self._errorDisplay.push_message("%s is not a valid callback number" % text)
627 elif text == self._backend.get_callback_number():
628 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
630 self._backend.set_callback_number(text)
631 except RuntimeError, e:
632 self._errorDisplay.push_exception(e)
635 class RecentCallsView(object):
637 def __init__(self, widgetTree, backend, errorDisplay):
638 self._errorDisplay = errorDisplay
639 self._backend = backend
641 self._recenttime = 0.0
642 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
643 self._recentview = widgetTree.get_widget("recentview")
644 self._recentviewselection = None
645 self._onRecentviewRowActivatedId = 0
647 # @todo Make seperate columns for each item in recent item payload
648 textrenderer = gtk.CellRendererText()
649 self._recentviewColumn = gtk.TreeViewColumn("Calls")
650 self._recentviewColumn.pack_start(textrenderer, expand=True)
651 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
652 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
654 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
657 assert self._backend.is_authed()
658 self._recentview.set_model(self._recentmodel)
660 self._recentview.append_column(self._recentviewColumn)
661 self._recentviewselection = self._recentview.get_selection()
662 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
664 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
667 self._recentview.disconnect(self._onRecentviewRowActivatedId)
668 self._recentview.remove_column(self._recentviewColumn)
669 self._recentview.set_model(None)
671 def number_selected(self, action, number, message):
673 @note Actual dial function is patched in later
675 raise NotImplementedError
678 if (time.time() - self._recenttime) < 300:
680 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
681 backgroundPopulate.setDaemon(True)
682 backgroundPopulate.start()
685 self._recenttime = 0.0
686 self._recentmodel.clear()
688 def _idly_populate_recentview(self):
689 self._recenttime = time.time()
690 self._recentmodel.clear()
693 recentItems = self._backend.get_recent()
694 except RuntimeError, e:
695 self._errorDisplay.push_exception_with_lock(e)
696 self._recenttime = 0.0
699 for personsName, phoneNumber, date, action in recentItems:
700 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
701 item = (phoneNumber, description)
702 with gtk_toolbox.gtk_lock():
703 self._recentmodel.append(item)
707 def _on_recentview_row_activated(self, treeview, path, view_column):
708 model, itr = self._recentviewselection.get_selected()
712 number = self._recentmodel.get_value(itr, 0)
713 number = make_ugly(number)
714 contactPhoneNumbers = [("Phone", number)]
715 description = self._recentmodel.get_value(itr, 1)
717 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
718 if action == PhoneTypeSelector.ACTION_CANCEL:
722 self.number_selected(action, phoneNumber, message)
723 self._recentviewselection.unselect_all()
726 class MessagesView(object):
728 def __init__(self, widgetTree, backend, errorDisplay):
729 self._errorDisplay = errorDisplay
730 self._backend = backend
732 self._messagetime = 0.0
733 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
734 self._messageview = widgetTree.get_widget("messages_view")
735 self._messageviewselection = None
736 self._onMessageviewRowActivatedId = 0
738 textrenderer = gtk.CellRendererText()
739 # @todo Make seperate columns for each item in message payload
740 self._messageviewColumn = gtk.TreeViewColumn("Messages")
741 self._messageviewColumn.pack_start(textrenderer, expand=True)
742 self._messageviewColumn.add_attribute(textrenderer, "markup", 1)
743 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
745 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
748 assert self._backend.is_authed()
749 self._messageview.set_model(self._messagemodel)
751 self._messageview.append_column(self._messageviewColumn)
752 self._messageviewselection = self._messageview.get_selection()
753 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
755 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
758 self._messageview.disconnect(self._onMessageviewRowActivatedId)
759 self._messageview.remove_column(self._messageviewColumn)
760 self._messageview.set_model(None)
762 def number_selected(self, action, number, message):
764 @note Actual dial function is patched in later
766 raise NotImplementedError
769 if (time.time() - self._messagetime) < 300:
771 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
772 backgroundPopulate.setDaemon(True)
773 backgroundPopulate.start()
776 self._messagetime = 0.0
777 self._messagemodel.clear()
779 def _idly_populate_messageview(self):
780 self._messagetime = time.time()
781 self._messagemodel.clear()
784 messageItems = self._backend.get_messages()
785 except RuntimeError, e:
786 self._errorDisplay.push_exception_with_lock(e)
787 self._messagetime = 0.0
790 for header, number, relativeDate, message in messageItems:
791 number = make_ugly(number)
792 row = (number, message)
793 with gtk_toolbox.gtk_lock():
794 self._messagemodel.append(row)
798 def _on_messageview_row_activated(self, treeview, path, view_column):
799 model, itr = self._messageviewselection.get_selected()
803 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
804 description = self._messagemodel.get_value(itr, 1)
806 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
807 if action == PhoneTypeSelector.ACTION_CANCEL:
811 self.number_selected(action, phoneNumber, message)
812 self._messageviewselection.unselect_all()
815 class ContactsView(object):
817 def __init__(self, widgetTree, backend, errorDisplay):
818 self._errorDisplay = errorDisplay
819 self._backend = backend
821 self._addressBook = None
822 self._addressBookFactories = [DummyAddressBook()]
824 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
825 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
827 self._contactstime = 0.0
828 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
829 self._contactsviewselection = None
830 self._contactsview = widgetTree.get_widget("contactsview")
832 self._contactColumn = gtk.TreeViewColumn("Contact")
833 displayContactSource = False
834 if displayContactSource:
835 textrenderer = gtk.CellRendererText()
836 self._contactColumn.pack_start(textrenderer, expand=False)
837 self._contactColumn.add_attribute(textrenderer, 'text', 0)
838 textrenderer = gtk.CellRendererText()
839 self._contactColumn.pack_start(textrenderer, expand=True)
840 self._contactColumn.add_attribute(textrenderer, 'text', 1)
841 textrenderer = gtk.CellRendererText()
842 self._contactColumn.pack_start(textrenderer, expand=True)
843 self._contactColumn.add_attribute(textrenderer, 'text', 4)
844 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
845 self._contactColumn.set_sort_column_id(1)
846 self._contactColumn.set_visible(True)
848 self._onContactsviewRowActivatedId = 0
849 self._onAddressbookComboChangedId = 0
850 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
853 assert self._backend.is_authed()
855 self._contactsview.set_model(self._contactsmodel)
856 self._contactsview.append_column(self._contactColumn)
857 self._contactsviewselection = self._contactsview.get_selection()
858 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
860 self._booksList.clear()
861 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
862 if factoryName and bookName:
863 entryName = "%s: %s" % (factoryName, bookName)
865 entryName = factoryName
869 entryName = "Bad name (%d)" % factoryId
870 row = (str(factoryId), bookId, entryName)
871 self._booksList.append(row)
873 self._booksSelectionBox.set_model(self._booksList)
874 cell = gtk.CellRendererText()
875 self._booksSelectionBox.pack_start(cell, True)
876 self._booksSelectionBox.add_attribute(cell, 'text', 2)
877 self._booksSelectionBox.set_active(0)
879 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
880 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
883 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
884 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
886 self._booksSelectionBox.clear()
887 self._booksSelectionBox.set_model(None)
888 self._contactsview.set_model(None)
889 self._contactsview.remove_column(self._contactColumn)
891 def number_selected(self, action, number, message):
893 @note Actual dial function is patched in later
895 raise NotImplementedError
897 def get_addressbooks(self):
899 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
901 for i, factory in enumerate(self._addressBookFactories):
902 for bookFactory, bookId, bookName in factory.get_addressbooks():
903 yield (i, bookId), (factory.factory_name(), bookName)
905 def open_addressbook(self, bookFactoryId, bookId):
906 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
907 self._contactstime = 0
908 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
909 backgroundPopulate.setDaemon(True)
910 backgroundPopulate.start()
913 if (time.time() - self._contactstime) < 300:
915 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
916 backgroundPopulate.setDaemon(True)
917 backgroundPopulate.start()
920 self._contactstime = 0.0
921 self._contactsmodel.clear()
923 def clear_caches(self):
924 for factory in self._addressBookFactories:
925 factory.clear_caches()
926 self._addressBook.clear_caches()
928 def append(self, book):
929 self._addressBookFactories.append(book)
931 def extend(self, books):
932 self._addressBookFactories.extend(books)
934 def _idly_populate_contactsview(self):
935 #@todo Add a lock so only one code path can be in here at a time
938 # completely disable updating the treeview while we populate the data
939 self._contactsview.freeze_child_notify()
940 self._contactsview.set_model(None)
942 addressBook = self._addressBook
944 contacts = addressBook.get_contacts()
945 except RuntimeError, e:
947 self._contactstime = 0.0
948 self._errorDisplay.push_exception_with_lock(e)
949 for contactId, contactName in contacts:
950 contactType = (addressBook.contact_source_short_name(contactId), )
951 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
953 # restart the treeview data rendering
954 self._contactsview.set_model(self._contactsmodel)
955 self._contactsview.thaw_child_notify()
958 def _on_addressbook_combo_changed(self, *args, **kwds):
959 itr = self._booksSelectionBox.get_active_iter()
962 factoryId = int(self._booksList.get_value(itr, 0))
963 bookId = self._booksList.get_value(itr, 1)
964 self.open_addressbook(factoryId, bookId)
966 def _on_contactsview_row_activated(self, treeview, path, view_column):
967 model, itr = self._contactsviewselection.get_selected()
971 contactId = self._contactsmodel.get_value(itr, 3)
972 contactName = self._contactsmodel.get_value(itr, 1)
974 contactDetails = self._addressBook.get_contact_details(contactId)
975 except RuntimeError, e:
977 self._contactstime = 0.0
978 self._errorDisplay.push_exception(e)
979 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
981 if len(contactPhoneNumbers) == 0:
984 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
985 if action == PhoneTypeSelector.ACTION_CANCEL:
989 self.number_selected(action, phoneNumber, message)
990 self._contactsviewselection.unselect_all()