4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22 from __future__ import with_statement
34 def make_ugly(prettynumber):
36 function to take a phone number and strip out all non-numeric
39 >>> make_ugly("+012-(345)-678-90")
43 uglynumber = re.sub('\D', '', prettynumber)
47 def make_pretty(phonenumber):
49 Function to take a phone number and return the pretty version
51 if phonenumber begins with 0:
53 if phonenumber begins with 1: ( for gizmo callback numbers )
55 if phonenumber is 13 digits:
57 if phonenumber is 10 digits:
61 >>> make_pretty("1234567")
63 >>> make_pretty("2345678901")
65 >>> make_pretty("12345678901")
67 >>> make_pretty("01234567890")
70 if phonenumber is None or phonenumber is "":
73 phonenumber = make_ugly(phonenumber)
75 if len(phonenumber) < 3:
78 if phonenumber[0] == "0":
80 prettynumber += "+%s" % phonenumber[0:3]
81 if 3 < len(phonenumber):
82 prettynumber += "-(%s)" % phonenumber[3:6]
83 if 6 < len(phonenumber):
84 prettynumber += "-%s" % phonenumber[6:9]
85 if 9 < len(phonenumber):
86 prettynumber += "-%s" % phonenumber[9:]
88 elif len(phonenumber) <= 7:
89 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
90 elif len(phonenumber) > 8 and phonenumber[0] == "1":
91 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
92 elif len(phonenumber) > 7:
93 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
97 class MergedAddressBook(object):
99 Merger of all addressbooks
102 def __init__(self, addressbookFactories, sorter = None):
103 self.__addressbookFactories = addressbookFactories
104 self.__addressbooks = None
105 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
107 def clear_caches(self):
108 self.__addressbooks = None
109 for factory in self.__addressbookFactories:
110 factory.clear_caches()
112 def get_addressbooks(self):
114 @returns Iterable of (Address Book Factory, Book Id, Book Name)
118 def open_addressbook(self, bookId):
121 def contact_source_short_name(self, contactId):
122 if self.__addressbooks is None:
124 bookIndex, originalId = contactId.split("-", 1)
125 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
129 return "All Contacts"
131 def get_contacts(self):
133 @returns Iterable of (contact id, contact name)
135 if self.__addressbooks is None:
136 self.__addressbooks = list(
137 factory.open_addressbook(id)
138 for factory in self.__addressbookFactories
139 for (f, id, name) in factory.get_addressbooks()
142 ("-".join([str(bookIndex), contactId]), contactName)
143 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
144 for (contactId, contactName) in addressbook.get_contacts()
146 sortedContacts = self.__sort_contacts(contacts)
147 return sortedContacts
149 def get_contact_details(self, contactId):
151 @returns Iterable of (Phone Type, Phone Number)
153 if self.__addressbooks is None:
155 bookIndex, originalId = contactId.split("-", 1)
156 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
159 def null_sorter(contacts):
161 Good for speed/low memory
166 def basic_firtname_sorter(contacts):
168 Expects names in "First Last" format
171 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
172 for (contactId, contactName) in contacts
174 contactsWithKey.sort()
175 return (contactData for (lastName, contactData) in contactsWithKey)
178 def basic_lastname_sorter(contacts):
180 Expects names in "First Last" format
183 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
184 for (contactId, contactName) in contacts
186 contactsWithKey.sort()
187 return (contactData for (lastName, contactData) in contactsWithKey)
190 def reversed_firtname_sorter(contacts):
192 Expects names in "Last, First" format
195 (contactName.split(", ", 1)[-1], (contactId, contactName))
196 for (contactId, contactName) in contacts
198 contactsWithKey.sort()
199 return (contactData for (lastName, contactData) in contactsWithKey)
202 def reversed_lastname_sorter(contacts):
204 Expects names in "Last, First" format
207 (contactName.split(", ", 1)[0], (contactId, contactName))
208 for (contactId, contactName) in contacts
210 contactsWithKey.sort()
211 return (contactData for (lastName, contactData) in contactsWithKey)
214 def guess_firstname(name):
216 return name.split(", ", 1)[-1]
218 return name.rsplit(" ", 1)[0]
221 def guess_lastname(name):
223 return name.split(", ", 1)[0]
225 return name.rsplit(" ", 1)[-1]
228 def advanced_firstname_sorter(cls, contacts):
230 (cls.guess_firstname(contactName), (contactId, contactName))
231 for (contactId, contactName) in contacts
233 contactsWithKey.sort()
234 return (contactData for (lastName, contactData) in contactsWithKey)
237 def advanced_lastname_sorter(cls, contacts):
239 (cls.guess_lastname(contactName), (contactId, contactName))
240 for (contactId, contactName) in contacts
242 contactsWithKey.sort()
243 return (contactData for (lastName, contactData) in contactsWithKey)
246 class PhoneTypeSelector(object):
248 ACTION_CANCEL = "cancel"
249 ACTION_SELECT = "select"
251 ACTION_SEND_SMS = "sms"
253 def __init__(self, widgetTree, gcBackend):
254 self._gcBackend = gcBackend
255 self._widgetTree = widgetTree
257 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
258 self._smsDialog = SmsEntryDialog(self._widgetTree, self._gcBackend)
260 self._smsButton = self._widgetTree.get_widget("sms_button")
261 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
263 self._dialButton = self._widgetTree.get_widget("dial_button")
264 self._dialButton.connect("clicked", self._on_phonetype_dial)
266 self._selectButton = self._widgetTree.get_widget("select_button")
267 self._selectButton.connect("clicked", self._on_phonetype_select)
269 self._cancelButton = self._widgetTree.get_widget("cancel_button")
270 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
272 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
273 self._typeviewselection = None
275 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
276 self._typeview = self._widgetTree.get_widget("phonetypes")
277 self._typeview.connect("row-activated", self._on_phonetype_select)
279 self._action = self.ACTION_CANCEL
281 def run(self, contactDetails, message = "", parent = None):
282 self._action = self.ACTION_CANCEL
283 self._typemodel.clear()
284 self._typeview.set_model(self._typemodel)
286 # Add the column to the treeview
287 textrenderer = gtk.CellRendererText()
288 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
289 self._typeview.append_column(numberColumn)
291 textrenderer = gtk.CellRendererText()
292 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
293 self._typeview.append_column(typeColumn)
295 self._typeviewselection = self._typeview.get_selection()
296 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
298 for phoneType, phoneNumber in contactDetails:
299 display = " - ".join((phoneNumber, phoneType))
301 row = (phoneNumber, display)
302 self._typemodel.append(row)
304 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
306 self._message.set_markup(message)
309 self._message.set_markup("")
312 if parent is not None:
313 self._dialog.set_transient_for(parent)
316 userResponse = self._dialog.run()
320 if userResponse == gtk.RESPONSE_OK:
321 phoneNumber = self._get_number()
322 phoneNumber = make_ugly(phoneNumber)
326 self._action = self.ACTION_CANCEL
328 if self._action == self.ACTION_SEND_SMS:
329 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
332 self._action = self.ACTION_CANCEL
336 self._typeviewselection.unselect_all()
337 self._typeview.remove_column(numberColumn)
338 self._typeview.remove_column(typeColumn)
339 self._typeview.set_model(None)
341 return self._action, phoneNumber, smsMessage
343 def _get_number(self):
344 model, itr = self._typeviewselection.get_selected()
348 phoneNumber = self._typemodel.get_value(itr, 0)
351 def _on_phonetype_dial(self, *args):
352 self._dialog.response(gtk.RESPONSE_OK)
353 self._action = self.ACTION_DIAL
355 def _on_phonetype_send_sms(self, *args):
356 self._dialog.response(gtk.RESPONSE_OK)
357 self._action = self.ACTION_SEND_SMS
359 def _on_phonetype_select(self, *args):
360 self._dialog.response(gtk.RESPONSE_OK)
361 self._action = self.ACTION_SELECT
363 def _on_phonetype_cancel(self, *args):
364 self._dialog.response(gtk.RESPONSE_CANCEL)
365 self._action = self.ACTION_CANCEL
368 class SmsEntryDialog(object):
372 def __init__(self, widgetTree, gcBackend):
373 self._gcBackend = gcBackend
374 self._widgetTree = widgetTree
375 self._dialog = self._widgetTree.get_widget("smsDialog")
377 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
378 self._smsButton.connect("clicked", self._on_send)
380 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
381 self._cancelButton.connect("clicked", self._on_cancel)
383 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
384 self._message = self._widgetTree.get_widget("smsMessage")
385 self._smsEntry = self._widgetTree.get_widget("smsEntry")
386 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
388 def run(self, number, message = "", parent = None):
390 self._message.set_markup(message)
393 self._message.set_markup("")
395 self._smsEntry.get_buffer().set_text("")
396 self._update_letter_count()
398 if parent is not None:
399 self._dialog.set_transient_for(parent)
402 userResponse = self._dialog.run()
406 if userResponse == gtk.RESPONSE_OK:
407 entryBuffer = self._smsEntry.get_buffer()
408 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
409 enteredMessage = enteredMessage[0:self.MAX_CHAR]
413 return enteredMessage
415 def _update_letter_count(self, *args):
416 entryLength = self._smsEntry.get_buffer().get_char_count()
417 charsLeft = self.MAX_CHAR - entryLength
418 self._letterCountLabel.set_text(str(charsLeft))
420 self._smsButton.set_sensitive(False)
422 self._smsButton.set_sensitive(True)
424 def _on_entry_changed(self, *args):
425 self._update_letter_count()
427 def _on_send(self, *args):
428 self._dialog.response(gtk.RESPONSE_OK)
430 def _on_cancel(self, *args):
431 self._dialog.response(gtk.RESPONSE_CANCEL)
434 class Dialpad(object):
436 def __init__(self, widgetTree, errorDisplay):
437 self._errorDisplay = errorDisplay
438 self._numberdisplay = widgetTree.get_widget("numberdisplay")
439 self._dialButton = widgetTree.get_widget("dial")
440 self._phonenumber = ""
441 self._prettynumber = ""
442 self._clearall_id = None
445 "on_dial_clicked": self._on_dial_clicked,
446 "on_digit_clicked": self._on_digit_clicked,
447 "on_clear_number": self._on_clear_number,
448 "on_back_clicked": self._on_backspace,
449 "on_back_pressed": self._on_back_pressed,
450 "on_back_released": self._on_back_released,
452 widgetTree.signal_autoconnect(callbackMapping)
455 self._dialButton.grab_focus()
460 def number_selected(self, action, number, message):
462 @note Actual dial function is patched in later
464 raise NotImplementedError
466 def get_number(self):
467 return self._phonenumber
469 def set_number(self, number):
471 Set the callback phonenumber
474 self._phonenumber = make_ugly(number)
475 self._prettynumber = make_pretty(self._phonenumber)
476 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
478 self._errorDisplay.push_exception(e)
487 def load_settings(self, config, section):
490 def save_settings(self, config, section):
492 @note Thread Agnostic
496 def _on_dial_clicked(self, widget):
497 action = PhoneTypeSelector.ACTION_DIAL
498 phoneNumber = self.get_number()
500 self.number_selected(action, phoneNumber, message)
502 def _on_clear_number(self, *args):
505 def _on_digit_clicked(self, widget):
506 self.set_number(self._phonenumber + widget.get_name()[-1])
508 def _on_backspace(self, widget):
509 self.set_number(self._phonenumber[:-1])
511 def _on_clearall(self):
515 def _on_back_pressed(self, widget):
516 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
518 def _on_back_released(self, widget):
519 if self._clearall_id is not None:
520 gobject.source_remove(self._clearall_id)
521 self._clearall_id = None
524 class AccountInfo(object):
526 def __init__(self, widgetTree, backend, errorDisplay):
527 self._errorDisplay = errorDisplay
528 self._backend = backend
529 self._isPopulated = False
531 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
532 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
533 self._callbackCombo = widgetTree.get_widget("callbackcombo")
534 self._onCallbackentryChangedId = 0
536 self._defaultCallback = ""
539 assert self._backend.is_authed()
540 self._accountViewNumberDisplay.set_use_markup(True)
541 self.set_account_number("")
542 self._callbackList.clear()
543 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
544 self.update(force=True)
547 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
551 self._callbackList.clear()
553 def get_selected_callback_number(self):
554 return make_ugly(self._callbackCombo.get_child().get_text())
556 def set_account_number(self, number):
558 Displays current account number
560 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
562 def update(self, force = False):
563 if not force and self._isPopulated:
565 self._populate_callback_combo()
566 self.set_account_number(self._backend.get_account_number())
569 self._callbackCombo.get_child().set_text("")
570 self.set_account_number("")
571 self._isPopulated = False
575 return "Account Info"
577 def load_settings(self, config, section):
578 self._defaultCallback = config.get(section, "callback")
580 def save_settings(self, config, section):
582 @note Thread Agnostic
584 callback = self.get_selected_callback_number()
585 config.set(section, "callback", callback)
587 def _populate_callback_combo(self):
588 self._isPopulated = True
589 self._callbackList.clear()
591 callbackNumbers = self._backend.get_callback_numbers()
592 except RuntimeError, e:
593 self._errorDisplay.push_exception(e)
594 self._isPopulated = False
597 for number, description in callbackNumbers.iteritems():
598 self._callbackList.append((make_pretty(number),))
600 self._callbackCombo.set_model(self._callbackList)
601 self._callbackCombo.set_text_column(0)
602 #callbackNumber = self._backend.get_callback_number()
603 callbackNumber = self._defaultCallback
604 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
606 def _set_callback_number(self, number):
608 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
611 if not self._backend.is_valid_syntax(number):
612 self._errorDisplay.push_message("%s is not a valid callback number" % number)
613 elif number == self._backend.get_callback_number():
614 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
616 self._backend.set_callback_number(number)
617 warnings.warn("Callback number set to %s" % self._backend.get_callback_number(), UserWarning, 2)
618 except RuntimeError, e:
619 self._errorDisplay.push_exception(e)
621 def _on_callbackentry_changed(self, *args):
623 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
625 text = self.get_selected_callback_number()
626 self._set_callback_number(text)
629 class RecentCallsView(object):
636 def __init__(self, widgetTree, backend, errorDisplay):
637 self._errorDisplay = errorDisplay
638 self._backend = backend
640 self._isPopulated = False
641 self._recentmodel = gtk.ListStore(
642 gobject.TYPE_STRING, # number
643 gobject.TYPE_STRING, # date
644 gobject.TYPE_STRING, # action
645 gobject.TYPE_STRING, # from
647 self._recentview = widgetTree.get_widget("recentview")
648 self._recentviewselection = None
649 self._onRecentviewRowActivatedId = 0
651 textrenderer = gtk.CellRendererText()
652 textrenderer.set_property("yalign", 0)
653 self._dateColumn = gtk.TreeViewColumn("Date")
654 self._dateColumn.pack_start(textrenderer, expand=True)
655 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
657 textrenderer = gtk.CellRendererText()
658 textrenderer.set_property("yalign", 0)
659 self._actionColumn = gtk.TreeViewColumn("Action")
660 self._actionColumn.pack_start(textrenderer, expand=True)
661 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
663 textrenderer = gtk.CellRendererText()
664 textrenderer.set_property("yalign", 0)
665 self._fromColumn = gtk.TreeViewColumn("From")
666 self._fromColumn.pack_start(textrenderer, expand=True)
667 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
668 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
670 self._window = gtk_toolbox.find_parent_window(self._recentview)
671 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
674 assert self._backend.is_authed()
675 self._recentview.set_model(self._recentmodel)
677 self._recentview.append_column(self._dateColumn)
678 self._recentview.append_column(self._actionColumn)
679 self._recentview.append_column(self._fromColumn)
680 self._recentviewselection = self._recentview.get_selection()
681 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
683 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
686 self._recentview.disconnect(self._onRecentviewRowActivatedId)
690 self._recentview.remove_column(self._dateColumn)
691 self._recentview.remove_column(self._actionColumn)
692 self._recentview.remove_column(self._fromColumn)
693 self._recentview.set_model(None)
695 def number_selected(self, action, number, message):
697 @note Actual dial function is patched in later
699 raise NotImplementedError
701 def update(self, force = False):
702 if not force and self._isPopulated:
704 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
705 backgroundPopulate.setDaemon(True)
706 backgroundPopulate.start()
709 self._isPopulated = False
710 self._recentmodel.clear()
714 return "Recent Calls"
716 def load_settings(self, config, section):
719 def save_settings(self, config, section):
721 @note Thread Agnostic
725 def _idly_populate_recentview(self):
726 self._isPopulated = True
727 self._recentmodel.clear()
730 recentItems = self._backend.get_recent()
731 except RuntimeError, e:
732 self._errorDisplay.push_exception_with_lock(e)
733 self._isPopulated = False
736 for personName, phoneNumber, date, action in recentItems:
738 personName = "Unknown"
739 description = "%s (%s)" % (phoneNumber, personName)
740 item = (phoneNumber, date, action.capitalize(), description)
741 with gtk_toolbox.gtk_lock():
742 self._recentmodel.append(item)
746 def _on_recentview_row_activated(self, treeview, path, view_column):
747 model, itr = self._recentviewselection.get_selected()
751 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
752 number = make_ugly(number)
753 contactPhoneNumbers = [("Phone", number)]
754 description = self._recentmodel.get_value(itr, self.FROM_IDX)
756 action, phoneNumber, message = self._phoneTypeSelector.run(
758 message = description,
759 parent = self._window,
761 if action == PhoneTypeSelector.ACTION_CANCEL:
765 self.number_selected(action, phoneNumber, message)
766 self._recentviewselection.unselect_all()
769 class MessagesView(object):
776 def __init__(self, widgetTree, backend, errorDisplay):
777 self._errorDisplay = errorDisplay
778 self._backend = backend
780 self._isPopulated = False
781 self._messagemodel = gtk.ListStore(
782 gobject.TYPE_STRING, # number
783 gobject.TYPE_STRING, # date
784 gobject.TYPE_STRING, # header
785 gobject.TYPE_STRING, # message
787 self._messageview = widgetTree.get_widget("messages_view")
788 self._messageviewselection = None
789 self._onMessageviewRowActivatedId = 0
791 textrenderer = gtk.CellRendererText()
792 textrenderer.set_property("yalign", 0)
793 self._dateColumn = gtk.TreeViewColumn("Date")
794 self._dateColumn.pack_start(textrenderer, expand=True)
795 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
797 textrenderer = gtk.CellRendererText()
798 textrenderer.set_property("yalign", 0)
799 self._headerColumn = gtk.TreeViewColumn("From")
800 self._headerColumn.pack_start(textrenderer, expand=True)
801 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
803 textrenderer = gtk.CellRendererText()
804 textrenderer.set_property("yalign", 0)
805 self._messageColumn = gtk.TreeViewColumn("Messages")
806 self._messageColumn.pack_start(textrenderer, expand=True)
807 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
808 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
810 self._window = gtk_toolbox.find_parent_window(self._messageview)
811 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
814 assert self._backend.is_authed()
815 self._messageview.set_model(self._messagemodel)
817 self._messageview.append_column(self._dateColumn)
818 self._messageview.append_column(self._headerColumn)
819 self._messageview.append_column(self._messageColumn)
820 self._messageviewselection = self._messageview.get_selection()
821 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
823 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
826 self._messageview.disconnect(self._onMessageviewRowActivatedId)
830 self._messageview.remove_column(self._dateColumn)
831 self._messageview.remove_column(self._headerColumn)
832 self._messageview.remove_column(self._messageColumn)
833 self._messageview.set_model(None)
835 def number_selected(self, action, number, message):
837 @note Actual dial function is patched in later
839 raise NotImplementedError
841 def update(self, force = False):
842 if not force and self._isPopulated:
844 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
845 backgroundPopulate.setDaemon(True)
846 backgroundPopulate.start()
849 self._isPopulated = False
850 self._messagemodel.clear()
856 def load_settings(self, config, section):
859 def save_settings(self, config, section):
861 @note Thread Agnostic
865 def _idly_populate_messageview(self):
866 self._isPopulated = True
867 self._messagemodel.clear()
870 messageItems = self._backend.get_messages()
871 except RuntimeError, e:
872 self._errorDisplay.push_exception_with_lock(e)
873 self._isPopulated = False
876 for header, number, relativeDate, message in messageItems:
877 number = make_ugly(number)
878 row = (number, relativeDate, header, message)
879 with gtk_toolbox.gtk_lock():
880 self._messagemodel.append(row)
884 def _on_messageview_row_activated(self, treeview, path, view_column):
885 model, itr = self._messageviewselection.get_selected()
889 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
890 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
892 action, phoneNumber, message = self._phoneTypeSelector.run(
894 message = description,
895 parent = self._window,
897 if action == PhoneTypeSelector.ACTION_CANCEL:
901 self.number_selected(action, phoneNumber, message)
902 self._messageviewselection.unselect_all()
905 class ContactsView(object):
907 def __init__(self, widgetTree, backend, errorDisplay):
908 self._errorDisplay = errorDisplay
909 self._backend = backend
911 self._addressBook = None
912 self._addressBookFactories = [null_backend.NullAddressBook()]
914 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
915 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
917 self._isPopulated = False
918 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
919 self._contactsviewselection = None
920 self._contactsview = widgetTree.get_widget("contactsview")
922 self._contactColumn = gtk.TreeViewColumn("Contact")
923 displayContactSource = False
924 if displayContactSource:
925 textrenderer = gtk.CellRendererText()
926 self._contactColumn.pack_start(textrenderer, expand=False)
927 self._contactColumn.add_attribute(textrenderer, 'text', 0)
928 textrenderer = gtk.CellRendererText()
929 self._contactColumn.pack_start(textrenderer, expand=True)
930 self._contactColumn.add_attribute(textrenderer, 'text', 1)
931 textrenderer = gtk.CellRendererText()
932 self._contactColumn.pack_start(textrenderer, expand=True)
933 self._contactColumn.add_attribute(textrenderer, 'text', 4)
934 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
935 self._contactColumn.set_sort_column_id(1)
936 self._contactColumn.set_visible(True)
938 self._onContactsviewRowActivatedId = 0
939 self._onAddressbookComboChangedId = 0
940 self._window = gtk_toolbox.find_parent_window(self._contactsview)
941 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
944 assert self._backend.is_authed()
946 self._contactsview.set_model(self._contactsmodel)
947 self._contactsview.append_column(self._contactColumn)
948 self._contactsviewselection = self._contactsview.get_selection()
949 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
951 self._booksList.clear()
952 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
953 if factoryName and bookName:
954 entryName = "%s: %s" % (factoryName, bookName)
956 entryName = factoryName
960 entryName = "Bad name (%d)" % factoryId
961 row = (str(factoryId), bookId, entryName)
962 self._booksList.append(row)
964 self._booksSelectionBox.set_model(self._booksList)
965 cell = gtk.CellRendererText()
966 self._booksSelectionBox.pack_start(cell, True)
967 self._booksSelectionBox.add_attribute(cell, 'text', 2)
968 self._booksSelectionBox.set_active(0)
970 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
971 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
974 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
975 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
979 self._booksSelectionBox.clear()
980 self._booksSelectionBox.set_model(None)
981 self._contactsview.set_model(None)
982 self._contactsview.remove_column(self._contactColumn)
984 def number_selected(self, action, number, message):
986 @note Actual dial function is patched in later
988 raise NotImplementedError
990 def get_addressbooks(self):
992 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
994 for i, factory in enumerate(self._addressBookFactories):
995 for bookFactory, bookId, bookName in factory.get_addressbooks():
996 yield (i, bookId), (factory.factory_name(), bookName)
998 def open_addressbook(self, bookFactoryId, bookId):
999 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1000 self.update(force=True)
1002 def update(self, force = False):
1003 if not force and self._isPopulated:
1005 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1006 backgroundPopulate.setDaemon(True)
1007 backgroundPopulate.start()
1010 self._isPopulated = False
1011 self._contactsmodel.clear()
1013 def clear_caches(self):
1014 for factory in self._addressBookFactories:
1015 factory.clear_caches()
1016 self._addressBook.clear_caches()
1018 def append(self, book):
1019 self._addressBookFactories.append(book)
1021 def extend(self, books):
1022 self._addressBookFactories.extend(books)
1028 def load_settings(self, config, section):
1031 def save_settings(self, config, section):
1033 @note Thread Agnostic
1037 def _idly_populate_contactsview(self):
1038 self._isPopulated = True
1041 # completely disable updating the treeview while we populate the data
1042 self._contactsview.freeze_child_notify()
1043 self._contactsview.set_model(None)
1045 addressBook = self._addressBook
1047 contacts = addressBook.get_contacts()
1048 except RuntimeError, e:
1050 self._isPopulated = False
1051 self._errorDisplay.push_exception_with_lock(e)
1052 for contactId, contactName in contacts:
1053 contactType = (addressBook.contact_source_short_name(contactId), )
1054 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1056 # restart the treeview data rendering
1057 self._contactsview.set_model(self._contactsmodel)
1058 self._contactsview.thaw_child_notify()
1061 def _on_addressbook_combo_changed(self, *args, **kwds):
1062 itr = self._booksSelectionBox.get_active_iter()
1065 factoryId = int(self._booksList.get_value(itr, 0))
1066 bookId = self._booksList.get_value(itr, 1)
1067 self.open_addressbook(factoryId, bookId)
1069 def _on_contactsview_row_activated(self, treeview, path, view_column):
1070 model, itr = self._contactsviewselection.get_selected()
1074 contactId = self._contactsmodel.get_value(itr, 3)
1075 contactName = self._contactsmodel.get_value(itr, 1)
1077 contactDetails = self._addressBook.get_contact_details(contactId)
1078 except RuntimeError, e:
1080 self._errorDisplay.push_exception(e)
1081 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1083 if len(contactPhoneNumbers) == 0:
1086 action, phoneNumber, message = self._phoneTypeSelector.run(
1087 contactPhoneNumbers,
1088 message = contactName,
1089 parent = self._window,
1091 if action == PhoneTypeSelector.ACTION_CANCEL:
1095 self.number_selected(action, phoneNumber, message)
1096 self._contactsviewselection.unselect_all()