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)
704 assert self._backend.is_authed()
705 self._recentview.set_model(self._recentmodel)
707 self._recentview.append_column(self._dateColumn)
708 self._recentview.append_column(self._actionColumn)
709 self._recentview.append_column(self._fromColumn)
710 self._recentviewselection = self._recentview.get_selection()
711 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
713 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
716 self._recentview.disconnect(self._onRecentviewRowActivatedId)
720 self._recentview.remove_column(self._dateColumn)
721 self._recentview.remove_column(self._actionColumn)
722 self._recentview.remove_column(self._fromColumn)
723 self._recentview.set_model(None)
725 def number_selected(self, action, number, message):
727 @note Actual dial function is patched in later
729 raise NotImplementedError
731 def update(self, force = False):
732 if not force and self._isPopulated:
734 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
735 backgroundPopulate.setDaemon(True)
736 backgroundPopulate.start()
739 self._isPopulated = False
740 self._recentmodel.clear()
744 return "Recent Calls"
746 def load_settings(self, config, section):
749 def save_settings(self, config, section):
751 @note Thread Agnostic
755 def _idly_populate_recentview(self):
756 self._isPopulated = True
757 self._recentmodel.clear()
760 recentItems = self._backend.get_recent()
761 except StandardError, e:
762 self._errorDisplay.push_exception_with_lock(e)
763 self._isPopulated = False
766 for personName, phoneNumber, date, action in recentItems:
768 personName = "Unknown"
769 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
770 prettyNumber = make_pretty(prettyNumber)
771 description = "%s - %s" % (personName, prettyNumber)
772 item = (phoneNumber, date, action.capitalize(), description)
773 with gtk_toolbox.gtk_lock():
774 self._recentmodel.append(item)
778 def _on_recentview_row_activated(self, treeview, path, view_column):
779 model, itr = self._recentviewselection.get_selected()
783 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
784 number = make_ugly(number)
785 contactPhoneNumbers = [("Phone", number)]
786 description = self._recentmodel.get_value(itr, self.FROM_IDX)
788 action, phoneNumber, message = self._phoneTypeSelector.run(
790 message = description,
791 parent = self._window,
793 if action == PhoneTypeSelector.ACTION_CANCEL:
797 self.number_selected(action, phoneNumber, message)
798 self._recentviewselection.unselect_all()
801 class MessagesView(object):
808 def __init__(self, widgetTree, backend, errorDisplay):
809 self._errorDisplay = errorDisplay
810 self._backend = backend
812 self._isPopulated = False
813 self._messagemodel = gtk.ListStore(
814 gobject.TYPE_STRING, # number
815 gobject.TYPE_STRING, # date
816 gobject.TYPE_STRING, # header
817 gobject.TYPE_STRING, # message
819 self._messageview = widgetTree.get_widget("messages_view")
820 self._messageviewselection = None
821 self._onMessageviewRowActivatedId = 0
823 textrenderer = gtk.CellRendererText()
824 textrenderer.set_property("yalign", 0)
825 self._dateColumn = gtk.TreeViewColumn("Date")
826 self._dateColumn.pack_start(textrenderer, expand=True)
827 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
829 textrenderer = gtk.CellRendererText()
830 textrenderer.set_property("yalign", 0)
831 self._headerColumn = gtk.TreeViewColumn("From")
832 self._headerColumn.pack_start(textrenderer, expand=True)
833 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
835 textrenderer = gtk.CellRendererText()
836 textrenderer.set_property("yalign", 0)
837 self._messageColumn = gtk.TreeViewColumn("Messages")
838 self._messageColumn.pack_start(textrenderer, expand=True)
839 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
840 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
842 self._window = gtk_toolbox.find_parent_window(self._messageview)
843 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
846 assert self._backend.is_authed()
847 self._messageview.set_model(self._messagemodel)
849 self._messageview.append_column(self._dateColumn)
850 self._messageview.append_column(self._headerColumn)
851 self._messageview.append_column(self._messageColumn)
852 self._messageviewselection = self._messageview.get_selection()
853 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
855 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
858 self._messageview.disconnect(self._onMessageviewRowActivatedId)
862 self._messageview.remove_column(self._dateColumn)
863 self._messageview.remove_column(self._headerColumn)
864 self._messageview.remove_column(self._messageColumn)
865 self._messageview.set_model(None)
867 def number_selected(self, action, number, message):
869 @note Actual dial function is patched in later
871 raise NotImplementedError
873 def update(self, force = False):
874 if not force and self._isPopulated:
876 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
877 backgroundPopulate.setDaemon(True)
878 backgroundPopulate.start()
881 self._isPopulated = False
882 self._messagemodel.clear()
888 def load_settings(self, config, section):
891 def save_settings(self, config, section):
893 @note Thread Agnostic
897 def _idly_populate_messageview(self):
898 self._isPopulated = True
899 self._messagemodel.clear()
902 messageItems = self._backend.get_messages()
903 except StandardError, e:
904 self._errorDisplay.push_exception_with_lock(e)
905 self._isPopulated = False
908 for header, number, relativeDate, message in messageItems:
909 number = make_ugly(number)
910 row = (number, relativeDate, header, message)
911 with gtk_toolbox.gtk_lock():
912 self._messagemodel.append(row)
916 def _on_messageview_row_activated(self, treeview, path, view_column):
917 model, itr = self._messageviewselection.get_selected()
921 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
922 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
924 action, phoneNumber, message = self._phoneTypeSelector.run(
926 message = description,
927 parent = self._window,
929 if action == PhoneTypeSelector.ACTION_CANCEL:
933 self.number_selected(action, phoneNumber, message)
934 self._messageviewselection.unselect_all()
937 class ContactsView(object):
939 def __init__(self, widgetTree, backend, errorDisplay):
940 self._errorDisplay = errorDisplay
941 self._backend = backend
943 self._addressBook = None
944 self._addressBookFactories = [null_backend.NullAddressBook()]
946 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
947 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
949 self._isPopulated = False
950 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
951 self._contactsviewselection = None
952 self._contactsview = widgetTree.get_widget("contactsview")
954 self._contactColumn = gtk.TreeViewColumn("Contact")
955 displayContactSource = False
956 if displayContactSource:
957 textrenderer = gtk.CellRendererText()
958 self._contactColumn.pack_start(textrenderer, expand=False)
959 self._contactColumn.add_attribute(textrenderer, 'text', 0)
960 textrenderer = gtk.CellRendererText()
961 self._contactColumn.pack_start(textrenderer, expand=True)
962 self._contactColumn.add_attribute(textrenderer, 'text', 1)
963 textrenderer = gtk.CellRendererText()
964 self._contactColumn.pack_start(textrenderer, expand=True)
965 self._contactColumn.add_attribute(textrenderer, 'text', 4)
966 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
967 self._contactColumn.set_sort_column_id(1)
968 self._contactColumn.set_visible(True)
970 self._onContactsviewRowActivatedId = 0
971 self._onAddressbookComboChangedId = 0
972 self._window = gtk_toolbox.find_parent_window(self._contactsview)
973 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
976 assert self._backend.is_authed()
978 self._contactsview.set_model(self._contactsmodel)
979 self._contactsview.append_column(self._contactColumn)
980 self._contactsviewselection = self._contactsview.get_selection()
981 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
983 self._booksList.clear()
984 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
985 if factoryName and bookName:
986 entryName = "%s: %s" % (factoryName, bookName)
988 entryName = factoryName
992 entryName = "Bad name (%d)" % factoryId
993 row = (str(factoryId), bookId, entryName)
994 self._booksList.append(row)
996 self._booksSelectionBox.set_model(self._booksList)
997 cell = gtk.CellRendererText()
998 self._booksSelectionBox.pack_start(cell, True)
999 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1000 self._booksSelectionBox.set_active(0)
1002 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1003 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1006 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1007 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1011 self._booksSelectionBox.clear()
1012 self._booksSelectionBox.set_model(None)
1013 self._contactsview.set_model(None)
1014 self._contactsview.remove_column(self._contactColumn)
1016 def number_selected(self, action, number, message):
1018 @note Actual dial function is patched in later
1020 raise NotImplementedError
1022 def get_addressbooks(self):
1024 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1026 for i, factory in enumerate(self._addressBookFactories):
1027 for bookFactory, bookId, bookName in factory.get_addressbooks():
1028 yield (i, bookId), (factory.factory_name(), bookName)
1030 def open_addressbook(self, bookFactoryId, bookId):
1031 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1032 self.update(force=True)
1034 def update(self, force = False):
1035 if not force and self._isPopulated:
1037 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1038 backgroundPopulate.setDaemon(True)
1039 backgroundPopulate.start()
1042 self._isPopulated = False
1043 self._contactsmodel.clear()
1045 def clear_caches(self):
1046 for factory in self._addressBookFactories:
1047 factory.clear_caches()
1048 self._addressBook.clear_caches()
1050 def append(self, book):
1051 self._addressBookFactories.append(book)
1053 def extend(self, books):
1054 self._addressBookFactories.extend(books)
1060 def load_settings(self, config, section):
1063 def save_settings(self, config, section):
1065 @note Thread Agnostic
1069 def _idly_populate_contactsview(self):
1070 self._isPopulated = True
1073 # completely disable updating the treeview while we populate the data
1074 self._contactsview.freeze_child_notify()
1076 self._contactsview.set_model(None)
1078 addressBook = self._addressBook
1080 contacts = addressBook.get_contacts()
1081 except StandardError, e:
1083 self._isPopulated = False
1084 self._errorDisplay.push_exception_with_lock(e)
1085 for contactId, contactName in contacts:
1086 contactType = (addressBook.contact_source_short_name(contactId), )
1087 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1089 # restart the treeview data rendering
1090 self._contactsview.set_model(self._contactsmodel)
1092 self._contactsview.thaw_child_notify()
1095 def _on_addressbook_combo_changed(self, *args, **kwds):
1096 itr = self._booksSelectionBox.get_active_iter()
1099 factoryId = int(self._booksList.get_value(itr, 0))
1100 bookId = self._booksList.get_value(itr, 1)
1101 self.open_addressbook(factoryId, bookId)
1103 def _on_contactsview_row_activated(self, treeview, path, view_column):
1104 model, itr = self._contactsviewselection.get_selected()
1108 contactId = self._contactsmodel.get_value(itr, 3)
1109 contactName = self._contactsmodel.get_value(itr, 1)
1111 contactDetails = self._addressBook.get_contact_details(contactId)
1112 except StandardError, e:
1114 self._errorDisplay.push_exception(e)
1115 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1117 if len(contactPhoneNumbers) == 0:
1120 action, phoneNumber, message = self._phoneTypeSelector.run(
1121 contactPhoneNumbers,
1122 message = contactName,
1123 parent = self._window,
1125 if action == PhoneTypeSelector.ACTION_CANCEL:
1129 self.number_selected(action, phoneNumber, message)
1130 self._contactsviewselection.unselect_all()