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
33 def make_ugly(prettynumber):
35 function to take a phone number and strip out all non-numeric
38 >>> make_ugly("+012-(345)-678-90")
42 uglynumber = re.sub('\D', '', prettynumber)
46 def make_pretty(phonenumber):
48 Function to take a phone number and return the pretty version
50 if phonenumber begins with 0:
52 if phonenumber begins with 1: ( for gizmo callback numbers )
54 if phonenumber is 13 digits:
56 if phonenumber is 10 digits:
60 >>> make_pretty("1234567")
62 >>> make_pretty("2345678901")
64 >>> make_pretty("12345678901")
66 >>> make_pretty("01234567890")
69 if phonenumber is None or phonenumber is "":
72 phonenumber = make_ugly(phonenumber)
74 if len(phonenumber) < 3:
77 if phonenumber[0] == "0":
79 prettynumber += "+%s" % phonenumber[0:3]
80 if 3 < len(phonenumber):
81 prettynumber += "-(%s)" % phonenumber[3:6]
82 if 6 < len(phonenumber):
83 prettynumber += "-%s" % phonenumber[6:9]
84 if 9 < len(phonenumber):
85 prettynumber += "-%s" % phonenumber[9:]
87 elif len(phonenumber) <= 7:
88 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
89 elif len(phonenumber) > 8 and phonenumber[0] == "1":
90 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
91 elif len(phonenumber) > 7:
92 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
98 Decorator that makes a generator-function into a function that will continue execution on next call
102 def decorated_func(*args, **kwds):
104 a.append(func(*args, **kwds))
108 except StopIteration:
112 decorated_func.__name__ = func.__name__
113 decorated_func.__doc__ = func.__doc__
114 decorated_func.__dict__.update(func.__dict__)
116 return decorated_func
119 class DummyAddressBook(object):
121 Minimal example of both an addressbook factory and an addressbook
124 def clear_caches(self):
127 def get_addressbooks(self):
129 @returns Iterable of (Address Book Factory, Book Id, Book Name)
131 yield self, "", "None"
133 def open_addressbook(self, bookId):
137 def contact_source_short_name(contactId):
147 @returns Iterable of (contact id, contact name)
152 def get_contact_details(contactId):
154 @returns Iterable of (Phone Type, Phone Number)
159 class MergedAddressBook(object):
161 Merger of all addressbooks
164 def __init__(self, addressbookFactories, sorter = None):
165 self.__addressbookFactories = addressbookFactories
166 self.__addressbooks = None
167 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
169 def clear_caches(self):
170 self.__addressbooks = None
171 for factory in self.__addressbookFactories:
172 factory.clear_caches()
174 def get_addressbooks(self):
176 @returns Iterable of (Address Book Factory, Book Id, Book Name)
180 def open_addressbook(self, bookId):
183 def contact_source_short_name(self, contactId):
184 if self.__addressbooks is None:
186 bookIndex, originalId = contactId.split("-", 1)
187 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
191 return "All Contacts"
193 def get_contacts(self):
195 @returns Iterable of (contact id, contact name)
197 if self.__addressbooks is None:
198 self.__addressbooks = list(
199 factory.open_addressbook(id)
200 for factory in self.__addressbookFactories
201 for (f, id, name) in factory.get_addressbooks()
204 ("-".join([str(bookIndex), contactId]), contactName)
205 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
206 for (contactId, contactName) in addressbook.get_contacts()
208 sortedContacts = self.__sort_contacts(contacts)
209 return sortedContacts
211 def get_contact_details(self, contactId):
213 @returns Iterable of (Phone Type, Phone Number)
215 if self.__addressbooks is None:
217 bookIndex, originalId = contactId.split("-", 1)
218 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
221 def null_sorter(contacts):
223 Good for speed/low memory
228 def basic_firtname_sorter(contacts):
230 Expects names in "First Last" format
233 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
234 for (contactId, contactName) in contacts
236 contactsWithKey.sort()
237 return (contactData for (lastName, contactData) in contactsWithKey)
240 def basic_lastname_sorter(contacts):
242 Expects names in "First Last" format
245 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
246 for (contactId, contactName) in contacts
248 contactsWithKey.sort()
249 return (contactData for (lastName, contactData) in contactsWithKey)
252 def reversed_firtname_sorter(contacts):
254 Expects names in "Last, First" format
257 (contactName.split(", ", 1)[-1], (contactId, contactName))
258 for (contactId, contactName) in contacts
260 contactsWithKey.sort()
261 return (contactData for (lastName, contactData) in contactsWithKey)
264 def reversed_lastname_sorter(contacts):
266 Expects names in "Last, First" format
269 (contactName.split(", ", 1)[0], (contactId, contactName))
270 for (contactId, contactName) in contacts
272 contactsWithKey.sort()
273 return (contactData for (lastName, contactData) in contactsWithKey)
276 def guess_firstname(name):
278 return name.split(", ", 1)[-1]
280 return name.rsplit(" ", 1)[0]
283 def guess_lastname(name):
285 return name.split(", ", 1)[0]
287 return name.rsplit(" ", 1)[-1]
290 def advanced_firstname_sorter(cls, contacts):
292 (cls.guess_firstname(contactName), (contactId, contactName))
293 for (contactId, contactName) in contacts
295 contactsWithKey.sort()
296 return (contactData for (lastName, contactData) in contactsWithKey)
299 def advanced_lastname_sorter(cls, contacts):
301 (cls.guess_lastname(contactName), (contactId, contactName))
302 for (contactId, contactName) in contacts
304 contactsWithKey.sort()
305 return (contactData for (lastName, contactData) in contactsWithKey)
308 class PhoneTypeSelector(object):
310 ACTION_CANCEL = "cancel"
311 ACTION_SELECT = "select"
313 ACTION_SEND_SMS = "sms"
315 def __init__(self, widgetTree, gcBackend):
316 self._gcBackend = gcBackend
317 self._widgetTree = widgetTree
319 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
320 self._smsDialog = SmsEntryDialog(self._widgetTree, self._gcBackend)
322 self._smsButton = self._widgetTree.get_widget("sms_button")
323 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
325 self._dialButton = self._widgetTree.get_widget("dial_button")
326 self._dialButton.connect("clicked", self._on_phonetype_dial)
328 self._selectButton = self._widgetTree.get_widget("select_button")
329 self._selectButton.connect("clicked", self._on_phonetype_select)
331 self._cancelButton = self._widgetTree.get_widget("cancel_button")
332 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
334 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
335 self._typeviewselection = None
337 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
338 self._typeview = self._widgetTree.get_widget("phonetypes")
339 self._typeview.connect("row-activated", self._on_phonetype_select)
341 self._action = self.ACTION_CANCEL
343 def run(self, contactDetails, message = ""):
344 self._action = self.ACTION_CANCEL
345 self._typemodel.clear()
346 self._typeview.set_model(self._typemodel)
348 # Add the column to the treeview
349 textrenderer = gtk.CellRendererText()
350 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
351 self._typeview.append_column(numberColumn)
353 textrenderer = gtk.CellRendererText()
354 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
355 self._typeview.append_column(typeColumn)
357 self._typeviewselection = self._typeview.get_selection()
358 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
360 for phoneType, phoneNumber in contactDetails:
361 display = " - ".join((phoneNumber, phoneType))
363 row = (phoneNumber, display)
364 self._typemodel.append(row)
366 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
368 self._message.set_markup(message)
371 self._message.set_markup("")
374 userResponse = self._dialog.run()
376 if userResponse == gtk.RESPONSE_OK:
377 phoneNumber = self._get_number()
378 phoneNumber = make_ugly(phoneNumber)
382 self._action = self.ACTION_CANCEL
384 if self._action == self.ACTION_SEND_SMS:
385 smsMessage = self._smsDialog.run(phoneNumber, message)
388 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)
537 def load_settings(self, config, section):
540 def save_settings(self, config, section):
542 @note Thread Agnostic
546 def _on_dial_clicked(self, widget):
547 self.dial(self.get_number())
549 def _on_clear_number(self, *args):
552 def _on_digit_clicked(self, widget):
553 self.set_number(self._phonenumber + widget.get_name()[-1])
555 def _on_backspace(self, widget):
556 self.set_number(self._phonenumber[:-1])
558 def _on_clearall(self):
562 def _on_back_pressed(self, widget):
563 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
565 def _on_back_released(self, widget):
566 if self._clearall_id is not None:
567 gobject.source_remove(self._clearall_id)
568 self._clearall_id = None
571 class AccountInfo(object):
573 def __init__(self, widgetTree, backend, errorDisplay):
574 self._errorDisplay = errorDisplay
575 self._backend = backend
577 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
578 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
579 self._callbackCombo = widgetTree.get_widget("callbackcombo")
580 self._onCallbackentryChangedId = 0
582 self._defaultCallback = ""
585 assert self._backend.is_authed()
586 self._accountViewNumberDisplay.set_use_markup(True)
587 self.set_account_number("")
588 self._callbackList.clear()
590 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
593 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
595 self._callbackList.clear()
597 def get_selected_callback_number(self):
598 return make_ugly(self._callbackCombo.get_child().get_text())
600 def set_account_number(self, number):
602 Displays current account number
604 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
606 def update(self, force = False):
607 self._populate_callback_combo()
608 self.set_account_number(self._backend.get_account_number())
611 self._callbackCombo.get_child().set_text("")
612 self.set_account_number("")
616 return "Account Info"
618 def load_settings(self, config, section):
619 self._defaultCallback = config.get(section, "callback")
621 def save_settings(self, config, section):
623 @note Thread Agnostic
625 callback = self.get_selected_callback_number()
626 config.set(section, "callback", callback)
628 def _populate_callback_combo(self):
629 self._callbackList.clear()
631 callbackNumbers = self._backend.get_callback_numbers()
632 except RuntimeError, e:
633 self._errorDisplay.push_exception(e)
636 for number, description in callbackNumbers.iteritems():
637 self._callbackList.append((make_pretty(number),))
639 self._callbackCombo.set_model(self._callbackList)
640 self._callbackCombo.set_text_column(0)
642 callbackNumber = self._backend.get_callback_number()
643 except RuntimeError, e:
644 callbackNumber = self._defaultCallback
645 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
647 def _on_callbackentry_changed(self, *args):
649 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
652 text = self.get_selected_callback_number()
653 if not self._backend.is_valid_syntax(text):
654 self._errorDisplay.push_message("%s is not a valid callback number" % text)
655 elif text == self._backend.get_callback_number():
656 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
658 self._backend.set_callback_number(text)
659 except RuntimeError, e:
660 self._errorDisplay.push_exception(e)
663 class RecentCallsView(object):
665 def __init__(self, widgetTree, backend, errorDisplay):
666 self._errorDisplay = errorDisplay
667 self._backend = backend
669 self._isPopulated = False
670 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
671 self._recentview = widgetTree.get_widget("recentview")
672 self._recentviewselection = None
673 self._onRecentviewRowActivatedId = 0
675 # @todo Make seperate columns for each item in recent item payload
676 textrenderer = gtk.CellRendererText()
677 self._recentviewColumn = gtk.TreeViewColumn("Calls")
678 self._recentviewColumn.pack_start(textrenderer, expand=True)
679 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
680 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
682 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
685 assert self._backend.is_authed()
686 self._recentview.set_model(self._recentmodel)
688 self._recentview.append_column(self._recentviewColumn)
689 self._recentviewselection = self._recentview.get_selection()
690 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
692 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
695 self._recentview.disconnect(self._onRecentviewRowActivatedId)
696 self._recentview.remove_column(self._recentviewColumn)
697 self._recentview.set_model(None)
699 def number_selected(self, action, number, message):
701 @note Actual dial function is patched in later
703 raise NotImplementedError
705 def update(self, force = False):
706 if not force and self._isPopulated:
708 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
709 backgroundPopulate.setDaemon(True)
710 backgroundPopulate.start()
713 self._isPopulated = False
714 self._recentmodel.clear()
718 return "Recent Calls"
720 def load_settings(self, config, section):
723 def save_settings(self, config, section):
725 @note Thread Agnostic
729 def _idly_populate_recentview(self):
730 self._isPopulated = True
731 self._recentmodel.clear()
734 recentItems = self._backend.get_recent()
735 except RuntimeError, e:
736 self._errorDisplay.push_exception_with_lock(e)
737 self._isPopulated = False
740 for personsName, phoneNumber, date, action in recentItems:
741 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
742 item = (phoneNumber, description)
743 with gtk_toolbox.gtk_lock():
744 self._recentmodel.append(item)
748 def _on_recentview_row_activated(self, treeview, path, view_column):
749 model, itr = self._recentviewselection.get_selected()
753 number = self._recentmodel.get_value(itr, 0)
754 number = make_ugly(number)
755 contactPhoneNumbers = [("Phone", number)]
756 description = self._recentmodel.get_value(itr, 1)
758 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
759 if action == PhoneTypeSelector.ACTION_CANCEL:
763 self.number_selected(action, phoneNumber, message)
764 self._recentviewselection.unselect_all()
767 class MessagesView(object):
769 def __init__(self, widgetTree, backend, errorDisplay):
770 self._errorDisplay = errorDisplay
771 self._backend = backend
773 self._isPopulated = False
774 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
775 self._messageview = widgetTree.get_widget("messages_view")
776 self._messageviewselection = None
777 self._onMessageviewRowActivatedId = 0
779 textrenderer = gtk.CellRendererText()
780 # @todo Make seperate columns for each item in message payload
781 self._messageviewColumn = gtk.TreeViewColumn("Messages")
782 self._messageviewColumn.pack_start(textrenderer, expand=True)
783 self._messageviewColumn.add_attribute(textrenderer, "markup", 1)
784 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
786 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
789 assert self._backend.is_authed()
790 self._messageview.set_model(self._messagemodel)
792 self._messageview.append_column(self._messageviewColumn)
793 self._messageviewselection = self._messageview.get_selection()
794 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
796 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
799 self._messageview.disconnect(self._onMessageviewRowActivatedId)
800 self._messageview.remove_column(self._messageviewColumn)
801 self._messageview.set_model(None)
803 def number_selected(self, action, number, message):
805 @note Actual dial function is patched in later
807 raise NotImplementedError
809 def update(self, force = False):
810 if not force and self._isPopulated:
812 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
813 backgroundPopulate.setDaemon(True)
814 backgroundPopulate.start()
817 self._isPopulated = False
818 self._messagemodel.clear()
824 def load_settings(self, config, section):
827 def save_settings(self, config, section):
829 @note Thread Agnostic
833 def _idly_populate_messageview(self):
834 self._isPopulated = True
835 self._messagemodel.clear()
838 messageItems = self._backend.get_messages()
839 except RuntimeError, e:
840 self._errorDisplay.push_exception_with_lock(e)
841 self._isPopulated = False
844 for header, number, relativeDate, message in messageItems:
845 number = make_ugly(number)
846 row = (number, message)
847 with gtk_toolbox.gtk_lock():
848 self._messagemodel.append(row)
852 def _on_messageview_row_activated(self, treeview, path, view_column):
853 model, itr = self._messageviewselection.get_selected()
857 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
858 description = self._messagemodel.get_value(itr, 1)
860 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
861 if action == PhoneTypeSelector.ACTION_CANCEL:
865 self.number_selected(action, phoneNumber, message)
866 self._messageviewselection.unselect_all()
869 class ContactsView(object):
871 def __init__(self, widgetTree, backend, errorDisplay):
872 self._errorDisplay = errorDisplay
873 self._backend = backend
875 self._addressBook = None
876 self._addressBookFactories = [DummyAddressBook()]
878 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
879 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
881 self._isPopulated = False
882 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
883 self._contactsviewselection = None
884 self._contactsview = widgetTree.get_widget("contactsview")
886 self._contactColumn = gtk.TreeViewColumn("Contact")
887 displayContactSource = False
888 if displayContactSource:
889 textrenderer = gtk.CellRendererText()
890 self._contactColumn.pack_start(textrenderer, expand=False)
891 self._contactColumn.add_attribute(textrenderer, 'text', 0)
892 textrenderer = gtk.CellRendererText()
893 self._contactColumn.pack_start(textrenderer, expand=True)
894 self._contactColumn.add_attribute(textrenderer, 'text', 1)
895 textrenderer = gtk.CellRendererText()
896 self._contactColumn.pack_start(textrenderer, expand=True)
897 self._contactColumn.add_attribute(textrenderer, 'text', 4)
898 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
899 self._contactColumn.set_sort_column_id(1)
900 self._contactColumn.set_visible(True)
902 self._onContactsviewRowActivatedId = 0
903 self._onAddressbookComboChangedId = 0
904 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
907 assert self._backend.is_authed()
909 self._contactsview.set_model(self._contactsmodel)
910 self._contactsview.append_column(self._contactColumn)
911 self._contactsviewselection = self._contactsview.get_selection()
912 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
914 self._booksList.clear()
915 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
916 if factoryName and bookName:
917 entryName = "%s: %s" % (factoryName, bookName)
919 entryName = factoryName
923 entryName = "Bad name (%d)" % factoryId
924 row = (str(factoryId), bookId, entryName)
925 self._booksList.append(row)
927 self._booksSelectionBox.set_model(self._booksList)
928 cell = gtk.CellRendererText()
929 self._booksSelectionBox.pack_start(cell, True)
930 self._booksSelectionBox.add_attribute(cell, 'text', 2)
931 self._booksSelectionBox.set_active(0)
933 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
934 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
937 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
938 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
940 self._booksSelectionBox.clear()
941 self._booksSelectionBox.set_model(None)
942 self._contactsview.set_model(None)
943 self._contactsview.remove_column(self._contactColumn)
945 def number_selected(self, action, number, message):
947 @note Actual dial function is patched in later
949 raise NotImplementedError
951 def get_addressbooks(self):
953 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
955 for i, factory in enumerate(self._addressBookFactories):
956 for bookFactory, bookId, bookName in factory.get_addressbooks():
957 yield (i, bookId), (factory.factory_name(), bookName)
959 def open_addressbook(self, bookFactoryId, bookId):
960 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
961 self.update(force=True)
963 def update(self, force = False):
964 if not force and self._isPopulated:
966 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
967 backgroundPopulate.setDaemon(True)
968 backgroundPopulate.start()
971 self._isPopulated = False
972 self._contactsmodel.clear()
974 def clear_caches(self):
975 for factory in self._addressBookFactories:
976 factory.clear_caches()
977 self._addressBook.clear_caches()
979 def append(self, book):
980 self._addressBookFactories.append(book)
982 def extend(self, books):
983 self._addressBookFactories.extend(books)
989 def load_settings(self, config, section):
992 def save_settings(self, config, section):
994 @note Thread Agnostic
998 def _idly_populate_contactsview(self):
999 self._isPopulated = True
1002 # completely disable updating the treeview while we populate the data
1003 self._contactsview.freeze_child_notify()
1004 self._contactsview.set_model(None)
1006 addressBook = self._addressBook
1008 contacts = addressBook.get_contacts()
1009 except RuntimeError, e:
1011 self._isPopulated = False
1012 self._errorDisplay.push_exception_with_lock(e)
1013 for contactId, contactName in contacts:
1014 contactType = (addressBook.contact_source_short_name(contactId), )
1015 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1017 # restart the treeview data rendering
1018 self._contactsview.set_model(self._contactsmodel)
1019 self._contactsview.thaw_child_notify()
1022 def _on_addressbook_combo_changed(self, *args, **kwds):
1023 itr = self._booksSelectionBox.get_active_iter()
1026 factoryId = int(self._booksList.get_value(itr, 0))
1027 bookId = self._booksList.get_value(itr, 1)
1028 self.open_addressbook(factoryId, bookId)
1030 def _on_contactsview_row_activated(self, treeview, path, view_column):
1031 model, itr = self._contactsviewselection.get_selected()
1035 contactId = self._contactsmodel.get_value(itr, 3)
1036 contactName = self._contactsmodel.get_value(itr, 1)
1038 contactDetails = self._addressBook.get_contact_details(contactId)
1039 except RuntimeError, e:
1041 self._errorDisplay.push_exception(e)
1042 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1044 if len(contactPhoneNumbers) == 0:
1047 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
1048 if action == PhoneTypeSelector.ACTION_CANCEL:
1052 self.number_selected(action, phoneNumber, message)
1053 self._contactsviewselection.unselect_all()