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 number_selected(self, action, number, message):
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 action = PhoneTypeSelector.ACTION_DIAL
560 phoneNumber = self.get_number()
562 self.number_selected(action, phoneNumber, message)
564 def _on_clear_number(self, *args):
567 def _on_digit_clicked(self, widget):
568 self.set_number(self._phonenumber + widget.get_name()[-1])
570 def _on_backspace(self, widget):
571 self.set_number(self._phonenumber[:-1])
573 def _on_clearall(self):
577 def _on_back_pressed(self, widget):
578 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
580 def _on_back_released(self, widget):
581 if self._clearall_id is not None:
582 gobject.source_remove(self._clearall_id)
583 self._clearall_id = None
586 class AccountInfo(object):
588 def __init__(self, widgetTree, backend, errorDisplay):
589 self._errorDisplay = errorDisplay
590 self._backend = backend
591 self._isPopulated = False
593 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
594 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
595 self._callbackCombo = widgetTree.get_widget("callbackcombo")
596 self._onCallbackentryChangedId = 0
598 self._defaultCallback = ""
601 assert self._backend.is_authed()
602 self._accountViewNumberDisplay.set_use_markup(True)
603 self.set_account_number("")
604 self._callbackList.clear()
605 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
606 self.update(force=True)
609 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
613 self._callbackList.clear()
615 def get_selected_callback_number(self):
616 return make_ugly(self._callbackCombo.get_child().get_text())
618 def set_account_number(self, number):
620 Displays current account number
622 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
624 def update(self, force = False):
625 if not force and self._isPopulated:
627 self._populate_callback_combo()
628 self.set_account_number(self._backend.get_account_number())
631 self._callbackCombo.get_child().set_text("")
632 self.set_account_number("")
633 self._isPopulated = False
637 return "Account Info"
639 def load_settings(self, config, section):
640 self._defaultCallback = config.get(section, "callback")
642 def save_settings(self, config, section):
644 @note Thread Agnostic
646 callback = self.get_selected_callback_number()
647 config.set(section, "callback", callback)
649 def _populate_callback_combo(self):
650 self._isPopulated = True
651 self._callbackList.clear()
653 callbackNumbers = self._backend.get_callback_numbers()
654 except RuntimeError, e:
655 self._errorDisplay.push_exception(e)
656 self._isPopulated = False
659 for number, description in callbackNumbers.iteritems():
660 self._callbackList.append((make_pretty(number),))
662 self._callbackCombo.set_model(self._callbackList)
663 self._callbackCombo.set_text_column(0)
664 #callbackNumber = self._backend.get_callback_number()
665 callbackNumber = self._defaultCallback
666 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
668 def _set_callback_number(self, number):
670 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
673 if not self._backend.is_valid_syntax(number):
674 self._errorDisplay.push_message("%s is not a valid callback number" % number)
675 elif number == self._backend.get_callback_number():
676 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
678 self._backend.set_callback_number(number)
679 warnings.warn("Callback number set to %s" % self._backend.get_callback_number(), UserWarning, 2)
680 except RuntimeError, e:
681 self._errorDisplay.push_exception(e)
683 def _on_callbackentry_changed(self, *args):
685 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
687 text = self.get_selected_callback_number()
688 self._set_callback_number(text)
691 class RecentCallsView(object):
698 def __init__(self, widgetTree, backend, errorDisplay):
699 self._errorDisplay = errorDisplay
700 self._backend = backend
702 self._isPopulated = False
703 self._recentmodel = gtk.ListStore(
704 gobject.TYPE_STRING, # number
705 gobject.TYPE_STRING, # date
706 gobject.TYPE_STRING, # action
707 gobject.TYPE_STRING, # from
709 self._recentview = widgetTree.get_widget("recentview")
710 self._recentviewselection = None
711 self._onRecentviewRowActivatedId = 0
713 textrenderer = gtk.CellRendererText()
714 textrenderer.set_property("yalign", 0)
715 self._dateColumn = gtk.TreeViewColumn("Date")
716 self._dateColumn.pack_start(textrenderer, expand=True)
717 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
719 textrenderer = gtk.CellRendererText()
720 textrenderer.set_property("yalign", 0)
721 self._actionColumn = gtk.TreeViewColumn("Action")
722 self._actionColumn.pack_start(textrenderer, expand=True)
723 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
725 textrenderer = gtk.CellRendererText()
726 textrenderer.set_property("yalign", 0)
727 self._fromColumn = gtk.TreeViewColumn("From")
728 self._fromColumn.pack_start(textrenderer, expand=True)
729 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
730 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
732 self._window = gtk_toolbox.find_parent_window(self._recentview)
733 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
736 assert self._backend.is_authed()
737 self._recentview.set_model(self._recentmodel)
739 self._recentview.append_column(self._dateColumn)
740 self._recentview.append_column(self._actionColumn)
741 self._recentview.append_column(self._fromColumn)
742 self._recentviewselection = self._recentview.get_selection()
743 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
745 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
748 self._recentview.disconnect(self._onRecentviewRowActivatedId)
752 self._recentview.remove_column(self._dateColumn)
753 self._recentview.remove_column(self._actionColumn)
754 self._recentview.remove_column(self._fromColumn)
755 self._recentview.set_model(None)
757 def number_selected(self, action, number, message):
759 @note Actual dial function is patched in later
761 raise NotImplementedError
763 def update(self, force = False):
764 if not force and self._isPopulated:
766 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
767 backgroundPopulate.setDaemon(True)
768 backgroundPopulate.start()
771 self._isPopulated = False
772 self._recentmodel.clear()
776 return "Recent Calls"
778 def load_settings(self, config, section):
781 def save_settings(self, config, section):
783 @note Thread Agnostic
787 def _idly_populate_recentview(self):
788 self._isPopulated = True
789 self._recentmodel.clear()
792 recentItems = self._backend.get_recent()
793 except RuntimeError, e:
794 self._errorDisplay.push_exception_with_lock(e)
795 self._isPopulated = False
798 for personName, phoneNumber, date, action in recentItems:
800 personName = "Unknown"
801 description = "%s (%s)" % (phoneNumber, personName)
802 item = (phoneNumber, date, action.capitalize(), description)
803 with gtk_toolbox.gtk_lock():
804 self._recentmodel.append(item)
808 def _on_recentview_row_activated(self, treeview, path, view_column):
809 model, itr = self._recentviewselection.get_selected()
813 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
814 number = make_ugly(number)
815 contactPhoneNumbers = [("Phone", number)]
816 description = self._recentmodel.get_value(itr, self.FROM_IDX)
818 action, phoneNumber, message = self._phoneTypeSelector.run(
820 message = description,
821 parent = self._window,
823 if action == PhoneTypeSelector.ACTION_CANCEL:
827 self.number_selected(action, phoneNumber, message)
828 self._recentviewselection.unselect_all()
831 class MessagesView(object):
838 def __init__(self, widgetTree, backend, errorDisplay):
839 self._errorDisplay = errorDisplay
840 self._backend = backend
842 self._isPopulated = False
843 self._messagemodel = gtk.ListStore(
844 gobject.TYPE_STRING, # number
845 gobject.TYPE_STRING, # date
846 gobject.TYPE_STRING, # header
847 gobject.TYPE_STRING, # message
849 self._messageview = widgetTree.get_widget("messages_view")
850 self._messageviewselection = None
851 self._onMessageviewRowActivatedId = 0
853 textrenderer = gtk.CellRendererText()
854 textrenderer.set_property("yalign", 0)
855 self._dateColumn = gtk.TreeViewColumn("Date")
856 self._dateColumn.pack_start(textrenderer, expand=True)
857 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
859 textrenderer = gtk.CellRendererText()
860 textrenderer.set_property("yalign", 0)
861 self._headerColumn = gtk.TreeViewColumn("From")
862 self._headerColumn.pack_start(textrenderer, expand=True)
863 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
865 textrenderer = gtk.CellRendererText()
866 textrenderer.set_property("yalign", 0)
867 self._messageColumn = gtk.TreeViewColumn("Messages")
868 self._messageColumn.pack_start(textrenderer, expand=True)
869 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
870 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
872 self._window = gtk_toolbox.find_parent_window(self._messageview)
873 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
876 assert self._backend.is_authed()
877 self._messageview.set_model(self._messagemodel)
879 self._messageview.append_column(self._dateColumn)
880 self._messageview.append_column(self._headerColumn)
881 self._messageview.append_column(self._messageColumn)
882 self._messageviewselection = self._messageview.get_selection()
883 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
885 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
888 self._messageview.disconnect(self._onMessageviewRowActivatedId)
892 self._messageview.remove_column(self._dateColumn)
893 self._messageview.remove_column(self._headerColumn)
894 self._messageview.remove_column(self._messageColumn)
895 self._messageview.set_model(None)
897 def number_selected(self, action, number, message):
899 @note Actual dial function is patched in later
901 raise NotImplementedError
903 def update(self, force = False):
904 if not force and self._isPopulated:
906 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
907 backgroundPopulate.setDaemon(True)
908 backgroundPopulate.start()
911 self._isPopulated = False
912 self._messagemodel.clear()
918 def load_settings(self, config, section):
921 def save_settings(self, config, section):
923 @note Thread Agnostic
927 def _idly_populate_messageview(self):
928 self._isPopulated = True
929 self._messagemodel.clear()
932 messageItems = self._backend.get_messages()
933 except RuntimeError, e:
934 self._errorDisplay.push_exception_with_lock(e)
935 self._isPopulated = False
938 for header, number, relativeDate, message in messageItems:
939 number = make_ugly(number)
940 row = (number, relativeDate, header, message)
941 with gtk_toolbox.gtk_lock():
942 self._messagemodel.append(row)
946 def _on_messageview_row_activated(self, treeview, path, view_column):
947 model, itr = self._messageviewselection.get_selected()
951 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
952 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
954 action, phoneNumber, message = self._phoneTypeSelector.run(
956 message = description,
957 parent = self._window,
959 if action == PhoneTypeSelector.ACTION_CANCEL:
963 self.number_selected(action, phoneNumber, message)
964 self._messageviewselection.unselect_all()
967 class ContactsView(object):
969 def __init__(self, widgetTree, backend, errorDisplay):
970 self._errorDisplay = errorDisplay
971 self._backend = backend
973 self._addressBook = None
974 self._addressBookFactories = [DummyAddressBook()]
976 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
977 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
979 self._isPopulated = False
980 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
981 self._contactsviewselection = None
982 self._contactsview = widgetTree.get_widget("contactsview")
984 self._contactColumn = gtk.TreeViewColumn("Contact")
985 displayContactSource = False
986 if displayContactSource:
987 textrenderer = gtk.CellRendererText()
988 self._contactColumn.pack_start(textrenderer, expand=False)
989 self._contactColumn.add_attribute(textrenderer, 'text', 0)
990 textrenderer = gtk.CellRendererText()
991 self._contactColumn.pack_start(textrenderer, expand=True)
992 self._contactColumn.add_attribute(textrenderer, 'text', 1)
993 textrenderer = gtk.CellRendererText()
994 self._contactColumn.pack_start(textrenderer, expand=True)
995 self._contactColumn.add_attribute(textrenderer, 'text', 4)
996 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
997 self._contactColumn.set_sort_column_id(1)
998 self._contactColumn.set_visible(True)
1000 self._onContactsviewRowActivatedId = 0
1001 self._onAddressbookComboChangedId = 0
1002 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1003 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1006 assert self._backend.is_authed()
1008 self._contactsview.set_model(self._contactsmodel)
1009 self._contactsview.append_column(self._contactColumn)
1010 self._contactsviewselection = self._contactsview.get_selection()
1011 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1013 self._booksList.clear()
1014 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1015 if factoryName and bookName:
1016 entryName = "%s: %s" % (factoryName, bookName)
1018 entryName = factoryName
1020 entryName = bookName
1022 entryName = "Bad name (%d)" % factoryId
1023 row = (str(factoryId), bookId, entryName)
1024 self._booksList.append(row)
1026 self._booksSelectionBox.set_model(self._booksList)
1027 cell = gtk.CellRendererText()
1028 self._booksSelectionBox.pack_start(cell, True)
1029 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1030 self._booksSelectionBox.set_active(0)
1032 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1033 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1036 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1037 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1041 self._booksSelectionBox.clear()
1042 self._booksSelectionBox.set_model(None)
1043 self._contactsview.set_model(None)
1044 self._contactsview.remove_column(self._contactColumn)
1046 def number_selected(self, action, number, message):
1048 @note Actual dial function is patched in later
1050 raise NotImplementedError
1052 def get_addressbooks(self):
1054 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1056 for i, factory in enumerate(self._addressBookFactories):
1057 for bookFactory, bookId, bookName in factory.get_addressbooks():
1058 yield (i, bookId), (factory.factory_name(), bookName)
1060 def open_addressbook(self, bookFactoryId, bookId):
1061 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1062 self.update(force=True)
1064 def update(self, force = False):
1065 if not force and self._isPopulated:
1067 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1068 backgroundPopulate.setDaemon(True)
1069 backgroundPopulate.start()
1072 self._isPopulated = False
1073 self._contactsmodel.clear()
1075 def clear_caches(self):
1076 for factory in self._addressBookFactories:
1077 factory.clear_caches()
1078 self._addressBook.clear_caches()
1080 def append(self, book):
1081 self._addressBookFactories.append(book)
1083 def extend(self, books):
1084 self._addressBookFactories.extend(books)
1090 def load_settings(self, config, section):
1093 def save_settings(self, config, section):
1095 @note Thread Agnostic
1099 def _idly_populate_contactsview(self):
1100 self._isPopulated = True
1103 # completely disable updating the treeview while we populate the data
1104 self._contactsview.freeze_child_notify()
1105 self._contactsview.set_model(None)
1107 addressBook = self._addressBook
1109 contacts = addressBook.get_contacts()
1110 except RuntimeError, e:
1112 self._isPopulated = False
1113 self._errorDisplay.push_exception_with_lock(e)
1114 for contactId, contactName in contacts:
1115 contactType = (addressBook.contact_source_short_name(contactId), )
1116 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1118 # restart the treeview data rendering
1119 self._contactsview.set_model(self._contactsmodel)
1120 self._contactsview.thaw_child_notify()
1123 def _on_addressbook_combo_changed(self, *args, **kwds):
1124 itr = self._booksSelectionBox.get_active_iter()
1127 factoryId = int(self._booksList.get_value(itr, 0))
1128 bookId = self._booksList.get_value(itr, 1)
1129 self.open_addressbook(factoryId, bookId)
1131 def _on_contactsview_row_activated(self, treeview, path, view_column):
1132 model, itr = self._contactsviewselection.get_selected()
1136 contactId = self._contactsmodel.get_value(itr, 3)
1137 contactName = self._contactsmodel.get_value(itr, 1)
1139 contactDetails = self._addressBook.get_contact_details(contactId)
1140 except RuntimeError, e:
1142 self._errorDisplay.push_exception(e)
1143 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1145 if len(contactPhoneNumbers) == 0:
1148 action, phoneNumber, message = self._phoneTypeSelector.run(
1149 contactPhoneNumbers,
1150 message = contactName,
1151 parent = self._window,
1153 if action == PhoneTypeSelector.ACTION_CANCEL:
1157 self.number_selected(action, phoneNumber, message)
1158 self._contactsviewselection.unselect_all()