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 RuntimeError, 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():
632 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
634 self._backend.set_callback_number(number)
635 warnings.warn("Callback number set to %s" % self._backend.get_callback_number(), UserWarning, 2)
636 except RuntimeError, e:
637 self._errorDisplay.push_exception(e)
639 def _on_callbackentry_changed(self, *args):
640 text = self.get_selected_callback_number()
641 self._set_callback_number(text)
644 class RecentCallsView(object):
651 def __init__(self, widgetTree, backend, errorDisplay):
652 self._errorDisplay = errorDisplay
653 self._backend = backend
655 self._isPopulated = False
656 self._recentmodel = gtk.ListStore(
657 gobject.TYPE_STRING, # number
658 gobject.TYPE_STRING, # date
659 gobject.TYPE_STRING, # action
660 gobject.TYPE_STRING, # from
662 self._recentview = widgetTree.get_widget("recentview")
663 self._recentviewselection = None
664 self._onRecentviewRowActivatedId = 0
666 textrenderer = gtk.CellRendererText()
667 textrenderer.set_property("yalign", 0)
668 self._dateColumn = gtk.TreeViewColumn("Date")
669 self._dateColumn.pack_start(textrenderer, expand=True)
670 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
672 textrenderer = gtk.CellRendererText()
673 textrenderer.set_property("yalign", 0)
674 self._actionColumn = gtk.TreeViewColumn("Action")
675 self._actionColumn.pack_start(textrenderer, expand=True)
676 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
678 textrenderer = gtk.CellRendererText()
679 textrenderer.set_property("yalign", 0)
680 self._fromColumn = gtk.TreeViewColumn("From")
681 self._fromColumn.pack_start(textrenderer, expand=True)
682 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
683 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
685 self._window = gtk_toolbox.find_parent_window(self._recentview)
686 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
689 assert self._backend.is_authed()
690 self._recentview.set_model(self._recentmodel)
692 self._recentview.append_column(self._dateColumn)
693 self._recentview.append_column(self._actionColumn)
694 self._recentview.append_column(self._fromColumn)
695 self._recentviewselection = self._recentview.get_selection()
696 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
698 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
701 self._recentview.disconnect(self._onRecentviewRowActivatedId)
705 self._recentview.remove_column(self._dateColumn)
706 self._recentview.remove_column(self._actionColumn)
707 self._recentview.remove_column(self._fromColumn)
708 self._recentview.set_model(None)
710 def number_selected(self, action, number, message):
712 @note Actual dial function is patched in later
714 raise NotImplementedError
716 def update(self, force = False):
717 if not force and self._isPopulated:
719 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
720 backgroundPopulate.setDaemon(True)
721 backgroundPopulate.start()
724 self._isPopulated = False
725 self._recentmodel.clear()
729 return "Recent Calls"
731 def load_settings(self, config, section):
734 def save_settings(self, config, section):
736 @note Thread Agnostic
740 def _idly_populate_recentview(self):
741 self._isPopulated = True
742 self._recentmodel.clear()
745 recentItems = self._backend.get_recent()
746 except RuntimeError, e:
747 self._errorDisplay.push_exception_with_lock(e)
748 self._isPopulated = False
751 for personName, phoneNumber, date, action in recentItems:
753 personName = "Unknown"
754 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
755 prettyNumber = make_pretty(prettyNumber)
756 description = "%s - %s" % (personName, prettyNumber)
757 item = (phoneNumber, date, action.capitalize(), description)
758 with gtk_toolbox.gtk_lock():
759 self._recentmodel.append(item)
763 def _on_recentview_row_activated(self, treeview, path, view_column):
764 model, itr = self._recentviewselection.get_selected()
768 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
769 number = make_ugly(number)
770 contactPhoneNumbers = [("Phone", number)]
771 description = self._recentmodel.get_value(itr, self.FROM_IDX)
773 action, phoneNumber, message = self._phoneTypeSelector.run(
775 message = description,
776 parent = self._window,
778 if action == PhoneTypeSelector.ACTION_CANCEL:
782 self.number_selected(action, phoneNumber, message)
783 self._recentviewselection.unselect_all()
786 class MessagesView(object):
793 def __init__(self, widgetTree, backend, errorDisplay):
794 self._errorDisplay = errorDisplay
795 self._backend = backend
797 self._isPopulated = False
798 self._messagemodel = gtk.ListStore(
799 gobject.TYPE_STRING, # number
800 gobject.TYPE_STRING, # date
801 gobject.TYPE_STRING, # header
802 gobject.TYPE_STRING, # message
804 self._messageview = widgetTree.get_widget("messages_view")
805 self._messageviewselection = None
806 self._onMessageviewRowActivatedId = 0
808 textrenderer = gtk.CellRendererText()
809 textrenderer.set_property("yalign", 0)
810 self._dateColumn = gtk.TreeViewColumn("Date")
811 self._dateColumn.pack_start(textrenderer, expand=True)
812 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
814 textrenderer = gtk.CellRendererText()
815 textrenderer.set_property("yalign", 0)
816 self._headerColumn = gtk.TreeViewColumn("From")
817 self._headerColumn.pack_start(textrenderer, expand=True)
818 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
820 textrenderer = gtk.CellRendererText()
821 textrenderer.set_property("yalign", 0)
822 self._messageColumn = gtk.TreeViewColumn("Messages")
823 self._messageColumn.pack_start(textrenderer, expand=True)
824 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
825 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
827 self._window = gtk_toolbox.find_parent_window(self._messageview)
828 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
831 assert self._backend.is_authed()
832 self._messageview.set_model(self._messagemodel)
834 self._messageview.append_column(self._dateColumn)
835 self._messageview.append_column(self._headerColumn)
836 self._messageview.append_column(self._messageColumn)
837 self._messageviewselection = self._messageview.get_selection()
838 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
840 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
843 self._messageview.disconnect(self._onMessageviewRowActivatedId)
847 self._messageview.remove_column(self._dateColumn)
848 self._messageview.remove_column(self._headerColumn)
849 self._messageview.remove_column(self._messageColumn)
850 self._messageview.set_model(None)
852 def number_selected(self, action, number, message):
854 @note Actual dial function is patched in later
856 raise NotImplementedError
858 def update(self, force = False):
859 if not force and self._isPopulated:
861 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
862 backgroundPopulate.setDaemon(True)
863 backgroundPopulate.start()
866 self._isPopulated = False
867 self._messagemodel.clear()
873 def load_settings(self, config, section):
876 def save_settings(self, config, section):
878 @note Thread Agnostic
882 def _idly_populate_messageview(self):
883 self._isPopulated = True
884 self._messagemodel.clear()
887 messageItems = self._backend.get_messages()
888 except RuntimeError, e:
889 self._errorDisplay.push_exception_with_lock(e)
890 self._isPopulated = False
893 for header, number, relativeDate, message in messageItems:
894 number = make_ugly(number)
895 row = (number, relativeDate, header, message)
896 with gtk_toolbox.gtk_lock():
897 self._messagemodel.append(row)
901 def _on_messageview_row_activated(self, treeview, path, view_column):
902 model, itr = self._messageviewselection.get_selected()
906 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
907 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
909 action, phoneNumber, message = self._phoneTypeSelector.run(
911 message = description,
912 parent = self._window,
914 if action == PhoneTypeSelector.ACTION_CANCEL:
918 self.number_selected(action, phoneNumber, message)
919 self._messageviewselection.unselect_all()
922 class ContactsView(object):
924 def __init__(self, widgetTree, backend, errorDisplay):
925 self._errorDisplay = errorDisplay
926 self._backend = backend
928 self._addressBook = None
929 self._addressBookFactories = [null_backend.NullAddressBook()]
931 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
932 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
934 self._isPopulated = False
935 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
936 self._contactsviewselection = None
937 self._contactsview = widgetTree.get_widget("contactsview")
939 self._contactColumn = gtk.TreeViewColumn("Contact")
940 displayContactSource = False
941 if displayContactSource:
942 textrenderer = gtk.CellRendererText()
943 self._contactColumn.pack_start(textrenderer, expand=False)
944 self._contactColumn.add_attribute(textrenderer, 'text', 0)
945 textrenderer = gtk.CellRendererText()
946 self._contactColumn.pack_start(textrenderer, expand=True)
947 self._contactColumn.add_attribute(textrenderer, 'text', 1)
948 textrenderer = gtk.CellRendererText()
949 self._contactColumn.pack_start(textrenderer, expand=True)
950 self._contactColumn.add_attribute(textrenderer, 'text', 4)
951 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
952 self._contactColumn.set_sort_column_id(1)
953 self._contactColumn.set_visible(True)
955 self._onContactsviewRowActivatedId = 0
956 self._onAddressbookComboChangedId = 0
957 self._window = gtk_toolbox.find_parent_window(self._contactsview)
958 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
961 assert self._backend.is_authed()
963 self._contactsview.set_model(self._contactsmodel)
964 self._contactsview.append_column(self._contactColumn)
965 self._contactsviewselection = self._contactsview.get_selection()
966 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
968 self._booksList.clear()
969 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
970 if factoryName and bookName:
971 entryName = "%s: %s" % (factoryName, bookName)
973 entryName = factoryName
977 entryName = "Bad name (%d)" % factoryId
978 row = (str(factoryId), bookId, entryName)
979 self._booksList.append(row)
981 self._booksSelectionBox.set_model(self._booksList)
982 cell = gtk.CellRendererText()
983 self._booksSelectionBox.pack_start(cell, True)
984 self._booksSelectionBox.add_attribute(cell, 'text', 2)
985 self._booksSelectionBox.set_active(0)
987 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
988 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
991 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
992 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
996 self._booksSelectionBox.clear()
997 self._booksSelectionBox.set_model(None)
998 self._contactsview.set_model(None)
999 self._contactsview.remove_column(self._contactColumn)
1001 def number_selected(self, action, number, message):
1003 @note Actual dial function is patched in later
1005 raise NotImplementedError
1007 def get_addressbooks(self):
1009 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1011 for i, factory in enumerate(self._addressBookFactories):
1012 for bookFactory, bookId, bookName in factory.get_addressbooks():
1013 yield (i, bookId), (factory.factory_name(), bookName)
1015 def open_addressbook(self, bookFactoryId, bookId):
1016 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1017 self.update(force=True)
1019 def update(self, force = False):
1020 if not force and self._isPopulated:
1022 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1023 backgroundPopulate.setDaemon(True)
1024 backgroundPopulate.start()
1027 self._isPopulated = False
1028 self._contactsmodel.clear()
1030 def clear_caches(self):
1031 for factory in self._addressBookFactories:
1032 factory.clear_caches()
1033 self._addressBook.clear_caches()
1035 def append(self, book):
1036 self._addressBookFactories.append(book)
1038 def extend(self, books):
1039 self._addressBookFactories.extend(books)
1045 def load_settings(self, config, section):
1048 def save_settings(self, config, section):
1050 @note Thread Agnostic
1054 def _idly_populate_contactsview(self):
1055 self._isPopulated = True
1058 # completely disable updating the treeview while we populate the data
1059 self._contactsview.freeze_child_notify()
1060 self._contactsview.set_model(None)
1062 addressBook = self._addressBook
1064 contacts = addressBook.get_contacts()
1065 except RuntimeError, e:
1067 self._isPopulated = False
1068 self._errorDisplay.push_exception_with_lock(e)
1069 for contactId, contactName in contacts:
1070 contactType = (addressBook.contact_source_short_name(contactId), )
1071 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1073 # restart the treeview data rendering
1074 self._contactsview.set_model(self._contactsmodel)
1075 self._contactsview.thaw_child_notify()
1078 def _on_addressbook_combo_changed(self, *args, **kwds):
1079 itr = self._booksSelectionBox.get_active_iter()
1082 factoryId = int(self._booksList.get_value(itr, 0))
1083 bookId = self._booksList.get_value(itr, 1)
1084 self.open_addressbook(factoryId, bookId)
1086 def _on_contactsview_row_activated(self, treeview, path, view_column):
1087 model, itr = self._contactsviewselection.get_selected()
1091 contactId = self._contactsmodel.get_value(itr, 3)
1092 contactName = self._contactsmodel.get_value(itr, 1)
1094 contactDetails = self._addressBook.get_contact_details(contactId)
1095 except RuntimeError, e:
1097 self._errorDisplay.push_exception(e)
1098 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1100 if len(contactPhoneNumbers) == 0:
1103 action, phoneNumber, message = self._phoneTypeSelector.run(
1104 contactPhoneNumbers,
1105 message = contactName,
1106 parent = self._window,
1108 if action == PhoneTypeSelector.ACTION_CANCEL:
1112 self.number_selected(action, phoneNumber, message)
1113 self._contactsviewselection.unselect_all()