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):
372 def __init__(self, widgetTree):
373 self._widgetTree = widgetTree
374 self._dialog = self._widgetTree.get_widget("smsDialog")
376 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
377 self._smsButton.connect("clicked", self._on_send)
379 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
380 self._cancelButton.connect("clicked", self._on_cancel)
382 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
383 self._message = self._widgetTree.get_widget("smsMessage")
384 self._smsEntry = self._widgetTree.get_widget("smsEntry")
385 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
387 def run(self, number, message = "", parent = None):
389 self._message.set_markup(message)
392 self._message.set_markup("")
394 self._smsEntry.get_buffer().set_text("")
395 self._update_letter_count()
397 if parent is not None:
398 self._dialog.set_transient_for(parent)
401 userResponse = self._dialog.run()
405 if userResponse == gtk.RESPONSE_OK:
406 entryBuffer = self._smsEntry.get_buffer()
407 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
408 enteredMessage = enteredMessage[0:self.MAX_CHAR]
412 return enteredMessage.strip()
414 def _update_letter_count(self, *args):
415 entryLength = self._smsEntry.get_buffer().get_char_count()
416 charsLeft = self.MAX_CHAR - entryLength
417 self._letterCountLabel.set_text(str(charsLeft))
419 self._smsButton.set_sensitive(False)
421 self._smsButton.set_sensitive(True)
423 def _on_entry_changed(self, *args):
424 self._update_letter_count()
426 def _on_send(self, *args):
427 self._dialog.response(gtk.RESPONSE_OK)
429 def _on_cancel(self, *args):
430 self._dialog.response(gtk.RESPONSE_CANCEL)
433 class Dialpad(object):
435 def __init__(self, widgetTree, errorDisplay):
436 self._errorDisplay = errorDisplay
437 self._smsDialog = SmsEntryDialog(widgetTree)
439 self._numberdisplay = widgetTree.get_widget("numberdisplay")
440 self._dialButton = widgetTree.get_widget("dial")
441 self._phonenumber = ""
442 self._prettynumber = ""
443 self._clearall_id = None
446 "on_dial_clicked": self._on_dial_clicked,
447 "on_sms_clicked": self._on_sms_clicked,
448 "on_digit_clicked": self._on_digit_clicked,
449 "on_clear_number": self._on_clear_number,
450 "on_back_clicked": self._on_backspace,
451 "on_back_pressed": self._on_back_pressed,
452 "on_back_released": self._on_back_released,
454 widgetTree.signal_autoconnect(callbackMapping)
456 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
459 self._dialButton.grab_focus()
464 def number_selected(self, action, number, message):
466 @note Actual dial function is patched in later
468 raise NotImplementedError
470 def get_number(self):
471 return self._phonenumber
473 def set_number(self, number):
475 Set the callback phonenumber
478 self._phonenumber = make_ugly(number)
479 self._prettynumber = make_pretty(self._phonenumber)
480 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
482 self._errorDisplay.push_exception(e)
491 def load_settings(self, config, section):
494 def save_settings(self, config, section):
496 @note Thread Agnostic
500 def _on_sms_clicked(self, widget):
501 action = PhoneTypeSelector.ACTION_SEND_SMS
502 phoneNumber = self.get_number()
504 message = self._smsDialog.run(phoneNumber, "", self._window)
507 action = PhoneTypeSelector.ACTION_CANCEL
509 if action == PhoneTypeSelector.ACTION_CANCEL:
511 self.number_selected(action, phoneNumber, message)
513 def _on_dial_clicked(self, widget):
514 action = PhoneTypeSelector.ACTION_DIAL
515 phoneNumber = self.get_number()
517 self.number_selected(action, phoneNumber, message)
519 def _on_clear_number(self, *args):
522 def _on_digit_clicked(self, widget):
523 self.set_number(self._phonenumber + widget.get_name()[-1])
525 def _on_backspace(self, widget):
526 self.set_number(self._phonenumber[:-1])
528 def _on_clearall(self):
532 def _on_back_pressed(self, widget):
533 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
535 def _on_back_released(self, widget):
536 if self._clearall_id is not None:
537 gobject.source_remove(self._clearall_id)
538 self._clearall_id = None
541 class AccountInfo(object):
543 def __init__(self, widgetTree, backend, errorDisplay):
544 self._errorDisplay = errorDisplay
545 self._backend = backend
546 self._isPopulated = False
548 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
549 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
550 self._callbackCombo = widgetTree.get_widget("callbackcombo")
551 self._onCallbackentryChangedId = 0
553 self._defaultCallback = ""
556 assert self._backend.is_authed()
557 self._accountViewNumberDisplay.set_use_markup(True)
558 self.set_account_number("")
559 self._callbackList.clear()
560 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
561 self.update(force=True)
564 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
568 self._callbackList.clear()
570 def get_selected_callback_number(self):
571 return make_ugly(self._callbackCombo.get_child().get_text())
573 def set_account_number(self, number):
575 Displays current account number
577 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
579 def update(self, force = False):
580 if not force and self._isPopulated:
582 self._populate_callback_combo()
583 self.set_account_number(self._backend.get_account_number())
586 self._callbackCombo.get_child().set_text("")
587 self.set_account_number("")
588 self._isPopulated = False
592 return "Account Info"
594 def load_settings(self, config, section):
595 self._defaultCallback = config.get(section, "callback")
597 def save_settings(self, config, section):
599 @note Thread Agnostic
601 callback = self.get_selected_callback_number()
602 config.set(section, "callback", callback)
604 def _populate_callback_combo(self):
605 self._isPopulated = True
606 self._callbackList.clear()
608 callbackNumbers = self._backend.get_callback_numbers()
609 except RuntimeError, e:
610 self._errorDisplay.push_exception(e)
611 self._isPopulated = False
614 for number, description in callbackNumbers.iteritems():
615 self._callbackList.append((make_pretty(number),))
617 self._callbackCombo.set_model(self._callbackList)
618 self._callbackCombo.set_text_column(0)
619 #callbackNumber = self._backend.get_callback_number()
620 callbackNumber = self._defaultCallback
621 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
623 def _set_callback_number(self, number):
625 if not self._backend.is_valid_syntax(number):
626 self._errorDisplay.push_message("%s is not a valid callback number" % number)
627 elif number == self._backend.get_callback_number():
628 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
630 self._backend.set_callback_number(number)
631 warnings.warn("Callback number set to %s" % self._backend.get_callback_number(), UserWarning, 2)
632 except RuntimeError, e:
633 self._errorDisplay.push_exception(e)
635 def _on_callbackentry_changed(self, *args):
636 text = self.get_selected_callback_number()
637 self._set_callback_number(text)
640 class RecentCallsView(object):
647 def __init__(self, widgetTree, backend, errorDisplay):
648 self._errorDisplay = errorDisplay
649 self._backend = backend
651 self._isPopulated = False
652 self._recentmodel = gtk.ListStore(
653 gobject.TYPE_STRING, # number
654 gobject.TYPE_STRING, # date
655 gobject.TYPE_STRING, # action
656 gobject.TYPE_STRING, # from
658 self._recentview = widgetTree.get_widget("recentview")
659 self._recentviewselection = None
660 self._onRecentviewRowActivatedId = 0
662 textrenderer = gtk.CellRendererText()
663 textrenderer.set_property("yalign", 0)
664 self._dateColumn = gtk.TreeViewColumn("Date")
665 self._dateColumn.pack_start(textrenderer, expand=True)
666 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
668 textrenderer = gtk.CellRendererText()
669 textrenderer.set_property("yalign", 0)
670 self._actionColumn = gtk.TreeViewColumn("Action")
671 self._actionColumn.pack_start(textrenderer, expand=True)
672 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
674 textrenderer = gtk.CellRendererText()
675 textrenderer.set_property("yalign", 0)
676 self._fromColumn = gtk.TreeViewColumn("From")
677 self._fromColumn.pack_start(textrenderer, expand=True)
678 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
679 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
681 self._window = gtk_toolbox.find_parent_window(self._recentview)
682 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
685 assert self._backend.is_authed()
686 self._recentview.set_model(self._recentmodel)
688 self._recentview.append_column(self._dateColumn)
689 self._recentview.append_column(self._actionColumn)
690 self._recentview.append_column(self._fromColumn)
691 self._recentviewselection = self._recentview.get_selection()
692 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
694 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
697 self._recentview.disconnect(self._onRecentviewRowActivatedId)
701 self._recentview.remove_column(self._dateColumn)
702 self._recentview.remove_column(self._actionColumn)
703 self._recentview.remove_column(self._fromColumn)
704 self._recentview.set_model(None)
706 def number_selected(self, action, number, message):
708 @note Actual dial function is patched in later
710 raise NotImplementedError
712 def update(self, force = False):
713 if not force and self._isPopulated:
715 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
716 backgroundPopulate.setDaemon(True)
717 backgroundPopulate.start()
720 self._isPopulated = False
721 self._recentmodel.clear()
725 return "Recent Calls"
727 def load_settings(self, config, section):
730 def save_settings(self, config, section):
732 @note Thread Agnostic
736 def _idly_populate_recentview(self):
737 self._isPopulated = True
738 self._recentmodel.clear()
741 recentItems = self._backend.get_recent()
742 except RuntimeError, e:
743 self._errorDisplay.push_exception_with_lock(e)
744 self._isPopulated = False
747 for personName, phoneNumber, date, action in recentItems:
749 personName = "Unknown"
750 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
751 prettyNumber = make_pretty(prettyNumber)
752 description = "%s - %s" % (personName, prettyNumber)
753 item = (phoneNumber, date, action.capitalize(), description)
754 with gtk_toolbox.gtk_lock():
755 self._recentmodel.append(item)
759 def _on_recentview_row_activated(self, treeview, path, view_column):
760 model, itr = self._recentviewselection.get_selected()
764 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
765 number = make_ugly(number)
766 contactPhoneNumbers = [("Phone", number)]
767 description = self._recentmodel.get_value(itr, self.FROM_IDX)
769 action, phoneNumber, message = self._phoneTypeSelector.run(
771 message = description,
772 parent = self._window,
774 if action == PhoneTypeSelector.ACTION_CANCEL:
778 self.number_selected(action, phoneNumber, message)
779 self._recentviewselection.unselect_all()
782 class MessagesView(object):
789 def __init__(self, widgetTree, backend, errorDisplay):
790 self._errorDisplay = errorDisplay
791 self._backend = backend
793 self._isPopulated = False
794 self._messagemodel = gtk.ListStore(
795 gobject.TYPE_STRING, # number
796 gobject.TYPE_STRING, # date
797 gobject.TYPE_STRING, # header
798 gobject.TYPE_STRING, # message
800 self._messageview = widgetTree.get_widget("messages_view")
801 self._messageviewselection = None
802 self._onMessageviewRowActivatedId = 0
804 textrenderer = gtk.CellRendererText()
805 textrenderer.set_property("yalign", 0)
806 self._dateColumn = gtk.TreeViewColumn("Date")
807 self._dateColumn.pack_start(textrenderer, expand=True)
808 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
810 textrenderer = gtk.CellRendererText()
811 textrenderer.set_property("yalign", 0)
812 self._headerColumn = gtk.TreeViewColumn("From")
813 self._headerColumn.pack_start(textrenderer, expand=True)
814 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
816 textrenderer = gtk.CellRendererText()
817 textrenderer.set_property("yalign", 0)
818 self._messageColumn = gtk.TreeViewColumn("Messages")
819 self._messageColumn.pack_start(textrenderer, expand=True)
820 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
821 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
823 self._window = gtk_toolbox.find_parent_window(self._messageview)
824 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
827 assert self._backend.is_authed()
828 self._messageview.set_model(self._messagemodel)
830 self._messageview.append_column(self._dateColumn)
831 self._messageview.append_column(self._headerColumn)
832 self._messageview.append_column(self._messageColumn)
833 self._messageviewselection = self._messageview.get_selection()
834 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
836 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
839 self._messageview.disconnect(self._onMessageviewRowActivatedId)
843 self._messageview.remove_column(self._dateColumn)
844 self._messageview.remove_column(self._headerColumn)
845 self._messageview.remove_column(self._messageColumn)
846 self._messageview.set_model(None)
848 def number_selected(self, action, number, message):
850 @note Actual dial function is patched in later
852 raise NotImplementedError
854 def update(self, force = False):
855 if not force and self._isPopulated:
857 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
858 backgroundPopulate.setDaemon(True)
859 backgroundPopulate.start()
862 self._isPopulated = False
863 self._messagemodel.clear()
869 def load_settings(self, config, section):
872 def save_settings(self, config, section):
874 @note Thread Agnostic
878 def _idly_populate_messageview(self):
879 self._isPopulated = True
880 self._messagemodel.clear()
883 messageItems = self._backend.get_messages()
884 except RuntimeError, e:
885 self._errorDisplay.push_exception_with_lock(e)
886 self._isPopulated = False
889 for header, number, relativeDate, message in messageItems:
890 number = make_ugly(number)
891 row = (number, relativeDate, header, message)
892 with gtk_toolbox.gtk_lock():
893 self._messagemodel.append(row)
897 def _on_messageview_row_activated(self, treeview, path, view_column):
898 model, itr = self._messageviewselection.get_selected()
902 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
903 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
905 action, phoneNumber, message = self._phoneTypeSelector.run(
907 message = description,
908 parent = self._window,
910 if action == PhoneTypeSelector.ACTION_CANCEL:
914 self.number_selected(action, phoneNumber, message)
915 self._messageviewselection.unselect_all()
918 class ContactsView(object):
920 def __init__(self, widgetTree, backend, errorDisplay):
921 self._errorDisplay = errorDisplay
922 self._backend = backend
924 self._addressBook = None
925 self._addressBookFactories = [null_backend.NullAddressBook()]
927 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
928 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
930 self._isPopulated = False
931 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
932 self._contactsviewselection = None
933 self._contactsview = widgetTree.get_widget("contactsview")
935 self._contactColumn = gtk.TreeViewColumn("Contact")
936 displayContactSource = False
937 if displayContactSource:
938 textrenderer = gtk.CellRendererText()
939 self._contactColumn.pack_start(textrenderer, expand=False)
940 self._contactColumn.add_attribute(textrenderer, 'text', 0)
941 textrenderer = gtk.CellRendererText()
942 self._contactColumn.pack_start(textrenderer, expand=True)
943 self._contactColumn.add_attribute(textrenderer, 'text', 1)
944 textrenderer = gtk.CellRendererText()
945 self._contactColumn.pack_start(textrenderer, expand=True)
946 self._contactColumn.add_attribute(textrenderer, 'text', 4)
947 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
948 self._contactColumn.set_sort_column_id(1)
949 self._contactColumn.set_visible(True)
951 self._onContactsviewRowActivatedId = 0
952 self._onAddressbookComboChangedId = 0
953 self._window = gtk_toolbox.find_parent_window(self._contactsview)
954 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
957 assert self._backend.is_authed()
959 self._contactsview.set_model(self._contactsmodel)
960 self._contactsview.append_column(self._contactColumn)
961 self._contactsviewselection = self._contactsview.get_selection()
962 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
964 self._booksList.clear()
965 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
966 if factoryName and bookName:
967 entryName = "%s: %s" % (factoryName, bookName)
969 entryName = factoryName
973 entryName = "Bad name (%d)" % factoryId
974 row = (str(factoryId), bookId, entryName)
975 self._booksList.append(row)
977 self._booksSelectionBox.set_model(self._booksList)
978 cell = gtk.CellRendererText()
979 self._booksSelectionBox.pack_start(cell, True)
980 self._booksSelectionBox.add_attribute(cell, 'text', 2)
981 self._booksSelectionBox.set_active(0)
983 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
984 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
987 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
988 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
992 self._booksSelectionBox.clear()
993 self._booksSelectionBox.set_model(None)
994 self._contactsview.set_model(None)
995 self._contactsview.remove_column(self._contactColumn)
997 def number_selected(self, action, number, message):
999 @note Actual dial function is patched in later
1001 raise NotImplementedError
1003 def get_addressbooks(self):
1005 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1007 for i, factory in enumerate(self._addressBookFactories):
1008 for bookFactory, bookId, bookName in factory.get_addressbooks():
1009 yield (i, bookId), (factory.factory_name(), bookName)
1011 def open_addressbook(self, bookFactoryId, bookId):
1012 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1013 self.update(force=True)
1015 def update(self, force = False):
1016 if not force and self._isPopulated:
1018 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1019 backgroundPopulate.setDaemon(True)
1020 backgroundPopulate.start()
1023 self._isPopulated = False
1024 self._contactsmodel.clear()
1026 def clear_caches(self):
1027 for factory in self._addressBookFactories:
1028 factory.clear_caches()
1029 self._addressBook.clear_caches()
1031 def append(self, book):
1032 self._addressBookFactories.append(book)
1034 def extend(self, books):
1035 self._addressBookFactories.extend(books)
1041 def load_settings(self, config, section):
1044 def save_settings(self, config, section):
1046 @note Thread Agnostic
1050 def _idly_populate_contactsview(self):
1051 self._isPopulated = True
1054 # completely disable updating the treeview while we populate the data
1055 self._contactsview.freeze_child_notify()
1056 self._contactsview.set_model(None)
1058 addressBook = self._addressBook
1060 contacts = addressBook.get_contacts()
1061 except RuntimeError, e:
1063 self._isPopulated = False
1064 self._errorDisplay.push_exception_with_lock(e)
1065 for contactId, contactName in contacts:
1066 contactType = (addressBook.contact_source_short_name(contactId), )
1067 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1069 # restart the treeview data rendering
1070 self._contactsview.set_model(self._contactsmodel)
1071 self._contactsview.thaw_child_notify()
1074 def _on_addressbook_combo_changed(self, *args, **kwds):
1075 itr = self._booksSelectionBox.get_active_iter()
1078 factoryId = int(self._booksList.get_value(itr, 0))
1079 bookId = self._booksList.get_value(itr, 1)
1080 self.open_addressbook(factoryId, bookId)
1082 def _on_contactsview_row_activated(self, treeview, path, view_column):
1083 model, itr = self._contactsviewselection.get_selected()
1087 contactId = self._contactsmodel.get_value(itr, 3)
1088 contactName = self._contactsmodel.get_value(itr, 1)
1090 contactDetails = self._addressBook.get_contact_details(contactId)
1091 except RuntimeError, e:
1093 self._errorDisplay.push_exception(e)
1094 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1096 if len(contactPhoneNumbers) == 0:
1099 action, phoneNumber, message = self._phoneTypeSelector.run(
1100 contactPhoneNumbers,
1101 message = contactName,
1102 parent = self._window,
1104 if action == PhoneTypeSelector.ACTION_CANCEL:
1108 self.number_selected(action, phoneNumber, message)
1109 self._contactsviewselection.unselect_all()