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 description = "%s (%s)" % (phoneNumber, personName)
751 item = (phoneNumber, date, action.capitalize(), description)
752 with gtk_toolbox.gtk_lock():
753 self._recentmodel.append(item)
757 def _on_recentview_row_activated(self, treeview, path, view_column):
758 model, itr = self._recentviewselection.get_selected()
762 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
763 number = make_ugly(number)
764 contactPhoneNumbers = [("Phone", number)]
765 description = self._recentmodel.get_value(itr, self.FROM_IDX)
767 action, phoneNumber, message = self._phoneTypeSelector.run(
769 message = description,
770 parent = self._window,
772 if action == PhoneTypeSelector.ACTION_CANCEL:
776 self.number_selected(action, phoneNumber, message)
777 self._recentviewselection.unselect_all()
780 class MessagesView(object):
787 def __init__(self, widgetTree, backend, errorDisplay):
788 self._errorDisplay = errorDisplay
789 self._backend = backend
791 self._isPopulated = False
792 self._messagemodel = gtk.ListStore(
793 gobject.TYPE_STRING, # number
794 gobject.TYPE_STRING, # date
795 gobject.TYPE_STRING, # header
796 gobject.TYPE_STRING, # message
798 self._messageview = widgetTree.get_widget("messages_view")
799 self._messageviewselection = None
800 self._onMessageviewRowActivatedId = 0
802 textrenderer = gtk.CellRendererText()
803 textrenderer.set_property("yalign", 0)
804 self._dateColumn = gtk.TreeViewColumn("Date")
805 self._dateColumn.pack_start(textrenderer, expand=True)
806 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
808 textrenderer = gtk.CellRendererText()
809 textrenderer.set_property("yalign", 0)
810 self._headerColumn = gtk.TreeViewColumn("From")
811 self._headerColumn.pack_start(textrenderer, expand=True)
812 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
814 textrenderer = gtk.CellRendererText()
815 textrenderer.set_property("yalign", 0)
816 self._messageColumn = gtk.TreeViewColumn("Messages")
817 self._messageColumn.pack_start(textrenderer, expand=True)
818 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
819 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
821 self._window = gtk_toolbox.find_parent_window(self._messageview)
822 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
825 assert self._backend.is_authed()
826 self._messageview.set_model(self._messagemodel)
828 self._messageview.append_column(self._dateColumn)
829 self._messageview.append_column(self._headerColumn)
830 self._messageview.append_column(self._messageColumn)
831 self._messageviewselection = self._messageview.get_selection()
832 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
834 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
837 self._messageview.disconnect(self._onMessageviewRowActivatedId)
841 self._messageview.remove_column(self._dateColumn)
842 self._messageview.remove_column(self._headerColumn)
843 self._messageview.remove_column(self._messageColumn)
844 self._messageview.set_model(None)
846 def number_selected(self, action, number, message):
848 @note Actual dial function is patched in later
850 raise NotImplementedError
852 def update(self, force = False):
853 if not force and self._isPopulated:
855 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
856 backgroundPopulate.setDaemon(True)
857 backgroundPopulate.start()
860 self._isPopulated = False
861 self._messagemodel.clear()
867 def load_settings(self, config, section):
870 def save_settings(self, config, section):
872 @note Thread Agnostic
876 def _idly_populate_messageview(self):
877 self._isPopulated = True
878 self._messagemodel.clear()
881 messageItems = self._backend.get_messages()
882 except RuntimeError, e:
883 self._errorDisplay.push_exception_with_lock(e)
884 self._isPopulated = False
887 for header, number, relativeDate, message in messageItems:
888 number = make_ugly(number)
889 row = (number, relativeDate, header, message)
890 with gtk_toolbox.gtk_lock():
891 self._messagemodel.append(row)
895 def _on_messageview_row_activated(self, treeview, path, view_column):
896 model, itr = self._messageviewselection.get_selected()
900 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
901 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
903 action, phoneNumber, message = self._phoneTypeSelector.run(
905 message = description,
906 parent = self._window,
908 if action == PhoneTypeSelector.ACTION_CANCEL:
912 self.number_selected(action, phoneNumber, message)
913 self._messageviewselection.unselect_all()
916 class ContactsView(object):
918 def __init__(self, widgetTree, backend, errorDisplay):
919 self._errorDisplay = errorDisplay
920 self._backend = backend
922 self._addressBook = None
923 self._addressBookFactories = [null_backend.NullAddressBook()]
925 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
926 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
928 self._isPopulated = False
929 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
930 self._contactsviewselection = None
931 self._contactsview = widgetTree.get_widget("contactsview")
933 self._contactColumn = gtk.TreeViewColumn("Contact")
934 displayContactSource = False
935 if displayContactSource:
936 textrenderer = gtk.CellRendererText()
937 self._contactColumn.pack_start(textrenderer, expand=False)
938 self._contactColumn.add_attribute(textrenderer, 'text', 0)
939 textrenderer = gtk.CellRendererText()
940 self._contactColumn.pack_start(textrenderer, expand=True)
941 self._contactColumn.add_attribute(textrenderer, 'text', 1)
942 textrenderer = gtk.CellRendererText()
943 self._contactColumn.pack_start(textrenderer, expand=True)
944 self._contactColumn.add_attribute(textrenderer, 'text', 4)
945 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
946 self._contactColumn.set_sort_column_id(1)
947 self._contactColumn.set_visible(True)
949 self._onContactsviewRowActivatedId = 0
950 self._onAddressbookComboChangedId = 0
951 self._window = gtk_toolbox.find_parent_window(self._contactsview)
952 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
955 assert self._backend.is_authed()
957 self._contactsview.set_model(self._contactsmodel)
958 self._contactsview.append_column(self._contactColumn)
959 self._contactsviewselection = self._contactsview.get_selection()
960 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
962 self._booksList.clear()
963 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
964 if factoryName and bookName:
965 entryName = "%s: %s" % (factoryName, bookName)
967 entryName = factoryName
971 entryName = "Bad name (%d)" % factoryId
972 row = (str(factoryId), bookId, entryName)
973 self._booksList.append(row)
975 self._booksSelectionBox.set_model(self._booksList)
976 cell = gtk.CellRendererText()
977 self._booksSelectionBox.pack_start(cell, True)
978 self._booksSelectionBox.add_attribute(cell, 'text', 2)
979 self._booksSelectionBox.set_active(0)
981 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
982 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
985 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
986 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
990 self._booksSelectionBox.clear()
991 self._booksSelectionBox.set_model(None)
992 self._contactsview.set_model(None)
993 self._contactsview.remove_column(self._contactColumn)
995 def number_selected(self, action, number, message):
997 @note Actual dial function is patched in later
999 raise NotImplementedError
1001 def get_addressbooks(self):
1003 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1005 for i, factory in enumerate(self._addressBookFactories):
1006 for bookFactory, bookId, bookName in factory.get_addressbooks():
1007 yield (i, bookId), (factory.factory_name(), bookName)
1009 def open_addressbook(self, bookFactoryId, bookId):
1010 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1011 self.update(force=True)
1013 def update(self, force = False):
1014 if not force and self._isPopulated:
1016 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1017 backgroundPopulate.setDaemon(True)
1018 backgroundPopulate.start()
1021 self._isPopulated = False
1022 self._contactsmodel.clear()
1024 def clear_caches(self):
1025 for factory in self._addressBookFactories:
1026 factory.clear_caches()
1027 self._addressBook.clear_caches()
1029 def append(self, book):
1030 self._addressBookFactories.append(book)
1032 def extend(self, books):
1033 self._addressBookFactories.extend(books)
1039 def load_settings(self, config, section):
1042 def save_settings(self, config, section):
1044 @note Thread Agnostic
1048 def _idly_populate_contactsview(self):
1049 self._isPopulated = True
1052 # completely disable updating the treeview while we populate the data
1053 self._contactsview.freeze_child_notify()
1054 self._contactsview.set_model(None)
1056 addressBook = self._addressBook
1058 contacts = addressBook.get_contacts()
1059 except RuntimeError, e:
1061 self._isPopulated = False
1062 self._errorDisplay.push_exception_with_lock(e)
1063 for contactId, contactName in contacts:
1064 contactType = (addressBook.contact_source_short_name(contactId), )
1065 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1067 # restart the treeview data rendering
1068 self._contactsview.set_model(self._contactsmodel)
1069 self._contactsview.thaw_child_notify()
1072 def _on_addressbook_combo_changed(self, *args, **kwds):
1073 itr = self._booksSelectionBox.get_active_iter()
1076 factoryId = int(self._booksList.get_value(itr, 0))
1077 bookId = self._booksList.get_value(itr, 1)
1078 self.open_addressbook(factoryId, bookId)
1080 def _on_contactsview_row_activated(self, treeview, path, view_column):
1081 model, itr = self._contactsviewselection.get_selected()
1085 contactId = self._contactsmodel.get_value(itr, 3)
1086 contactName = self._contactsmodel.get_value(itr, 1)
1088 contactDetails = self._addressBook.get_contact_details(contactId)
1089 except RuntimeError, e:
1091 self._errorDisplay.push_exception(e)
1092 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1094 if len(contactPhoneNumbers) == 0:
1097 action, phoneNumber, message = self._phoneTypeSelector.run(
1098 contactPhoneNumbers,
1099 message = contactName,
1100 parent = self._window,
1102 if action == PhoneTypeSelector.ACTION_CANCEL:
1106 self.number_selected(action, phoneNumber, message)
1107 self._contactsviewselection.unselect_all()