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