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)
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):
371 @todo Add multi-SMS messages like GoogleVoice
376 def __init__(self, widgetTree):
377 self._widgetTree = widgetTree
378 self._dialog = self._widgetTree.get_widget("smsDialog")
380 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
381 self._smsButton.connect("clicked", self._on_send)
383 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
384 self._cancelButton.connect("clicked", self._on_cancel)
386 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
387 self._message = self._widgetTree.get_widget("smsMessage")
388 self._smsEntry = self._widgetTree.get_widget("smsEntry")
389 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
391 def run(self, number, message = "", parent = None):
393 self._message.set_markup(message)
396 self._message.set_markup("")
398 self._smsEntry.get_buffer().set_text("")
399 self._update_letter_count()
401 if parent is not None:
402 self._dialog.set_transient_for(parent)
405 userResponse = self._dialog.run()
409 if userResponse == gtk.RESPONSE_OK:
410 entryBuffer = self._smsEntry.get_buffer()
411 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
412 enteredMessage = enteredMessage[0:self.MAX_CHAR]
416 return enteredMessage.strip()
418 def _update_letter_count(self, *args):
419 entryLength = self._smsEntry.get_buffer().get_char_count()
420 charsLeft = self.MAX_CHAR - entryLength
421 self._letterCountLabel.set_text(str(charsLeft))
423 self._smsButton.set_sensitive(False)
425 self._smsButton.set_sensitive(True)
427 def _on_entry_changed(self, *args):
428 self._update_letter_count()
430 def _on_send(self, *args):
431 self._dialog.response(gtk.RESPONSE_OK)
433 def _on_cancel(self, *args):
434 self._dialog.response(gtk.RESPONSE_CANCEL)
437 class Dialpad(object):
439 def __init__(self, widgetTree, errorDisplay):
440 self._errorDisplay = errorDisplay
441 self._smsDialog = SmsEntryDialog(widgetTree)
443 self._numberdisplay = widgetTree.get_widget("numberdisplay")
444 self._dialButton = widgetTree.get_widget("dial")
445 self._phonenumber = ""
446 self._prettynumber = ""
447 self._clearall_id = None
450 "on_dial_clicked": self._on_dial_clicked,
451 "on_sms_clicked": self._on_sms_clicked,
452 "on_digit_clicked": self._on_digit_clicked,
453 "on_clear_number": self._on_clear_number,
454 "on_back_clicked": self._on_backspace,
455 "on_back_pressed": self._on_back_pressed,
456 "on_back_released": self._on_back_released,
458 widgetTree.signal_autoconnect(callbackMapping)
460 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
463 self._dialButton.grab_focus()
468 def number_selected(self, action, number, message):
470 @note Actual dial function is patched in later
472 raise NotImplementedError
474 def get_number(self):
475 return self._phonenumber
477 def set_number(self, number):
479 Set the callback phonenumber
482 self._phonenumber = make_ugly(number)
483 self._prettynumber = make_pretty(self._phonenumber)
484 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
486 self._errorDisplay.push_exception(e)
495 def load_settings(self, config, section):
498 def save_settings(self, config, section):
500 @note Thread Agnostic
504 def _on_sms_clicked(self, widget):
505 action = PhoneTypeSelector.ACTION_SEND_SMS
506 phoneNumber = self.get_number()
508 message = self._smsDialog.run(phoneNumber, "", self._window)
511 action = PhoneTypeSelector.ACTION_CANCEL
513 if action == PhoneTypeSelector.ACTION_CANCEL:
515 self.number_selected(action, phoneNumber, message)
517 def _on_dial_clicked(self, widget):
518 action = PhoneTypeSelector.ACTION_DIAL
519 phoneNumber = self.get_number()
521 self.number_selected(action, phoneNumber, message)
523 def _on_clear_number(self, *args):
526 def _on_digit_clicked(self, widget):
527 self.set_number(self._phonenumber + widget.get_name()[-1])
529 def _on_backspace(self, widget):
530 self.set_number(self._phonenumber[:-1])
532 def _on_clearall(self):
536 def _on_back_pressed(self, widget):
537 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
539 def _on_back_released(self, widget):
540 if self._clearall_id is not None:
541 gobject.source_remove(self._clearall_id)
542 self._clearall_id = None
545 class AccountInfo(object):
547 def __init__(self, widgetTree, backend, errorDisplay):
548 self._errorDisplay = errorDisplay
549 self._backend = backend
550 self._isPopulated = False
552 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
553 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
554 self._callbackCombo = widgetTree.get_widget("callbackcombo")
555 self._onCallbackentryChangedId = 0
557 self._defaultCallback = ""
560 assert self._backend.is_authed()
561 self._accountViewNumberDisplay.set_use_markup(True)
562 self.set_account_number("")
563 self._callbackList.clear()
564 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
565 self.update(force=True)
568 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
572 self._callbackList.clear()
574 def get_selected_callback_number(self):
575 return make_ugly(self._callbackCombo.get_child().get_text())
577 def set_account_number(self, number):
579 Displays current account number
581 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
583 def update(self, force = False):
584 if not force and self._isPopulated:
586 self._populate_callback_combo()
587 self.set_account_number(self._backend.get_account_number())
590 self._callbackCombo.get_child().set_text("")
591 self.set_account_number("")
592 self._isPopulated = False
596 return "Account Info"
598 def load_settings(self, config, section):
599 self._defaultCallback = config.get(section, "callback")
601 def save_settings(self, config, section):
603 @note Thread Agnostic
605 callback = self.get_selected_callback_number()
606 config.set(section, "callback", callback)
608 def _populate_callback_combo(self):
609 self._isPopulated = True
610 self._callbackList.clear()
612 callbackNumbers = self._backend.get_callback_numbers()
613 except StandardError, e:
614 self._errorDisplay.push_exception(e)
615 self._isPopulated = False
618 for number, description in callbackNumbers.iteritems():
619 self._callbackList.append((make_pretty(number),))
621 self._callbackCombo.set_model(self._callbackList)
622 self._callbackCombo.set_text_column(0)
623 #callbackNumber = self._backend.get_callback_number()
624 callbackNumber = self._defaultCallback
625 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
627 def _set_callback_number(self, number):
629 if not self._backend.is_valid_syntax(number):
630 self._errorDisplay.push_message("%s is not a valid callback number" % number)
631 elif number == self._backend.get_callback_number():
633 "Callback number already is %s" % (
634 self._backend.get_callback_number(),
640 self._backend.set_callback_number(number)
641 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
642 make_pretty(number), make_pretty(self._backend.get_callback_number())
645 "Callback number set to %s" % (
646 self._backend.get_callback_number(),
650 except StandardError, e:
651 self._errorDisplay.push_exception(e)
653 def _on_callbackentry_changed(self, *args):
654 text = self.get_selected_callback_number()
655 number = make_ugly(text)
656 self._set_callback_number(number)
659 class RecentCallsView(object):
666 def __init__(self, widgetTree, backend, errorDisplay):
667 self._errorDisplay = errorDisplay
668 self._backend = backend
670 self._isPopulated = False
671 self._recentmodel = gtk.ListStore(
672 gobject.TYPE_STRING, # number
673 gobject.TYPE_STRING, # date
674 gobject.TYPE_STRING, # action
675 gobject.TYPE_STRING, # from
677 self._recentview = widgetTree.get_widget("recentview")
678 self._recentviewselection = None
679 self._onRecentviewRowActivatedId = 0
681 textrenderer = gtk.CellRendererText()
682 textrenderer.set_property("yalign", 0)
683 self._dateColumn = gtk.TreeViewColumn("Date")
684 self._dateColumn.pack_start(textrenderer, expand=True)
685 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
687 textrenderer = gtk.CellRendererText()
688 textrenderer.set_property("yalign", 0)
689 self._actionColumn = gtk.TreeViewColumn("Action")
690 self._actionColumn.pack_start(textrenderer, expand=True)
691 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
693 textrenderer = gtk.CellRendererText()
694 textrenderer.set_property("yalign", 0)
695 self._fromColumn = gtk.TreeViewColumn("From")
696 self._fromColumn.pack_start(textrenderer, expand=True)
697 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
698 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
700 self._window = gtk_toolbox.find_parent_window(self._recentview)
701 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
703 self._updateSink = gtk_toolbox.threaded_stage(
705 self._idly_populate_recentview,
706 gtk_toolbox.null_sink(),
711 assert self._backend.is_authed()
712 self._recentview.set_model(self._recentmodel)
714 self._recentview.append_column(self._dateColumn)
715 self._recentview.append_column(self._actionColumn)
716 self._recentview.append_column(self._fromColumn)
717 self._recentviewselection = self._recentview.get_selection()
718 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
720 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
723 self._recentview.disconnect(self._onRecentviewRowActivatedId)
727 self._recentview.remove_column(self._dateColumn)
728 self._recentview.remove_column(self._actionColumn)
729 self._recentview.remove_column(self._fromColumn)
730 self._recentview.set_model(None)
732 def number_selected(self, action, number, message):
734 @note Actual dial function is patched in later
736 raise NotImplementedError
738 def update(self, force = False):
739 if not force and self._isPopulated:
741 self._updateSink.send(())
744 self._isPopulated = False
745 self._recentmodel.clear()
749 return "Recent Calls"
751 def load_settings(self, config, section):
754 def save_settings(self, config, section):
756 @note Thread Agnostic
760 def _idly_populate_recentview(self):
761 self._isPopulated = True
762 self._recentmodel.clear()
765 recentItems = self._backend.get_recent()
766 except StandardError, e:
767 self._errorDisplay.push_exception_with_lock(e)
768 self._isPopulated = False
771 for personName, phoneNumber, date, action in recentItems:
773 personName = "Unknown"
774 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
775 prettyNumber = make_pretty(prettyNumber)
776 description = "%s - %s" % (personName, prettyNumber)
777 item = (phoneNumber, date, action.capitalize(), description)
778 with gtk_toolbox.gtk_lock():
779 self._recentmodel.append(item)
783 def _on_recentview_row_activated(self, treeview, path, view_column):
784 model, itr = self._recentviewselection.get_selected()
788 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
789 number = make_ugly(number)
790 contactPhoneNumbers = [("Phone", number)]
791 description = self._recentmodel.get_value(itr, self.FROM_IDX)
793 action, phoneNumber, message = self._phoneTypeSelector.run(
795 message = description,
796 parent = self._window,
798 if action == PhoneTypeSelector.ACTION_CANCEL:
802 self.number_selected(action, phoneNumber, message)
803 self._recentviewselection.unselect_all()
806 class MessagesView(object):
813 def __init__(self, widgetTree, backend, errorDisplay):
814 self._errorDisplay = errorDisplay
815 self._backend = backend
817 self._isPopulated = False
818 self._messagemodel = gtk.ListStore(
819 gobject.TYPE_STRING, # number
820 gobject.TYPE_STRING, # date
821 gobject.TYPE_STRING, # header
822 gobject.TYPE_STRING, # message
824 self._messageview = widgetTree.get_widget("messages_view")
825 self._messageviewselection = None
826 self._onMessageviewRowActivatedId = 0
828 textrenderer = gtk.CellRendererText()
829 textrenderer.set_property("yalign", 0)
830 self._dateColumn = gtk.TreeViewColumn("Date")
831 self._dateColumn.pack_start(textrenderer, expand=True)
832 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
834 textrenderer = gtk.CellRendererText()
835 textrenderer.set_property("yalign", 0)
836 self._headerColumn = gtk.TreeViewColumn("From")
837 self._headerColumn.pack_start(textrenderer, expand=True)
838 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
840 textrenderer = gtk.CellRendererText()
841 textrenderer.set_property("yalign", 0)
842 self._messageColumn = gtk.TreeViewColumn("Messages")
843 self._messageColumn.pack_start(textrenderer, expand=True)
844 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
845 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
847 self._window = gtk_toolbox.find_parent_window(self._messageview)
848 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
850 self._updateSink = gtk_toolbox.threaded_stage(
852 self._idly_populate_messageview,
853 gtk_toolbox.null_sink(),
858 assert self._backend.is_authed()
859 self._messageview.set_model(self._messagemodel)
861 self._messageview.append_column(self._dateColumn)
862 self._messageview.append_column(self._headerColumn)
863 self._messageview.append_column(self._messageColumn)
864 self._messageviewselection = self._messageview.get_selection()
865 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
867 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
870 self._messageview.disconnect(self._onMessageviewRowActivatedId)
874 self._messageview.remove_column(self._dateColumn)
875 self._messageview.remove_column(self._headerColumn)
876 self._messageview.remove_column(self._messageColumn)
877 self._messageview.set_model(None)
879 def number_selected(self, action, number, message):
881 @note Actual dial function is patched in later
883 raise NotImplementedError
885 def update(self, force = False):
886 if not force and self._isPopulated:
888 self._updateSink.send(())
891 self._isPopulated = False
892 self._messagemodel.clear()
898 def load_settings(self, config, section):
901 def save_settings(self, config, section):
903 @note Thread Agnostic
907 def _idly_populate_messageview(self):
908 self._isPopulated = True
909 self._messagemodel.clear()
912 messageItems = self._backend.get_messages()
913 except StandardError, e:
914 self._errorDisplay.push_exception_with_lock(e)
915 self._isPopulated = False
918 for header, number, relativeDate, message in messageItems:
919 number = make_ugly(number)
920 row = (number, relativeDate, header, message)
921 with gtk_toolbox.gtk_lock():
922 self._messagemodel.append(row)
926 def _on_messageview_row_activated(self, treeview, path, view_column):
927 model, itr = self._messageviewselection.get_selected()
931 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
932 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
934 action, phoneNumber, message = self._phoneTypeSelector.run(
936 message = description,
937 parent = self._window,
939 if action == PhoneTypeSelector.ACTION_CANCEL:
943 self.number_selected(action, phoneNumber, message)
944 self._messageviewselection.unselect_all()
947 class ContactsView(object):
949 def __init__(self, widgetTree, backend, errorDisplay):
950 self._errorDisplay = errorDisplay
951 self._backend = backend
953 self._addressBook = None
954 self._addressBookFactories = [null_backend.NullAddressBook()]
956 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
957 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
959 self._isPopulated = False
960 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
961 self._contactsviewselection = None
962 self._contactsview = widgetTree.get_widget("contactsview")
964 self._contactColumn = gtk.TreeViewColumn("Contact")
965 displayContactSource = False
966 if displayContactSource:
967 textrenderer = gtk.CellRendererText()
968 self._contactColumn.pack_start(textrenderer, expand=False)
969 self._contactColumn.add_attribute(textrenderer, 'text', 0)
970 textrenderer = gtk.CellRendererText()
971 self._contactColumn.pack_start(textrenderer, expand=True)
972 self._contactColumn.add_attribute(textrenderer, 'text', 1)
973 textrenderer = gtk.CellRendererText()
974 self._contactColumn.pack_start(textrenderer, expand=True)
975 self._contactColumn.add_attribute(textrenderer, 'text', 4)
976 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
977 self._contactColumn.set_sort_column_id(1)
978 self._contactColumn.set_visible(True)
980 self._onContactsviewRowActivatedId = 0
981 self._onAddressbookComboChangedId = 0
982 self._window = gtk_toolbox.find_parent_window(self._contactsview)
983 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
985 self._updateSink = gtk_toolbox.threaded_stage(
987 self._idly_populate_contactsview,
988 gtk_toolbox.null_sink(),
993 assert self._backend.is_authed()
995 self._contactsview.set_model(self._contactsmodel)
996 self._contactsview.append_column(self._contactColumn)
997 self._contactsviewselection = self._contactsview.get_selection()
998 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1000 self._booksList.clear()
1001 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1002 if factoryName and bookName:
1003 entryName = "%s: %s" % (factoryName, bookName)
1005 entryName = factoryName
1007 entryName = bookName
1009 entryName = "Bad name (%d)" % factoryId
1010 row = (str(factoryId), bookId, entryName)
1011 self._booksList.append(row)
1013 self._booksSelectionBox.set_model(self._booksList)
1014 cell = gtk.CellRendererText()
1015 self._booksSelectionBox.pack_start(cell, True)
1016 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1017 self._booksSelectionBox.set_active(0)
1019 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1020 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1023 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1024 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1028 self._booksSelectionBox.clear()
1029 self._booksSelectionBox.set_model(None)
1030 self._contactsview.set_model(None)
1031 self._contactsview.remove_column(self._contactColumn)
1033 def number_selected(self, action, number, message):
1035 @note Actual dial function is patched in later
1037 raise NotImplementedError
1039 def get_addressbooks(self):
1041 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1043 for i, factory in enumerate(self._addressBookFactories):
1044 for bookFactory, bookId, bookName in factory.get_addressbooks():
1045 yield (i, bookId), (factory.factory_name(), bookName)
1047 def open_addressbook(self, bookFactoryId, bookId):
1048 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1049 self.update(force=True)
1051 def update(self, force = False):
1052 if not force and self._isPopulated:
1054 self._updateSink.send(())
1057 self._isPopulated = False
1058 self._contactsmodel.clear()
1059 for factory in self._addressBookFactories:
1060 factory.clear_caches()
1061 self._addressBook.clear_caches()
1063 def append(self, book):
1064 self._addressBookFactories.append(book)
1066 def extend(self, books):
1067 self._addressBookFactories.extend(books)
1073 def load_settings(self, config, section):
1076 def save_settings(self, config, section):
1078 @note Thread Agnostic
1082 def _idly_populate_contactsview(self):
1083 self._isPopulated = True
1086 # completely disable updating the treeview while we populate the data
1087 self._contactsview.freeze_child_notify()
1089 self._contactsview.set_model(None)
1091 addressBook = self._addressBook
1093 contacts = addressBook.get_contacts()
1094 except StandardError, e:
1096 self._isPopulated = False
1097 self._errorDisplay.push_exception_with_lock(e)
1098 for contactId, contactName in contacts:
1099 contactType = (addressBook.contact_source_short_name(contactId), )
1100 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1102 # restart the treeview data rendering
1103 self._contactsview.set_model(self._contactsmodel)
1105 self._contactsview.thaw_child_notify()
1108 def _on_addressbook_combo_changed(self, *args, **kwds):
1109 itr = self._booksSelectionBox.get_active_iter()
1112 factoryId = int(self._booksList.get_value(itr, 0))
1113 bookId = self._booksList.get_value(itr, 1)
1114 self.open_addressbook(factoryId, bookId)
1116 def _on_contactsview_row_activated(self, treeview, path, view_column):
1117 model, itr = self._contactsviewselection.get_selected()
1121 contactId = self._contactsmodel.get_value(itr, 3)
1122 contactName = self._contactsmodel.get_value(itr, 1)
1124 contactDetails = self._addressBook.get_contact_details(contactId)
1125 except StandardError, e:
1127 self._errorDisplay.push_exception(e)
1128 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1130 if len(contactPhoneNumbers) == 0:
1133 action, phoneNumber, message = self._phoneTypeSelector.run(
1134 contactPhoneNumbers,
1135 message = contactName,
1136 parent = self._window,
1138 if action == PhoneTypeSelector.ACTION_CANCEL:
1142 self.number_selected(action, phoneNumber, message)
1143 self._contactsviewselection.unselect_all()