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):
695 def __init__(self, widgetTree, backend, errorDisplay):
696 self._errorDisplay = errorDisplay
697 self._backend = backend
699 self._isPopulated = False
700 self._recentmodel = gtk.ListStore(
701 gobject.TYPE_STRING, # number
702 gobject.TYPE_STRING, # date
703 gobject.TYPE_STRING, # action
704 gobject.TYPE_STRING, # from
706 self._recentview = widgetTree.get_widget("recentview")
707 self._recentviewselection = None
708 self._onRecentviewRowActivatedId = 0
710 textrenderer = gtk.CellRendererText()
711 textrenderer.set_property("yalign", 0)
712 self._dateColumn = gtk.TreeViewColumn("Date")
713 self._dateColumn.pack_start(textrenderer, expand=True)
714 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
716 textrenderer = gtk.CellRendererText()
717 textrenderer.set_property("yalign", 0)
718 self._actionColumn = gtk.TreeViewColumn("Action")
719 self._actionColumn.pack_start(textrenderer, expand=True)
720 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
722 textrenderer = gtk.CellRendererText()
723 textrenderer.set_property("yalign", 0)
724 self._fromColumn = gtk.TreeViewColumn("From")
725 self._fromColumn.pack_start(textrenderer, expand=True)
726 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
727 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
729 self._window = gtk_toolbox.find_parent_window(self._recentview)
730 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
733 assert self._backend.is_authed()
734 self._recentview.set_model(self._recentmodel)
736 self._recentview.append_column(self._dateColumn)
737 self._recentview.append_column(self._actionColumn)
738 self._recentview.append_column(self._fromColumn)
739 self._recentviewselection = self._recentview.get_selection()
740 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
742 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
745 self._recentview.disconnect(self._onRecentviewRowActivatedId)
749 self._recentview.remove_column(self._dateColumn)
750 self._recentview.remove_column(self._actionColumn)
751 self._recentview.remove_column(self._fromColumn)
752 self._recentview.set_model(None)
754 def number_selected(self, action, number, message):
756 @note Actual dial function is patched in later
758 raise NotImplementedError
760 def update(self, force = False):
761 if not force and self._isPopulated:
763 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
764 backgroundPopulate.setDaemon(True)
765 backgroundPopulate.start()
768 self._isPopulated = False
769 self._recentmodel.clear()
773 return "Recent Calls"
775 def load_settings(self, config, section):
778 def save_settings(self, config, section):
780 @note Thread Agnostic
784 def _idly_populate_recentview(self):
785 self._isPopulated = True
786 self._recentmodel.clear()
789 recentItems = self._backend.get_recent()
790 except RuntimeError, e:
791 self._errorDisplay.push_exception_with_lock(e)
792 self._isPopulated = False
795 for personName, phoneNumber, date, action in recentItems:
797 personName = "Unknown"
798 description = "%s (%s)" % (phoneNumber, personName)
799 item = (phoneNumber, date, action.capitalize(), description)
800 with gtk_toolbox.gtk_lock():
801 self._recentmodel.append(item)
805 def _on_recentview_row_activated(self, treeview, path, view_column):
806 model, itr = self._recentviewselection.get_selected()
810 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
811 number = make_ugly(number)
812 contactPhoneNumbers = [("Phone", number)]
813 description = self._recentmodel.get_value(itr, self.FROM_IDX)
815 action, phoneNumber, message = self._phoneTypeSelector.run(
817 message = description,
818 parent = self._window,
820 if action == PhoneTypeSelector.ACTION_CANCEL:
824 self.number_selected(action, phoneNumber, message)
825 self._recentviewselection.unselect_all()
828 class MessagesView(object):
835 def __init__(self, widgetTree, backend, errorDisplay):
836 self._errorDisplay = errorDisplay
837 self._backend = backend
839 self._isPopulated = False
840 self._messagemodel = gtk.ListStore(
841 gobject.TYPE_STRING, # number
842 gobject.TYPE_STRING, # date
843 gobject.TYPE_STRING, # header
844 gobject.TYPE_STRING, # message
846 self._messageview = widgetTree.get_widget("messages_view")
847 self._messageviewselection = None
848 self._onMessageviewRowActivatedId = 0
850 textrenderer = gtk.CellRendererText()
851 textrenderer.set_property("yalign", 0)
852 self._dateColumn = gtk.TreeViewColumn("Date")
853 self._dateColumn.pack_start(textrenderer, expand=True)
854 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
856 textrenderer = gtk.CellRendererText()
857 textrenderer.set_property("yalign", 0)
858 self._headerColumn = gtk.TreeViewColumn("From")
859 self._headerColumn.pack_start(textrenderer, expand=True)
860 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
862 textrenderer = gtk.CellRendererText()
863 textrenderer.set_property("yalign", 0)
864 self._messageColumn = gtk.TreeViewColumn("Messages")
865 self._messageColumn.pack_start(textrenderer, expand=True)
866 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
867 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
869 self._window = gtk_toolbox.find_parent_window(self._messageview)
870 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
873 assert self._backend.is_authed()
874 self._messageview.set_model(self._messagemodel)
876 self._messageview.append_column(self._dateColumn)
877 self._messageview.append_column(self._headerColumn)
878 self._messageview.append_column(self._messageColumn)
879 self._messageviewselection = self._messageview.get_selection()
880 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
882 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
885 self._messageview.disconnect(self._onMessageviewRowActivatedId)
889 self._messageview.remove_column(self._dateColumn)
890 self._messageview.remove_column(self._headerColumn)
891 self._messageview.remove_column(self._messageColumn)
892 self._messageview.set_model(None)
894 def number_selected(self, action, number, message):
896 @note Actual dial function is patched in later
898 raise NotImplementedError
900 def update(self, force = False):
901 if not force and self._isPopulated:
903 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
904 backgroundPopulate.setDaemon(True)
905 backgroundPopulate.start()
908 self._isPopulated = False
909 self._messagemodel.clear()
915 def load_settings(self, config, section):
918 def save_settings(self, config, section):
920 @note Thread Agnostic
924 def _idly_populate_messageview(self):
925 self._isPopulated = True
926 self._messagemodel.clear()
929 messageItems = self._backend.get_messages()
930 except RuntimeError, e:
931 self._errorDisplay.push_exception_with_lock(e)
932 self._isPopulated = False
935 for header, number, relativeDate, message in messageItems:
936 number = make_ugly(number)
937 row = (number, relativeDate, header, message)
938 with gtk_toolbox.gtk_lock():
939 self._messagemodel.append(row)
943 def _on_messageview_row_activated(self, treeview, path, view_column):
944 model, itr = self._messageviewselection.get_selected()
948 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
949 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
951 action, phoneNumber, message = self._phoneTypeSelector.run(
953 message = description,
954 parent = self._window,
956 if action == PhoneTypeSelector.ACTION_CANCEL:
960 self.number_selected(action, phoneNumber, message)
961 self._messageviewselection.unselect_all()
964 class ContactsView(object):
966 def __init__(self, widgetTree, backend, errorDisplay):
967 self._errorDisplay = errorDisplay
968 self._backend = backend
970 self._addressBook = None
971 self._addressBookFactories = [DummyAddressBook()]
973 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
974 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
976 self._isPopulated = False
977 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
978 self._contactsviewselection = None
979 self._contactsview = widgetTree.get_widget("contactsview")
981 self._contactColumn = gtk.TreeViewColumn("Contact")
982 displayContactSource = False
983 if displayContactSource:
984 textrenderer = gtk.CellRendererText()
985 self._contactColumn.pack_start(textrenderer, expand=False)
986 self._contactColumn.add_attribute(textrenderer, 'text', 0)
987 textrenderer = gtk.CellRendererText()
988 self._contactColumn.pack_start(textrenderer, expand=True)
989 self._contactColumn.add_attribute(textrenderer, 'text', 1)
990 textrenderer = gtk.CellRendererText()
991 self._contactColumn.pack_start(textrenderer, expand=True)
992 self._contactColumn.add_attribute(textrenderer, 'text', 4)
993 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
994 self._contactColumn.set_sort_column_id(1)
995 self._contactColumn.set_visible(True)
997 self._onContactsviewRowActivatedId = 0
998 self._onAddressbookComboChangedId = 0
999 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1000 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1003 assert self._backend.is_authed()
1005 self._contactsview.set_model(self._contactsmodel)
1006 self._contactsview.append_column(self._contactColumn)
1007 self._contactsviewselection = self._contactsview.get_selection()
1008 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1010 self._booksList.clear()
1011 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1012 if factoryName and bookName:
1013 entryName = "%s: %s" % (factoryName, bookName)
1015 entryName = factoryName
1017 entryName = bookName
1019 entryName = "Bad name (%d)" % factoryId
1020 row = (str(factoryId), bookId, entryName)
1021 self._booksList.append(row)
1023 self._booksSelectionBox.set_model(self._booksList)
1024 cell = gtk.CellRendererText()
1025 self._booksSelectionBox.pack_start(cell, True)
1026 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1027 self._booksSelectionBox.set_active(0)
1029 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1030 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1033 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1034 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1038 self._booksSelectionBox.clear()
1039 self._booksSelectionBox.set_model(None)
1040 self._contactsview.set_model(None)
1041 self._contactsview.remove_column(self._contactColumn)
1043 def number_selected(self, action, number, message):
1045 @note Actual dial function is patched in later
1047 raise NotImplementedError
1049 def get_addressbooks(self):
1051 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1053 for i, factory in enumerate(self._addressBookFactories):
1054 for bookFactory, bookId, bookName in factory.get_addressbooks():
1055 yield (i, bookId), (factory.factory_name(), bookName)
1057 def open_addressbook(self, bookFactoryId, bookId):
1058 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1059 self.update(force=True)
1061 def update(self, force = False):
1062 if not force and self._isPopulated:
1064 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1065 backgroundPopulate.setDaemon(True)
1066 backgroundPopulate.start()
1069 self._isPopulated = False
1070 self._contactsmodel.clear()
1072 def clear_caches(self):
1073 for factory in self._addressBookFactories:
1074 factory.clear_caches()
1075 self._addressBook.clear_caches()
1077 def append(self, book):
1078 self._addressBookFactories.append(book)
1080 def extend(self, books):
1081 self._addressBookFactories.extend(books)
1087 def load_settings(self, config, section):
1090 def save_settings(self, config, section):
1092 @note Thread Agnostic
1096 def _idly_populate_contactsview(self):
1097 self._isPopulated = True
1100 # completely disable updating the treeview while we populate the data
1101 self._contactsview.freeze_child_notify()
1102 self._contactsview.set_model(None)
1104 addressBook = self._addressBook
1106 contacts = addressBook.get_contacts()
1107 except RuntimeError, e:
1109 self._isPopulated = False
1110 self._errorDisplay.push_exception_with_lock(e)
1111 for contactId, contactName in contacts:
1112 contactType = (addressBook.contact_source_short_name(contactId), )
1113 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1115 # restart the treeview data rendering
1116 self._contactsview.set_model(self._contactsmodel)
1117 self._contactsview.thaw_child_notify()
1120 def _on_addressbook_combo_changed(self, *args, **kwds):
1121 itr = self._booksSelectionBox.get_active_iter()
1124 factoryId = int(self._booksList.get_value(itr, 0))
1125 bookId = self._booksList.get_value(itr, 1)
1126 self.open_addressbook(factoryId, bookId)
1128 def _on_contactsview_row_activated(self, treeview, path, view_column):
1129 model, itr = self._contactsviewselection.get_selected()
1133 contactId = self._contactsmodel.get_value(itr, 3)
1134 contactName = self._contactsmodel.get_value(itr, 1)
1136 contactDetails = self._addressBook.get_contact_details(contactId)
1137 except RuntimeError, e:
1139 self._errorDisplay.push_exception(e)
1140 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1142 if len(contactPhoneNumbers) == 0:
1145 action, phoneNumber, message = self._phoneTypeSelector.run(
1146 contactPhoneNumbers,
1147 message = contactName,
1148 parent = self._window,
1150 if action == PhoneTypeSelector.ACTION_CANCEL:
1154 self.number_selected(action, phoneNumber, message)
1155 self._contactsviewselection.unselect_all()