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 = "", parent = None):
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 if parent is not None:
375 self._dialog.set_transient_for(parent)
378 userResponse = self._dialog.run()
382 if userResponse == gtk.RESPONSE_OK:
383 phoneNumber = self._get_number()
384 phoneNumber = make_ugly(phoneNumber)
388 self._action = self.ACTION_CANCEL
390 if self._action == self.ACTION_SEND_SMS:
391 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
394 self._action = self.ACTION_CANCEL
398 self._typeviewselection.unselect_all()
399 self._typeview.remove_column(numberColumn)
400 self._typeview.remove_column(typeColumn)
401 self._typeview.set_model(None)
403 return self._action, phoneNumber, smsMessage
405 def _get_number(self):
406 model, itr = self._typeviewselection.get_selected()
410 phoneNumber = self._typemodel.get_value(itr, 0)
413 def _on_phonetype_dial(self, *args):
414 self._dialog.response(gtk.RESPONSE_OK)
415 self._action = self.ACTION_DIAL
417 def _on_phonetype_send_sms(self, *args):
418 self._dialog.response(gtk.RESPONSE_OK)
419 self._action = self.ACTION_SEND_SMS
421 def _on_phonetype_select(self, *args):
422 self._dialog.response(gtk.RESPONSE_OK)
423 self._action = self.ACTION_SELECT
425 def _on_phonetype_cancel(self, *args):
426 self._dialog.response(gtk.RESPONSE_CANCEL)
427 self._action = self.ACTION_CANCEL
430 class SmsEntryDialog(object):
434 def __init__(self, widgetTree, gcBackend):
435 self._gcBackend = gcBackend
436 self._widgetTree = widgetTree
437 self._dialog = self._widgetTree.get_widget("smsDialog")
439 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
440 self._smsButton.connect("clicked", self._on_send)
442 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
443 self._cancelButton.connect("clicked", self._on_cancel)
445 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
446 self._message = self._widgetTree.get_widget("smsMessage")
447 self._smsEntry = self._widgetTree.get_widget("smsEntry")
448 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
450 def run(self, number, message = "", parent = None):
452 self._message.set_markup(message)
455 self._message.set_markup("")
457 self._smsEntry.get_buffer().set_text("")
458 self._update_letter_count()
460 if parent is not None:
461 self._dialog.set_transient_for(parent)
464 userResponse = self._dialog.run()
468 if userResponse == gtk.RESPONSE_OK:
469 entryBuffer = self._smsEntry.get_buffer()
470 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
471 enteredMessage = enteredMessage[0:self.MAX_CHAR]
475 return enteredMessage
477 def _update_letter_count(self, *args):
478 entryLength = self._smsEntry.get_buffer().get_char_count()
479 charsLeft = self.MAX_CHAR - entryLength
480 self._letterCountLabel.set_text(str(charsLeft))
482 self._smsButton.set_sensitive(False)
484 self._smsButton.set_sensitive(True)
486 def _on_entry_changed(self, *args):
487 self._update_letter_count()
489 def _on_send(self, *args):
490 self._dialog.response(gtk.RESPONSE_OK)
492 def _on_cancel(self, *args):
493 self._dialog.response(gtk.RESPONSE_CANCEL)
496 class Dialpad(object):
498 def __init__(self, widgetTree, errorDisplay):
499 self._errorDisplay = errorDisplay
500 self._numberdisplay = widgetTree.get_widget("numberdisplay")
501 self._dialButton = widgetTree.get_widget("dial")
502 self._phonenumber = ""
503 self._prettynumber = ""
504 self._clearall_id = None
507 "on_dial_clicked": self._on_dial_clicked,
508 "on_digit_clicked": self._on_digit_clicked,
509 "on_clear_number": self._on_clear_number,
510 "on_back_clicked": self._on_backspace,
511 "on_back_pressed": self._on_back_pressed,
512 "on_back_released": self._on_back_released,
514 widgetTree.signal_autoconnect(callbackMapping)
517 self._dialButton.grab_focus()
522 def dial(self, number):
524 @note Actual dial function is patched in later
526 raise NotImplementedError
528 def get_number(self):
529 return self._phonenumber
531 def set_number(self, number):
533 Set the callback phonenumber
536 self._phonenumber = make_ugly(number)
537 self._prettynumber = make_pretty(self._phonenumber)
538 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
540 self._errorDisplay.push_exception(e)
549 def load_settings(self, config, section):
552 def save_settings(self, config, section):
554 @note Thread Agnostic
558 def _on_dial_clicked(self, widget):
559 self.dial(self.get_number())
561 def _on_clear_number(self, *args):
564 def _on_digit_clicked(self, widget):
565 self.set_number(self._phonenumber + widget.get_name()[-1])
567 def _on_backspace(self, widget):
568 self.set_number(self._phonenumber[:-1])
570 def _on_clearall(self):
574 def _on_back_pressed(self, widget):
575 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
577 def _on_back_released(self, widget):
578 if self._clearall_id is not None:
579 gobject.source_remove(self._clearall_id)
580 self._clearall_id = None
583 class AccountInfo(object):
585 def __init__(self, widgetTree, backend, errorDisplay):
586 self._errorDisplay = errorDisplay
587 self._backend = backend
588 self._isPopulated = False
590 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
591 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
592 self._callbackCombo = widgetTree.get_widget("callbackcombo")
593 self._onCallbackentryChangedId = 0
595 self._defaultCallback = ""
598 assert self._backend.is_authed()
599 self._accountViewNumberDisplay.set_use_markup(True)
600 self.set_account_number("")
601 self._callbackList.clear()
602 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
603 self.update(force=True)
606 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
610 self._callbackList.clear()
612 def get_selected_callback_number(self):
613 return make_ugly(self._callbackCombo.get_child().get_text())
615 def set_account_number(self, number):
617 Displays current account number
619 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
621 def update(self, force = False):
622 if not force and self._isPopulated:
624 self._populate_callback_combo()
625 self.set_account_number(self._backend.get_account_number())
628 self._callbackCombo.get_child().set_text("")
629 self.set_account_number("")
630 self._isPopulated = False
634 return "Account Info"
636 def load_settings(self, config, section):
637 self._defaultCallback = config.get(section, "callback")
639 def save_settings(self, config, section):
641 @note Thread Agnostic
643 callback = self.get_selected_callback_number()
644 config.set(section, "callback", callback)
646 def _populate_callback_combo(self):
647 self._isPopulated = True
648 self._callbackList.clear()
650 callbackNumbers = self._backend.get_callback_numbers()
651 except RuntimeError, e:
652 self._errorDisplay.push_exception(e)
653 self._isPopulated = False
656 for number, description in callbackNumbers.iteritems():
657 self._callbackList.append((make_pretty(number),))
659 self._callbackCombo.set_model(self._callbackList)
660 self._callbackCombo.set_text_column(0)
661 #callbackNumber = self._backend.get_callback_number()
662 callbackNumber = self._defaultCallback
663 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
665 def _set_callback_number(self, number):
667 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
670 if not self._backend.is_valid_syntax(number):
671 self._errorDisplay.push_message("%s is not a valid callback number" % number)
672 elif number == self._backend.get_callback_number():
673 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
675 self._backend.set_callback_number(number)
676 warnings.warn("Callback number set to %s" % self._backend.get_callback_number(), UserWarning, 2)
677 except RuntimeError, e:
678 self._errorDisplay.push_exception(e)
680 def _on_callbackentry_changed(self, *args):
682 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
684 text = self.get_selected_callback_number()
685 self._set_callback_number(text)
688 class RecentCallsView(object):
690 def __init__(self, widgetTree, backend, errorDisplay):
691 self._errorDisplay = errorDisplay
692 self._backend = backend
694 self._isPopulated = False
695 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
696 self._recentview = widgetTree.get_widget("recentview")
697 self._recentviewselection = None
698 self._onRecentviewRowActivatedId = 0
700 # @todo Make seperate columns for each item in recent item payload
701 textrenderer = gtk.CellRendererText()
702 self._recentviewColumn = gtk.TreeViewColumn("Calls")
703 self._recentviewColumn.pack_start(textrenderer, expand=True)
704 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
705 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
707 self._window = gtk_toolbox.find_parent_window(self._recentview)
708 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
711 assert self._backend.is_authed()
712 self._recentview.set_model(self._recentmodel)
714 self._recentview.append_column(self._recentviewColumn)
715 self._recentviewselection = self._recentview.get_selection()
716 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
718 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
721 self._recentview.disconnect(self._onRecentviewRowActivatedId)
725 self._recentview.remove_column(self._recentviewColumn)
726 self._recentview.set_model(None)
728 def number_selected(self, action, number, message):
730 @note Actual dial function is patched in later
732 raise NotImplementedError
734 def update(self, force = False):
735 if not force and self._isPopulated:
737 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
738 backgroundPopulate.setDaemon(True)
739 backgroundPopulate.start()
742 self._isPopulated = False
743 self._recentmodel.clear()
747 return "Recent Calls"
749 def load_settings(self, config, section):
752 def save_settings(self, config, section):
754 @note Thread Agnostic
758 def _idly_populate_recentview(self):
759 self._isPopulated = True
760 self._recentmodel.clear()
763 recentItems = self._backend.get_recent()
764 except RuntimeError, e:
765 self._errorDisplay.push_exception_with_lock(e)
766 self._isPopulated = False
769 for personsName, phoneNumber, date, action in recentItems:
770 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
771 item = (phoneNumber, description)
772 with gtk_toolbox.gtk_lock():
773 self._recentmodel.append(item)
777 def _on_recentview_row_activated(self, treeview, path, view_column):
778 model, itr = self._recentviewselection.get_selected()
782 number = self._recentmodel.get_value(itr, 0)
783 number = make_ugly(number)
784 contactPhoneNumbers = [("Phone", number)]
785 description = self._recentmodel.get_value(itr, 1)
787 action, phoneNumber, message = self._phoneTypeSelector.run(
789 message = description,
790 parent = self._window,
792 if action == PhoneTypeSelector.ACTION_CANCEL:
796 self.number_selected(action, phoneNumber, message)
797 self._recentviewselection.unselect_all()
800 class MessagesView(object):
802 def __init__(self, widgetTree, backend, errorDisplay):
803 self._errorDisplay = errorDisplay
804 self._backend = backend
806 self._isPopulated = False
807 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
808 self._messageview = widgetTree.get_widget("messages_view")
809 self._messageviewselection = None
810 self._onMessageviewRowActivatedId = 0
812 textrenderer = gtk.CellRendererText()
813 # @todo Make seperate columns for each item in message payload
814 self._messageviewColumn = gtk.TreeViewColumn("Messages")
815 self._messageviewColumn.pack_start(textrenderer, expand=True)
816 self._messageviewColumn.add_attribute(textrenderer, "markup", 1)
817 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
819 self._window = gtk_toolbox.find_parent_window(self._messageview)
820 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
823 assert self._backend.is_authed()
824 self._messageview.set_model(self._messagemodel)
826 self._messageview.append_column(self._messageviewColumn)
827 self._messageviewselection = self._messageview.get_selection()
828 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
830 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
833 self._messageview.disconnect(self._onMessageviewRowActivatedId)
837 self._messageview.remove_column(self._messageviewColumn)
838 self._messageview.set_model(None)
840 def number_selected(self, action, number, message):
842 @note Actual dial function is patched in later
844 raise NotImplementedError
846 def update(self, force = False):
847 if not force and self._isPopulated:
849 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
850 backgroundPopulate.setDaemon(True)
851 backgroundPopulate.start()
854 self._isPopulated = False
855 self._messagemodel.clear()
861 def load_settings(self, config, section):
864 def save_settings(self, config, section):
866 @note Thread Agnostic
870 def _idly_populate_messageview(self):
871 self._isPopulated = True
872 self._messagemodel.clear()
875 messageItems = self._backend.get_messages()
876 except RuntimeError, e:
877 self._errorDisplay.push_exception_with_lock(e)
878 self._isPopulated = False
881 for header, number, relativeDate, message in messageItems:
882 number = make_ugly(number)
883 row = (number, message)
884 with gtk_toolbox.gtk_lock():
885 self._messagemodel.append(row)
889 def _on_messageview_row_activated(self, treeview, path, view_column):
890 model, itr = self._messageviewselection.get_selected()
894 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
895 description = self._messagemodel.get_value(itr, 1)
897 action, phoneNumber, message = self._phoneTypeSelector.run(
899 message = description,
900 parent = self._window,
902 if action == PhoneTypeSelector.ACTION_CANCEL:
906 self.number_selected(action, phoneNumber, message)
907 self._messageviewselection.unselect_all()
910 class ContactsView(object):
912 def __init__(self, widgetTree, backend, errorDisplay):
913 self._errorDisplay = errorDisplay
914 self._backend = backend
916 self._addressBook = None
917 self._addressBookFactories = [DummyAddressBook()]
919 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
920 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
922 self._isPopulated = False
923 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
924 self._contactsviewselection = None
925 self._contactsview = widgetTree.get_widget("contactsview")
927 self._contactColumn = gtk.TreeViewColumn("Contact")
928 displayContactSource = False
929 if displayContactSource:
930 textrenderer = gtk.CellRendererText()
931 self._contactColumn.pack_start(textrenderer, expand=False)
932 self._contactColumn.add_attribute(textrenderer, 'text', 0)
933 textrenderer = gtk.CellRendererText()
934 self._contactColumn.pack_start(textrenderer, expand=True)
935 self._contactColumn.add_attribute(textrenderer, 'text', 1)
936 textrenderer = gtk.CellRendererText()
937 self._contactColumn.pack_start(textrenderer, expand=True)
938 self._contactColumn.add_attribute(textrenderer, 'text', 4)
939 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
940 self._contactColumn.set_sort_column_id(1)
941 self._contactColumn.set_visible(True)
943 self._onContactsviewRowActivatedId = 0
944 self._onAddressbookComboChangedId = 0
945 self._window = gtk_toolbox.find_parent_window(self._contactsview)
946 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
949 assert self._backend.is_authed()
951 self._contactsview.set_model(self._contactsmodel)
952 self._contactsview.append_column(self._contactColumn)
953 self._contactsviewselection = self._contactsview.get_selection()
954 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
956 self._booksList.clear()
957 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
958 if factoryName and bookName:
959 entryName = "%s: %s" % (factoryName, bookName)
961 entryName = factoryName
965 entryName = "Bad name (%d)" % factoryId
966 row = (str(factoryId), bookId, entryName)
967 self._booksList.append(row)
969 self._booksSelectionBox.set_model(self._booksList)
970 cell = gtk.CellRendererText()
971 self._booksSelectionBox.pack_start(cell, True)
972 self._booksSelectionBox.add_attribute(cell, 'text', 2)
973 self._booksSelectionBox.set_active(0)
975 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
976 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
979 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
980 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
984 self._booksSelectionBox.clear()
985 self._booksSelectionBox.set_model(None)
986 self._contactsview.set_model(None)
987 self._contactsview.remove_column(self._contactColumn)
989 def number_selected(self, action, number, message):
991 @note Actual dial function is patched in later
993 raise NotImplementedError
995 def get_addressbooks(self):
997 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
999 for i, factory in enumerate(self._addressBookFactories):
1000 for bookFactory, bookId, bookName in factory.get_addressbooks():
1001 yield (i, bookId), (factory.factory_name(), bookName)
1003 def open_addressbook(self, bookFactoryId, bookId):
1004 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1005 self.update(force=True)
1007 def update(self, force = False):
1008 if not force and self._isPopulated:
1010 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1011 backgroundPopulate.setDaemon(True)
1012 backgroundPopulate.start()
1015 self._isPopulated = False
1016 self._contactsmodel.clear()
1018 def clear_caches(self):
1019 for factory in self._addressBookFactories:
1020 factory.clear_caches()
1021 self._addressBook.clear_caches()
1023 def append(self, book):
1024 self._addressBookFactories.append(book)
1026 def extend(self, books):
1027 self._addressBookFactories.extend(books)
1033 def load_settings(self, config, section):
1036 def save_settings(self, config, section):
1038 @note Thread Agnostic
1042 def _idly_populate_contactsview(self):
1043 self._isPopulated = True
1046 # completely disable updating the treeview while we populate the data
1047 self._contactsview.freeze_child_notify()
1048 self._contactsview.set_model(None)
1050 addressBook = self._addressBook
1052 contacts = addressBook.get_contacts()
1053 except RuntimeError, e:
1055 self._isPopulated = False
1056 self._errorDisplay.push_exception_with_lock(e)
1057 for contactId, contactName in contacts:
1058 contactType = (addressBook.contact_source_short_name(contactId), )
1059 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1061 # restart the treeview data rendering
1062 self._contactsview.set_model(self._contactsmodel)
1063 self._contactsview.thaw_child_notify()
1066 def _on_addressbook_combo_changed(self, *args, **kwds):
1067 itr = self._booksSelectionBox.get_active_iter()
1070 factoryId = int(self._booksList.get_value(itr, 0))
1071 bookId = self._booksList.get_value(itr, 1)
1072 self.open_addressbook(factoryId, bookId)
1074 def _on_contactsview_row_activated(self, treeview, path, view_column):
1075 model, itr = self._contactsviewselection.get_selected()
1079 contactId = self._contactsmodel.get_value(itr, 3)
1080 contactName = self._contactsmodel.get_value(itr, 1)
1082 contactDetails = self._addressBook.get_contact_details(contactId)
1083 except RuntimeError, e:
1085 self._errorDisplay.push_exception(e)
1086 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1088 if len(contactPhoneNumbers) == 0:
1091 action, phoneNumber, message = self._phoneTypeSelector.run(
1092 contactPhoneNumbers,
1093 message = contactName,
1094 parent = self._window,
1096 if action == PhoneTypeSelector.ACTION_CANCEL:
1100 self.number_selected(action, phoneNumber, message)
1101 self._contactsviewselection.unselect_all()