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()
379 phoneNumber = make_ugly(phoneNumber)
383 self._action = self.ACTION_CANCEL
385 if self._action == self.ACTION_SEND_SMS:
386 smsMessage = self._smsDialog.run(phoneNumber, message)
389 self._action = self.ACTION_CANCEL
393 self._typeviewselection.unselect_all()
394 self._typeview.remove_column(numberColumn)
395 self._typeview.remove_column(typeColumn)
396 self._typeview.set_model(None)
398 return self._action, phoneNumber, smsMessage
400 def _get_number(self):
401 model, itr = self._typeviewselection.get_selected()
405 phoneNumber = self._typemodel.get_value(itr, 0)
408 def _on_phonetype_dial(self, *args):
409 self._dialog.response(gtk.RESPONSE_OK)
410 self._action = self.ACTION_DIAL
412 def _on_phonetype_send_sms(self, *args):
413 self._dialog.response(gtk.RESPONSE_OK)
414 self._action = self.ACTION_SEND_SMS
416 def _on_phonetype_select(self, *args):
417 self._dialog.response(gtk.RESPONSE_OK)
418 self._action = self.ACTION_SELECT
420 def _on_phonetype_cancel(self, *args):
421 self._dialog.response(gtk.RESPONSE_CANCEL)
422 self._action = self.ACTION_CANCEL
425 class SmsEntryDialog(object):
429 def __init__(self, widgetTree, gcBackend):
430 self._gcBackend = gcBackend
431 self._widgetTree = widgetTree
432 self._dialog = self._widgetTree.get_widget("smsDialog")
434 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
435 self._smsButton.connect("clicked", self._on_send)
437 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
438 self._cancelButton.connect("clicked", self._on_cancel)
440 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
441 self._message = self._widgetTree.get_widget("smsMessage")
442 self._smsEntry = self._widgetTree.get_widget("smsEntry")
443 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
445 def run(self, number, message = ""):
447 self._message.set_markup(message)
450 self._message.set_markup("")
452 self._smsEntry.get_buffer().set_text("")
453 self._update_letter_count()
455 userResponse = self._dialog.run()
456 if userResponse == gtk.RESPONSE_OK:
457 entryBuffer = self._smsEntry.get_buffer()
458 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
459 enteredMessage = enteredMessage[0:self.MAX_CHAR]
464 return enteredMessage
466 def _update_letter_count(self, *args):
467 entryLength = self._smsEntry.get_buffer().get_char_count()
468 charsLeft = self.MAX_CHAR - entryLength
469 self._letterCountLabel.set_text(str(charsLeft))
471 self._smsButton.set_sensitive(False)
473 self._smsButton.set_sensitive(True)
475 def _on_entry_changed(self, *args):
476 self._update_letter_count()
478 def _on_send(self, *args):
479 self._dialog.response(gtk.RESPONSE_OK)
481 def _on_cancel(self, *args):
482 self._dialog.response(gtk.RESPONSE_CANCEL)
485 class Dialpad(object):
487 def __init__(self, widgetTree, errorDisplay):
488 self._errorDisplay = errorDisplay
489 self._numberdisplay = widgetTree.get_widget("numberdisplay")
490 self._dialButton = widgetTree.get_widget("dial")
491 self._phonenumber = ""
492 self._prettynumber = ""
493 self._clearall_id = None
496 "on_dial_clicked": self._on_dial_clicked,
497 "on_digit_clicked": self._on_digit_clicked,
498 "on_clear_number": self._on_clear_number,
499 "on_back_clicked": self._on_backspace,
500 "on_back_pressed": self._on_back_pressed,
501 "on_back_released": self._on_back_released,
503 widgetTree.signal_autoconnect(callbackMapping)
506 self._dialButton.grab_focus()
511 def dial(self, number):
513 @note Actual dial function is patched in later
515 raise NotImplementedError
517 def get_number(self):
518 return self._phonenumber
520 def set_number(self, number):
522 Set the callback phonenumber
525 self._phonenumber = make_ugly(number)
526 self._prettynumber = make_pretty(self._phonenumber)
527 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
529 self._errorDisplay.push_exception(e)
534 def _on_dial_clicked(self, widget):
535 self.dial(self.get_number())
537 def _on_clear_number(self, *args):
540 def _on_digit_clicked(self, widget):
541 self.set_number(self._phonenumber + widget.get_name()[-1])
543 def _on_backspace(self, widget):
544 self.set_number(self._phonenumber[:-1])
546 def _on_clearall(self):
550 def _on_back_pressed(self, widget):
551 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
553 def _on_back_released(self, widget):
554 if self._clearall_id is not None:
555 gobject.source_remove(self._clearall_id)
556 self._clearall_id = None
559 class AccountInfo(object):
561 def __init__(self, widgetTree, backend, errorDisplay):
562 self._errorDisplay = errorDisplay
563 self._backend = backend
565 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
566 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
567 self._callbackCombo = widgetTree.get_widget("callbackcombo")
568 self._onCallbackentryChangedId = 0
571 assert self._backend.is_authed()
572 self._accountViewNumberDisplay.set_use_markup(True)
573 self.set_account_number("")
574 self._callbackList.clear()
576 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
579 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
581 self._callbackList.clear()
583 def get_selected_callback_number(self):
584 return make_ugly(self._callbackCombo.get_child().get_text())
586 def set_account_number(self, number):
588 Displays current account number
590 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
593 self.populate_callback_combo()
594 self.set_account_number(self._backend.get_account_number())
597 self._callbackCombo.get_child().set_text("")
598 self.set_account_number("")
600 def populate_callback_combo(self):
601 self._callbackList.clear()
603 callbackNumbers = self._backend.get_callback_numbers()
604 except RuntimeError, e:
605 self._errorDisplay.push_exception(e)
608 for number, description in callbackNumbers.iteritems():
609 self._callbackList.append((make_pretty(number),))
611 self._callbackCombo.set_model(self._callbackList)
612 self._callbackCombo.set_text_column(0)
614 callbackNumber = self._backend.get_callback_number()
615 except RuntimeError, e:
616 self._errorDisplay.push_exception(e)
618 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
620 def _on_callbackentry_changed(self, *args):
622 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
625 text = self.get_selected_callback_number()
626 if not self._backend.is_valid_syntax(text):
627 self._errorDisplay.push_message("%s is not a valid callback number" % text)
628 elif text == self._backend.get_callback_number():
629 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
631 self._backend.set_callback_number(text)
632 except RuntimeError, e:
633 self._errorDisplay.push_exception(e)
636 class RecentCallsView(object):
638 def __init__(self, widgetTree, backend, errorDisplay):
639 self._errorDisplay = errorDisplay
640 self._backend = backend
642 self._recenttime = 0.0
643 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
644 self._recentview = widgetTree.get_widget("recentview")
645 self._recentviewselection = None
646 self._onRecentviewRowActivatedId = 0
648 # @todo Make seperate columns for each item in recent item payload
649 textrenderer = gtk.CellRendererText()
650 self._recentviewColumn = gtk.TreeViewColumn("Calls")
651 self._recentviewColumn.pack_start(textrenderer, expand=True)
652 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
653 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
655 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
658 assert self._backend.is_authed()
659 self._recentview.set_model(self._recentmodel)
661 self._recentview.append_column(self._recentviewColumn)
662 self._recentviewselection = self._recentview.get_selection()
663 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
665 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
668 self._recentview.disconnect(self._onRecentviewRowActivatedId)
669 self._recentview.remove_column(self._recentviewColumn)
670 self._recentview.set_model(None)
672 def number_selected(self, action, number, message):
674 @note Actual dial function is patched in later
676 raise NotImplementedError
679 if (time.time() - self._recenttime) < 300:
681 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
682 backgroundPopulate.setDaemon(True)
683 backgroundPopulate.start()
686 self._recenttime = 0.0
687 self._recentmodel.clear()
689 def _idly_populate_recentview(self):
690 self._recenttime = time.time()
691 self._recentmodel.clear()
694 recentItems = self._backend.get_recent()
695 except RuntimeError, e:
696 self._errorDisplay.push_exception_with_lock(e)
697 self._recenttime = 0.0
700 for personsName, phoneNumber, date, action in recentItems:
701 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
702 item = (phoneNumber, description)
703 with gtk_toolbox.gtk_lock():
704 self._recentmodel.append(item)
708 def _on_recentview_row_activated(self, treeview, path, view_column):
709 model, itr = self._recentviewselection.get_selected()
713 number = self._recentmodel.get_value(itr, 0)
714 number = make_ugly(number)
715 contactPhoneNumbers = [("Phone", number)]
716 description = self._recentmodel.get_value(itr, 1)
718 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
719 if action == PhoneTypeSelector.ACTION_CANCEL:
723 self.number_selected(action, phoneNumber, message)
724 self._recentviewselection.unselect_all()
727 class MessagesView(object):
729 def __init__(self, widgetTree, backend, errorDisplay):
730 self._errorDisplay = errorDisplay
731 self._backend = backend
733 self._messagetime = 0.0
734 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
735 self._messageview = widgetTree.get_widget("messages_view")
736 self._messageviewselection = None
737 self._onMessageviewRowActivatedId = 0
739 textrenderer = gtk.CellRendererText()
740 # @todo Make seperate columns for each item in message payload
741 self._messageviewColumn = gtk.TreeViewColumn("Messages")
742 self._messageviewColumn.pack_start(textrenderer, expand=True)
743 self._messageviewColumn.add_attribute(textrenderer, "markup", 1)
744 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
746 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
749 assert self._backend.is_authed()
750 self._messageview.set_model(self._messagemodel)
752 self._messageview.append_column(self._messageviewColumn)
753 self._messageviewselection = self._messageview.get_selection()
754 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
756 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
759 self._messageview.disconnect(self._onMessageviewRowActivatedId)
760 self._messageview.remove_column(self._messageviewColumn)
761 self._messageview.set_model(None)
763 def number_selected(self, action, number, message):
765 @note Actual dial function is patched in later
767 raise NotImplementedError
770 if (time.time() - self._messagetime) < 300:
772 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
773 backgroundPopulate.setDaemon(True)
774 backgroundPopulate.start()
777 self._messagetime = 0.0
778 self._messagemodel.clear()
780 def _idly_populate_messageview(self):
781 self._messagetime = time.time()
782 self._messagemodel.clear()
785 messageItems = self._backend.get_messages()
786 except RuntimeError, e:
787 self._errorDisplay.push_exception_with_lock(e)
788 self._messagetime = 0.0
791 for header, number, relativeDate, message in messageItems:
792 number = make_ugly(number)
793 row = (number, message)
794 with gtk_toolbox.gtk_lock():
795 self._messagemodel.append(row)
799 def _on_messageview_row_activated(self, treeview, path, view_column):
800 model, itr = self._messageviewselection.get_selected()
804 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
805 description = self._messagemodel.get_value(itr, 1)
807 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
808 if action == PhoneTypeSelector.ACTION_CANCEL:
812 self.number_selected(action, phoneNumber, message)
813 self._messageviewselection.unselect_all()
816 class ContactsView(object):
818 def __init__(self, widgetTree, backend, errorDisplay):
819 self._errorDisplay = errorDisplay
820 self._backend = backend
822 self._addressBook = None
823 self._addressBookFactories = [DummyAddressBook()]
825 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
826 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
828 self._contactstime = 0.0
829 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
830 self._contactsviewselection = None
831 self._contactsview = widgetTree.get_widget("contactsview")
833 self._contactColumn = gtk.TreeViewColumn("Contact")
834 displayContactSource = False
835 if displayContactSource:
836 textrenderer = gtk.CellRendererText()
837 self._contactColumn.pack_start(textrenderer, expand=False)
838 self._contactColumn.add_attribute(textrenderer, 'text', 0)
839 textrenderer = gtk.CellRendererText()
840 self._contactColumn.pack_start(textrenderer, expand=True)
841 self._contactColumn.add_attribute(textrenderer, 'text', 1)
842 textrenderer = gtk.CellRendererText()
843 self._contactColumn.pack_start(textrenderer, expand=True)
844 self._contactColumn.add_attribute(textrenderer, 'text', 4)
845 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
846 self._contactColumn.set_sort_column_id(1)
847 self._contactColumn.set_visible(True)
849 self._onContactsviewRowActivatedId = 0
850 self._onAddressbookComboChangedId = 0
851 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
854 assert self._backend.is_authed()
856 self._contactsview.set_model(self._contactsmodel)
857 self._contactsview.append_column(self._contactColumn)
858 self._contactsviewselection = self._contactsview.get_selection()
859 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
861 self._booksList.clear()
862 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
863 if factoryName and bookName:
864 entryName = "%s: %s" % (factoryName, bookName)
866 entryName = factoryName
870 entryName = "Bad name (%d)" % factoryId
871 row = (str(factoryId), bookId, entryName)
872 self._booksList.append(row)
874 self._booksSelectionBox.set_model(self._booksList)
875 cell = gtk.CellRendererText()
876 self._booksSelectionBox.pack_start(cell, True)
877 self._booksSelectionBox.add_attribute(cell, 'text', 2)
878 self._booksSelectionBox.set_active(0)
880 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
881 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
884 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
885 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
887 self._booksSelectionBox.clear()
888 self._booksSelectionBox.set_model(None)
889 self._contactsview.set_model(None)
890 self._contactsview.remove_column(self._contactColumn)
892 def number_selected(self, action, number, message):
894 @note Actual dial function is patched in later
896 raise NotImplementedError
898 def get_addressbooks(self):
900 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
902 for i, factory in enumerate(self._addressBookFactories):
903 for bookFactory, bookId, bookName in factory.get_addressbooks():
904 yield (i, bookId), (factory.factory_name(), bookName)
906 def open_addressbook(self, bookFactoryId, bookId):
907 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
908 self._contactstime = 0
909 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
910 backgroundPopulate.setDaemon(True)
911 backgroundPopulate.start()
914 if (time.time() - self._contactstime) < 300:
916 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
917 backgroundPopulate.setDaemon(True)
918 backgroundPopulate.start()
921 self._contactstime = 0.0
922 self._contactsmodel.clear()
924 def clear_caches(self):
925 for factory in self._addressBookFactories:
926 factory.clear_caches()
927 self._addressBook.clear_caches()
929 def append(self, book):
930 self._addressBookFactories.append(book)
932 def extend(self, books):
933 self._addressBookFactories.extend(books)
935 def _idly_populate_contactsview(self):
936 #@todo Add a lock so only one code path can be in here at a time
939 # completely disable updating the treeview while we populate the data
940 self._contactsview.freeze_child_notify()
941 self._contactsview.set_model(None)
943 addressBook = self._addressBook
945 contacts = addressBook.get_contacts()
946 except RuntimeError, e:
948 self._contactstime = 0.0
949 self._errorDisplay.push_exception_with_lock(e)
950 for contactId, contactName in contacts:
951 contactType = (addressBook.contact_source_short_name(contactId), )
952 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
954 # restart the treeview data rendering
955 self._contactsview.set_model(self._contactsmodel)
956 self._contactsview.thaw_child_notify()
959 def _on_addressbook_combo_changed(self, *args, **kwds):
960 itr = self._booksSelectionBox.get_active_iter()
963 factoryId = int(self._booksList.get_value(itr, 0))
964 bookId = self._booksList.get_value(itr, 1)
965 self.open_addressbook(factoryId, bookId)
967 def _on_contactsview_row_activated(self, treeview, path, view_column):
968 model, itr = self._contactsviewselection.get_selected()
972 contactId = self._contactsmodel.get_value(itr, 3)
973 contactName = self._contactsmodel.get_value(itr, 1)
975 contactDetails = self._addressBook.get_contact_details(contactId)
976 except RuntimeError, e:
978 self._contactstime = 0.0
979 self._errorDisplay.push_exception(e)
980 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
982 if len(contactPhoneNumbers) == 0:
985 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
986 if action == PhoneTypeSelector.ACTION_CANCEL:
990 self.number_selected(action, phoneNumber, message)
991 self._contactsviewselection.unselect_all()