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._backButton = widgetTree.get_widget("back")
446 self._phonenumber = ""
447 self._prettynumber = ""
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,
455 widgetTree.signal_autoconnect(callbackMapping)
457 self._originalLabel = self._backButton.get_label()
458 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
459 self._backTapHandler.on_tap = self._on_backspace
460 self._backTapHandler.on_hold = self._on_clearall
461 self._backTapHandler.on_holding = self._set_clear_button
462 self._backTapHandler.on_cancel = self._reset_back_button
464 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
467 self._dialButton.grab_focus()
468 self._backTapHandler.enable()
471 self._reset_back_button()
472 self._backTapHandler.disable()
474 def number_selected(self, action, number, message):
476 @note Actual dial function is patched in later
478 raise NotImplementedError("Horrible unknown error has occurred")
480 def get_number(self):
481 return self._phonenumber
483 def set_number(self, number):
485 Set the callback phonenumber
488 self._phonenumber = make_ugly(number)
489 self._prettynumber = make_pretty(self._phonenumber)
490 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
492 self._errorDisplay.push_exception(e)
501 def load_settings(self, config, section):
504 def save_settings(self, config, section):
506 @note Thread Agnostic
510 def _on_sms_clicked(self, widget):
511 action = PhoneTypeSelector.ACTION_SEND_SMS
512 phoneNumber = self.get_number()
514 message = self._smsDialog.run(phoneNumber, "", self._window)
517 action = PhoneTypeSelector.ACTION_CANCEL
519 if action == PhoneTypeSelector.ACTION_CANCEL:
521 self.number_selected(action, phoneNumber, message)
523 def _on_dial_clicked(self, widget):
524 action = PhoneTypeSelector.ACTION_DIAL
525 phoneNumber = self.get_number()
527 self.number_selected(action, phoneNumber, message)
529 def _on_clear_number(self, *args):
532 def _on_digit_clicked(self, widget):
533 self.set_number(self._phonenumber + widget.get_name()[-1])
535 def _on_backspace(self, taps):
536 self.set_number(self._phonenumber[:-taps])
537 self._reset_back_button()
539 def _on_clearall(self, taps):
541 self._reset_back_button()
544 def _set_clear_button(self):
545 self._backButton.set_label("gtk-clear")
547 def _reset_back_button(self):
548 self._backButton.set_label(self._originalLabel)
551 class AccountInfo(object):
553 def __init__(self, widgetTree, backend, errorDisplay):
554 self._errorDisplay = errorDisplay
555 self._backend = backend
556 self._isPopulated = False
558 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
559 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
560 self._callbackCombo = widgetTree.get_widget("callbackcombo")
561 self._onCallbackentryChangedId = 0
563 self._defaultCallback = ""
566 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
567 self._accountViewNumberDisplay.set_use_markup(True)
568 self.set_account_number("")
569 self._callbackList.clear()
570 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
571 self.update(force=True)
574 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
578 self._callbackList.clear()
580 def get_selected_callback_number(self):
581 return make_ugly(self._callbackCombo.get_child().get_text())
583 def set_account_number(self, number):
585 Displays current account number
587 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
589 def update(self, force = False):
590 if not force and self._isPopulated:
592 self._populate_callback_combo()
593 self.set_account_number(self._backend.get_account_number())
596 self._callbackCombo.get_child().set_text("")
597 self.set_account_number("")
598 self._isPopulated = False
602 return "Account Info"
604 def load_settings(self, config, section):
605 self._defaultCallback = config.get(section, "callback")
607 def save_settings(self, config, section):
609 @note Thread Agnostic
611 callback = self.get_selected_callback_number()
612 config.set(section, "callback", callback)
614 def _populate_callback_combo(self):
615 self._isPopulated = True
616 self._callbackList.clear()
618 callbackNumbers = self._backend.get_callback_numbers()
619 except StandardError, e:
620 self._errorDisplay.push_exception(e)
621 self._isPopulated = False
624 for number, description in callbackNumbers.iteritems():
625 self._callbackList.append((make_pretty(number),))
627 self._callbackCombo.set_model(self._callbackList)
628 self._callbackCombo.set_text_column(0)
629 #callbackNumber = self._backend.get_callback_number()
630 callbackNumber = self._defaultCallback
631 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
633 def _set_callback_number(self, number):
635 if not self._backend.is_valid_syntax(number):
636 self._errorDisplay.push_message("%s is not a valid callback number" % number)
637 elif number == self._backend.get_callback_number():
639 "Callback number already is %s" % (
640 self._backend.get_callback_number(),
646 self._backend.set_callback_number(number)
647 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
648 make_pretty(number), make_pretty(self._backend.get_callback_number())
651 "Callback number set to %s" % (
652 self._backend.get_callback_number(),
656 except StandardError, e:
657 self._errorDisplay.push_exception(e)
659 def _on_callbackentry_changed(self, *args):
660 text = self.get_selected_callback_number()
661 number = make_ugly(text)
662 self._set_callback_number(number)
665 class RecentCallsView(object):
672 def __init__(self, widgetTree, backend, errorDisplay):
673 self._errorDisplay = errorDisplay
674 self._backend = backend
676 self._isPopulated = False
677 self._recentmodel = gtk.ListStore(
678 gobject.TYPE_STRING, # number
679 gobject.TYPE_STRING, # date
680 gobject.TYPE_STRING, # action
681 gobject.TYPE_STRING, # from
683 self._recentview = widgetTree.get_widget("recentview")
684 self._recentviewselection = None
685 self._onRecentviewRowActivatedId = 0
687 textrenderer = gtk.CellRendererText()
688 textrenderer.set_property("yalign", 0)
689 self._dateColumn = gtk.TreeViewColumn("Date")
690 self._dateColumn.pack_start(textrenderer, expand=True)
691 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
693 textrenderer = gtk.CellRendererText()
694 textrenderer.set_property("yalign", 0)
695 self._actionColumn = gtk.TreeViewColumn("Action")
696 self._actionColumn.pack_start(textrenderer, expand=True)
697 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
699 textrenderer = gtk.CellRendererText()
700 textrenderer.set_property("yalign", 0)
701 self._fromColumn = gtk.TreeViewColumn("From")
702 self._fromColumn.pack_start(textrenderer, expand=True)
703 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
704 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
706 self._window = gtk_toolbox.find_parent_window(self._recentview)
707 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
709 self._updateSink = gtk_toolbox.threaded_stage(
711 self._idly_populate_recentview,
712 gtk_toolbox.null_sink(),
717 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
718 self._recentview.set_model(self._recentmodel)
720 self._recentview.append_column(self._dateColumn)
721 self._recentview.append_column(self._actionColumn)
722 self._recentview.append_column(self._fromColumn)
723 self._recentviewselection = self._recentview.get_selection()
724 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
726 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
729 self._recentview.disconnect(self._onRecentviewRowActivatedId)
733 self._recentview.remove_column(self._dateColumn)
734 self._recentview.remove_column(self._actionColumn)
735 self._recentview.remove_column(self._fromColumn)
736 self._recentview.set_model(None)
738 def number_selected(self, action, number, message):
740 @note Actual dial function is patched in later
742 raise NotImplementedError("Horrible unknown error has occurred")
744 def update(self, force = False):
745 if not force and self._isPopulated:
747 self._updateSink.send(())
750 self._isPopulated = False
751 self._recentmodel.clear()
755 return "Recent Calls"
757 def load_settings(self, config, section):
760 def save_settings(self, config, section):
762 @note Thread Agnostic
766 def _idly_populate_recentview(self):
767 self._recentmodel.clear()
768 self._isPopulated = True
771 recentItems = self._backend.get_recent()
772 except StandardError, e:
773 self._errorDisplay.push_exception_with_lock(e)
774 self._isPopulated = False
777 for personName, phoneNumber, date, action in recentItems:
779 personName = "Unknown"
780 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
781 prettyNumber = make_pretty(prettyNumber)
782 description = "%s - %s" % (personName, prettyNumber)
783 item = (phoneNumber, date, action.capitalize(), description)
784 with gtk_toolbox.gtk_lock():
785 self._recentmodel.append(item)
789 def _on_recentview_row_activated(self, treeview, path, view_column):
790 model, itr = self._recentviewselection.get_selected()
794 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
795 number = make_ugly(number)
796 contactPhoneNumbers = [("Phone", number)]
797 description = self._recentmodel.get_value(itr, self.FROM_IDX)
799 action, phoneNumber, message = self._phoneTypeSelector.run(
801 message = description,
802 parent = self._window,
804 if action == PhoneTypeSelector.ACTION_CANCEL:
806 assert phoneNumber, "A lack of phone number exists"
808 self.number_selected(action, phoneNumber, message)
809 self._recentviewselection.unselect_all()
812 class MessagesView(object):
819 def __init__(self, widgetTree, backend, errorDisplay):
820 self._errorDisplay = errorDisplay
821 self._backend = backend
823 self._isPopulated = False
824 self._messagemodel = gtk.ListStore(
825 gobject.TYPE_STRING, # number
826 gobject.TYPE_STRING, # date
827 gobject.TYPE_STRING, # header
828 gobject.TYPE_STRING, # message
830 self._messageview = widgetTree.get_widget("messages_view")
831 self._messageviewselection = None
832 self._onMessageviewRowActivatedId = 0
834 textrenderer = gtk.CellRendererText()
835 textrenderer.set_property("yalign", 0)
836 self._dateColumn = gtk.TreeViewColumn("Date")
837 self._dateColumn.pack_start(textrenderer, expand=True)
838 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
840 textrenderer = gtk.CellRendererText()
841 textrenderer.set_property("yalign", 0)
842 self._headerColumn = gtk.TreeViewColumn("From")
843 self._headerColumn.pack_start(textrenderer, expand=True)
844 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
846 textrenderer = gtk.CellRendererText()
847 textrenderer.set_property("yalign", 0)
848 self._messageColumn = gtk.TreeViewColumn("Messages")
849 self._messageColumn.pack_start(textrenderer, expand=True)
850 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
851 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
853 self._window = gtk_toolbox.find_parent_window(self._messageview)
854 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
856 self._updateSink = gtk_toolbox.threaded_stage(
858 self._idly_populate_messageview,
859 gtk_toolbox.null_sink(),
864 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
865 self._messageview.set_model(self._messagemodel)
867 self._messageview.append_column(self._dateColumn)
868 self._messageview.append_column(self._headerColumn)
869 self._messageview.append_column(self._messageColumn)
870 self._messageviewselection = self._messageview.get_selection()
871 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
873 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
876 self._messageview.disconnect(self._onMessageviewRowActivatedId)
880 self._messageview.remove_column(self._dateColumn)
881 self._messageview.remove_column(self._headerColumn)
882 self._messageview.remove_column(self._messageColumn)
883 self._messageview.set_model(None)
885 def number_selected(self, action, number, message):
887 @note Actual dial function is patched in later
889 raise NotImplementedError("Horrible unknown error has occurred")
891 def update(self, force = False):
892 if not force and self._isPopulated:
894 self._updateSink.send(())
897 self._isPopulated = False
898 self._messagemodel.clear()
904 def load_settings(self, config, section):
907 def save_settings(self, config, section):
909 @note Thread Agnostic
913 def _idly_populate_messageview(self):
914 self._messagemodel.clear()
915 self._isPopulated = True
918 messageItems = self._backend.get_messages()
919 except StandardError, e:
920 self._errorDisplay.push_exception_with_lock(e)
921 self._isPopulated = False
924 for header, number, relativeDate, message in messageItems:
925 number = make_ugly(number)
926 row = (number, relativeDate, header, message)
927 with gtk_toolbox.gtk_lock():
928 self._messagemodel.append(row)
932 def _on_messageview_row_activated(self, treeview, path, view_column):
933 model, itr = self._messageviewselection.get_selected()
937 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
938 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
940 action, phoneNumber, message = self._phoneTypeSelector.run(
942 message = description,
943 parent = self._window,
945 if action == PhoneTypeSelector.ACTION_CANCEL:
947 assert phoneNumber, "A lock of phone number exists"
949 self.number_selected(action, phoneNumber, message)
950 self._messageviewselection.unselect_all()
953 class ContactsView(object):
955 def __init__(self, widgetTree, backend, errorDisplay):
956 self._errorDisplay = errorDisplay
957 self._backend = backend
959 self._addressBook = None
960 self._addressBookFactories = [null_backend.NullAddressBook()]
962 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
963 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
965 self._isPopulated = False
966 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
967 self._contactsviewselection = None
968 self._contactsview = widgetTree.get_widget("contactsview")
970 self._contactColumn = gtk.TreeViewColumn("Contact")
971 displayContactSource = False
972 if displayContactSource:
973 textrenderer = gtk.CellRendererText()
974 self._contactColumn.pack_start(textrenderer, expand=False)
975 self._contactColumn.add_attribute(textrenderer, 'text', 0)
976 textrenderer = gtk.CellRendererText()
977 self._contactColumn.pack_start(textrenderer, expand=True)
978 self._contactColumn.add_attribute(textrenderer, 'text', 1)
979 textrenderer = gtk.CellRendererText()
980 self._contactColumn.pack_start(textrenderer, expand=True)
981 self._contactColumn.add_attribute(textrenderer, 'text', 4)
982 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
983 self._contactColumn.set_sort_column_id(1)
984 self._contactColumn.set_visible(True)
986 self._onContactsviewRowActivatedId = 0
987 self._onAddressbookComboChangedId = 0
988 self._window = gtk_toolbox.find_parent_window(self._contactsview)
989 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
991 self._updateSink = gtk_toolbox.threaded_stage(
993 self._idly_populate_contactsview,
994 gtk_toolbox.null_sink(),
999 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1001 self._contactsview.set_model(self._contactsmodel)
1002 self._contactsview.append_column(self._contactColumn)
1003 self._contactsviewselection = self._contactsview.get_selection()
1004 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1006 self._booksList.clear()
1007 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1008 if factoryName and bookName:
1009 entryName = "%s: %s" % (factoryName, bookName)
1011 entryName = factoryName
1013 entryName = bookName
1015 entryName = "Bad name (%d)" % factoryId
1016 row = (str(factoryId), bookId, entryName)
1017 self._booksList.append(row)
1019 self._booksSelectionBox.set_model(self._booksList)
1020 cell = gtk.CellRendererText()
1021 self._booksSelectionBox.pack_start(cell, True)
1022 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1023 self._booksSelectionBox.set_active(0)
1025 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1026 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1029 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1030 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1034 self._booksSelectionBox.clear()
1035 self._booksSelectionBox.set_model(None)
1036 self._contactsview.set_model(None)
1037 self._contactsview.remove_column(self._contactColumn)
1039 def number_selected(self, action, number, message):
1041 @note Actual dial function is patched in later
1043 raise NotImplementedError("Horrible unknown error has occurred")
1045 def get_addressbooks(self):
1047 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1049 for i, factory in enumerate(self._addressBookFactories):
1050 for bookFactory, bookId, bookName in factory.get_addressbooks():
1051 yield (i, bookId), (factory.factory_name(), bookName)
1053 def open_addressbook(self, bookFactoryId, bookId):
1054 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1055 self.update(force=True)
1057 def update(self, force = False):
1058 if not force and self._isPopulated:
1060 self._updateSink.send(())
1063 self._isPopulated = False
1064 self._contactsmodel.clear()
1065 for factory in self._addressBookFactories:
1066 factory.clear_caches()
1067 self._addressBook.clear_caches()
1069 def append(self, book):
1070 self._addressBookFactories.append(book)
1072 def extend(self, books):
1073 self._addressBookFactories.extend(books)
1079 def load_settings(self, config, section):
1082 def save_settings(self, config, section):
1084 @note Thread Agnostic
1088 def _idly_populate_contactsview(self):
1090 self._isPopulated = True
1092 # completely disable updating the treeview while we populate the data
1093 self._contactsview.freeze_child_notify()
1095 self._contactsview.set_model(None)
1097 addressBook = self._addressBook
1099 contacts = addressBook.get_contacts()
1100 except StandardError, e:
1102 self._isPopulated = False
1103 self._errorDisplay.push_exception_with_lock(e)
1104 for contactId, contactName in contacts:
1105 contactType = (addressBook.contact_source_short_name(contactId), )
1106 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1108 # restart the treeview data rendering
1109 self._contactsview.set_model(self._contactsmodel)
1111 self._contactsview.thaw_child_notify()
1114 def _on_addressbook_combo_changed(self, *args, **kwds):
1115 itr = self._booksSelectionBox.get_active_iter()
1118 factoryId = int(self._booksList.get_value(itr, 0))
1119 bookId = self._booksList.get_value(itr, 1)
1120 self.open_addressbook(factoryId, bookId)
1122 def _on_contactsview_row_activated(self, treeview, path, view_column):
1123 model, itr = self._contactsviewselection.get_selected()
1127 contactId = self._contactsmodel.get_value(itr, 3)
1128 contactName = self._contactsmodel.get_value(itr, 1)
1130 contactDetails = self._addressBook.get_contact_details(contactId)
1131 except StandardError, e:
1133 self._errorDisplay.push_exception(e)
1134 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1136 if len(contactPhoneNumbers) == 0:
1139 action, phoneNumber, message = self._phoneTypeSelector.run(
1140 contactPhoneNumbers,
1141 message = contactName,
1142 parent = self._window,
1144 if action == PhoneTypeSelector.ACTION_CANCEL:
1146 assert phoneNumber, "A lack of phone number exists"
1148 self.number_selected(action, phoneNumber, message)
1149 self._contactsviewselection.unselect_all()