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
21 @todo Feature request: The ability to go to relevant thing in web browser
24 from __future__ import with_statement
36 def make_ugly(prettynumber):
38 function to take a phone number and strip out all non-numeric
41 >>> make_ugly("+012-(345)-678-90")
45 uglynumber = re.sub('\D', '', prettynumber)
49 def make_pretty(phonenumber):
51 Function to take a phone number and return the pretty version
53 if phonenumber begins with 0:
55 if phonenumber begins with 1: ( for gizmo callback numbers )
57 if phonenumber is 13 digits:
59 if phonenumber is 10 digits:
63 >>> make_pretty("1234567")
65 >>> make_pretty("2345678901")
67 >>> make_pretty("12345678901")
69 >>> make_pretty("01234567890")
72 if phonenumber is None or phonenumber is "":
75 phonenumber = make_ugly(phonenumber)
77 if len(phonenumber) < 3:
80 if phonenumber[0] == "0":
82 prettynumber += "+%s" % phonenumber[0:3]
83 if 3 < len(phonenumber):
84 prettynumber += "-(%s)" % phonenumber[3:6]
85 if 6 < len(phonenumber):
86 prettynumber += "-%s" % phonenumber[6:9]
87 if 9 < len(phonenumber):
88 prettynumber += "-%s" % phonenumber[9:]
90 elif len(phonenumber) <= 7:
91 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
92 elif len(phonenumber) > 8 and phonenumber[0] == "1":
93 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
94 elif len(phonenumber) > 7:
95 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
99 class MergedAddressBook(object):
101 Merger of all addressbooks
104 def __init__(self, addressbookFactories, sorter = None):
105 self.__addressbookFactories = addressbookFactories
106 self.__addressbooks = None
107 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
109 def clear_caches(self):
110 self.__addressbooks = None
111 for factory in self.__addressbookFactories:
112 factory.clear_caches()
114 def get_addressbooks(self):
116 @returns Iterable of (Address Book Factory, Book Id, Book Name)
120 def open_addressbook(self, bookId):
123 def contact_source_short_name(self, contactId):
124 if self.__addressbooks is None:
126 bookIndex, originalId = contactId.split("-", 1)
127 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
131 return "All Contacts"
133 def get_contacts(self):
135 @returns Iterable of (contact id, contact name)
137 if self.__addressbooks is None:
138 self.__addressbooks = list(
139 factory.open_addressbook(id)
140 for factory in self.__addressbookFactories
141 for (f, id, name) in factory.get_addressbooks()
144 ("-".join([str(bookIndex), contactId]), contactName)
145 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
146 for (contactId, contactName) in addressbook.get_contacts()
148 sortedContacts = self.__sort_contacts(contacts)
149 return sortedContacts
151 def get_contact_details(self, contactId):
153 @returns Iterable of (Phone Type, Phone Number)
155 if self.__addressbooks is None:
157 bookIndex, originalId = contactId.split("-", 1)
158 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
161 def null_sorter(contacts):
163 Good for speed/low memory
168 def basic_firtname_sorter(contacts):
170 Expects names in "First Last" format
173 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
174 for (contactId, contactName) in contacts
176 contactsWithKey.sort()
177 return (contactData for (lastName, contactData) in contactsWithKey)
180 def basic_lastname_sorter(contacts):
182 Expects names in "First Last" format
185 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
186 for (contactId, contactName) in contacts
188 contactsWithKey.sort()
189 return (contactData for (lastName, contactData) in contactsWithKey)
192 def reversed_firtname_sorter(contacts):
194 Expects names in "Last, First" format
197 (contactName.split(", ", 1)[-1], (contactId, contactName))
198 for (contactId, contactName) in contacts
200 contactsWithKey.sort()
201 return (contactData for (lastName, contactData) in contactsWithKey)
204 def reversed_lastname_sorter(contacts):
206 Expects names in "Last, First" format
209 (contactName.split(", ", 1)[0], (contactId, contactName))
210 for (contactId, contactName) in contacts
212 contactsWithKey.sort()
213 return (contactData for (lastName, contactData) in contactsWithKey)
216 def guess_firstname(name):
218 return name.split(", ", 1)[-1]
220 return name.rsplit(" ", 1)[0]
223 def guess_lastname(name):
225 return name.split(", ", 1)[0]
227 return name.rsplit(" ", 1)[-1]
230 def advanced_firstname_sorter(cls, contacts):
232 (cls.guess_firstname(contactName), (contactId, contactName))
233 for (contactId, contactName) in contacts
235 contactsWithKey.sort()
236 return (contactData for (lastName, contactData) in contactsWithKey)
239 def advanced_lastname_sorter(cls, contacts):
241 (cls.guess_lastname(contactName), (contactId, contactName))
242 for (contactId, contactName) in contacts
244 contactsWithKey.sort()
245 return (contactData for (lastName, contactData) in contactsWithKey)
248 class PhoneTypeSelector(object):
250 ACTION_CANCEL = "cancel"
251 ACTION_SELECT = "select"
253 ACTION_SEND_SMS = "sms"
255 def __init__(self, widgetTree, gcBackend):
256 self._gcBackend = gcBackend
257 self._widgetTree = widgetTree
259 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
260 self._smsDialog = SmsEntryDialog(self._widgetTree)
262 self._smsButton = self._widgetTree.get_widget("sms_button")
263 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
265 self._dialButton = self._widgetTree.get_widget("dial_button")
266 self._dialButton.connect("clicked", self._on_phonetype_dial)
268 self._selectButton = self._widgetTree.get_widget("select_button")
269 self._selectButton.connect("clicked", self._on_phonetype_select)
271 self._cancelButton = self._widgetTree.get_widget("cancel_button")
272 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
274 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
275 self._typeviewselection = None
277 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
278 self._typeview = self._widgetTree.get_widget("phonetypes")
279 self._typeview.connect("row-activated", self._on_phonetype_select)
281 self._action = self.ACTION_CANCEL
283 def run(self, contactDetails, message = "", parent = None):
284 self._action = self.ACTION_CANCEL
285 self._typemodel.clear()
286 self._typeview.set_model(self._typemodel)
288 # Add the column to the treeview
289 textrenderer = gtk.CellRendererText()
290 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
291 self._typeview.append_column(numberColumn)
293 textrenderer = gtk.CellRendererText()
294 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
295 self._typeview.append_column(typeColumn)
297 self._typeviewselection = self._typeview.get_selection()
298 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
300 for phoneType, phoneNumber in contactDetails:
301 display = " - ".join((phoneNumber, phoneType))
303 row = (phoneNumber, display)
304 self._typemodel.append(row)
306 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
308 self._message.set_markup(message)
311 self._message.set_markup("")
314 if parent is not None:
315 self._dialog.set_transient_for(parent)
318 userResponse = self._dialog.run()
322 if userResponse == gtk.RESPONSE_OK:
323 phoneNumber = self._get_number()
324 phoneNumber = make_ugly(phoneNumber)
328 self._action = self.ACTION_CANCEL
330 if self._action == self.ACTION_SEND_SMS:
331 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
334 self._action = self.ACTION_CANCEL
338 self._typeviewselection.unselect_all()
339 self._typeview.remove_column(numberColumn)
340 self._typeview.remove_column(typeColumn)
341 self._typeview.set_model(None)
343 return self._action, phoneNumber, smsMessage
345 def _get_number(self):
346 model, itr = self._typeviewselection.get_selected()
350 phoneNumber = self._typemodel.get_value(itr, 0)
353 def _on_phonetype_dial(self, *args):
354 self._dialog.response(gtk.RESPONSE_OK)
355 self._action = self.ACTION_DIAL
357 def _on_phonetype_send_sms(self, *args):
358 self._dialog.response(gtk.RESPONSE_OK)
359 self._action = self.ACTION_SEND_SMS
361 def _on_phonetype_select(self, *args):
362 self._dialog.response(gtk.RESPONSE_OK)
363 self._action = self.ACTION_SELECT
365 def _on_phonetype_cancel(self, *args):
366 self._dialog.response(gtk.RESPONSE_CANCEL)
367 self._action = self.ACTION_CANCEL
370 class SmsEntryDialog(object):
373 @todo Add multi-SMS messages like GoogleVoice
378 def __init__(self, widgetTree):
379 self._widgetTree = widgetTree
380 self._dialog = self._widgetTree.get_widget("smsDialog")
382 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
383 self._smsButton.connect("clicked", self._on_send)
385 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
386 self._cancelButton.connect("clicked", self._on_cancel)
388 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
389 self._message = self._widgetTree.get_widget("smsMessage")
390 self._smsEntry = self._widgetTree.get_widget("smsEntry")
391 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
393 def run(self, number, message = "", parent = None):
395 self._message.set_markup(message)
398 self._message.set_markup("")
400 self._smsEntry.get_buffer().set_text("")
401 self._update_letter_count()
403 if parent is not None:
404 self._dialog.set_transient_for(parent)
407 userResponse = self._dialog.run()
411 if userResponse == gtk.RESPONSE_OK:
412 entryBuffer = self._smsEntry.get_buffer()
413 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
414 enteredMessage = enteredMessage[0:self.MAX_CHAR]
418 return enteredMessage.strip()
420 def _update_letter_count(self, *args):
421 entryLength = self._smsEntry.get_buffer().get_char_count()
422 charsLeft = self.MAX_CHAR - entryLength
423 self._letterCountLabel.set_text(str(charsLeft))
425 self._smsButton.set_sensitive(False)
427 self._smsButton.set_sensitive(True)
429 def _on_entry_changed(self, *args):
430 self._update_letter_count()
432 def _on_send(self, *args):
433 self._dialog.response(gtk.RESPONSE_OK)
435 def _on_cancel(self, *args):
436 self._dialog.response(gtk.RESPONSE_CANCEL)
439 class Dialpad(object):
441 def __init__(self, widgetTree, errorDisplay):
442 self._errorDisplay = errorDisplay
443 self._smsDialog = SmsEntryDialog(widgetTree)
445 self._numberdisplay = widgetTree.get_widget("numberdisplay")
446 self._dialButton = widgetTree.get_widget("dial")
447 self._backButton = widgetTree.get_widget("back")
448 self._phonenumber = ""
449 self._prettynumber = ""
452 "on_dial_clicked": self._on_dial_clicked,
453 "on_sms_clicked": self._on_sms_clicked,
454 "on_digit_clicked": self._on_digit_clicked,
455 "on_clear_number": self._on_clear_number,
457 widgetTree.signal_autoconnect(callbackMapping)
459 self._originalLabel = self._backButton.get_label()
460 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
461 self._backTapHandler.on_tap = self._on_backspace
462 self._backTapHandler.on_hold = self._on_clearall
463 self._backTapHandler.on_holding = self._set_clear_button
464 self._backTapHandler.on_cancel = self._reset_back_button
466 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
469 self._dialButton.grab_focus()
470 self._backTapHandler.enable()
473 self._reset_back_button()
474 self._backTapHandler.disable()
476 def number_selected(self, action, number, message):
478 @note Actual dial function is patched in later
480 raise NotImplementedError("Horrible unknown error has occurred")
482 def get_number(self):
483 return self._phonenumber
485 def set_number(self, number):
487 Set the callback phonenumber
490 self._phonenumber = make_ugly(number)
491 self._prettynumber = make_pretty(self._phonenumber)
492 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
494 self._errorDisplay.push_exception()
503 def load_settings(self, config, section):
506 def save_settings(self, config, section):
508 @note Thread Agnostic
512 def _on_sms_clicked(self, widget):
513 action = PhoneTypeSelector.ACTION_SEND_SMS
514 phoneNumber = self.get_number()
516 message = self._smsDialog.run(phoneNumber, "", self._window)
519 action = PhoneTypeSelector.ACTION_CANCEL
521 if action == PhoneTypeSelector.ACTION_CANCEL:
523 self.number_selected(action, phoneNumber, message)
525 def _on_dial_clicked(self, widget):
526 action = PhoneTypeSelector.ACTION_DIAL
527 phoneNumber = self.get_number()
529 self.number_selected(action, phoneNumber, message)
531 def _on_clear_number(self, *args):
534 def _on_digit_clicked(self, widget):
535 self.set_number(self._phonenumber + widget.get_name()[-1])
537 def _on_backspace(self, taps):
538 self.set_number(self._phonenumber[:-taps])
539 self._reset_back_button()
541 def _on_clearall(self, taps):
543 self._reset_back_button()
546 def _set_clear_button(self):
547 self._backButton.set_label("gtk-clear")
549 def _reset_back_button(self):
550 self._backButton.set_label(self._originalLabel)
553 class AccountInfo(object):
555 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
556 self._errorDisplay = errorDisplay
557 self._backend = backend
558 self._isPopulated = False
559 self._alarmHandler = alarmHandler
560 self._notifyOnMissed = False
561 self._notifyOnVoicemail = False
562 self._notifyOnSms = False
564 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
565 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
566 self._callbackCombo = widgetTree.get_widget("callbackcombo")
567 self._onCallbackentryChangedId = 0
569 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
570 self._minutesEntry = widgetTree.get_widget("minutesEntry")
571 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
572 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
573 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
574 self._onNotifyToggled = 0
575 self._onMinutesChanged = 0
576 self._onMissedToggled = 0
577 self._onVoicemailToggled = 0
578 self._onSmsToggled = 0
580 self._defaultCallback = ""
583 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
585 self._accountViewNumberDisplay.set_use_markup(True)
586 self.set_account_number("")
588 self._callbackList.clear()
589 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
591 if self._alarmHandler is not None:
592 self._minutesEntry.set_range(0, 60)
593 self._minutesEntry.set_increments(1, 5)
595 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
596 self._minutesEntry.set_value(self._alarmHandler.recurrence)
597 self._missedCheckbox.set_active(self._notifyOnMissed)
598 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
599 self._smsCheckbox.set_active(self._notifyOnSms)
601 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
602 self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
603 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
604 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
605 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
607 self._notifyCheckbox.set_sensitive(False)
608 self._minutesEntry.set_sensitive(False)
609 self._missedCheckbox.set_sensitive(False)
610 self._voicemailCheckbox.set_sensitive(False)
611 self._smsCheckbox.set_sensitive(False)
613 self.update(force=True)
616 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
617 self._onCallbackentryChangedId = 0
619 if self._alarmHandler is not None:
620 self._notifyCheckbox.disconnect(self._onNotifyToggled)
621 self._minutesEntry.disconnect(self._onMinutesChanged)
622 self._missedCheckbox.disconnect(self._onNotifyToggled)
623 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
624 self._smsCheckbox.disconnect(self._onNotifyToggled)
625 self._onNotifyToggled = 0
626 self._onMinutesChanged = 0
627 self._onMissedToggled = 0
628 self._onVoicemailToggled = 0
629 self._onSmsToggled = 0
631 self._notifyCheckbox.set_sensitive(True)
632 self._minutesEntry.set_sensitive(True)
633 self._missedCheckbox.set_sensitive(True)
634 self._voicemailCheckbox.set_sensitive(True)
635 self._smsCheckbox.set_sensitive(True)
638 self._callbackList.clear()
640 def get_selected_callback_number(self):
641 return make_ugly(self._callbackCombo.get_child().get_text())
643 def set_account_number(self, number):
645 Displays current account number
647 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
649 def update(self, force = False):
650 if not force and self._isPopulated:
652 self._populate_callback_combo()
653 self.set_account_number(self._backend.get_account_number())
657 self._callbackCombo.get_child().set_text("")
658 self.set_account_number("")
659 self._isPopulated = False
661 def save_everything(self):
662 raise NotImplementedError
666 return "Account Info"
668 def load_settings(self, config, section):
669 self._defaultCallback = config.get(section, "callback")
670 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
671 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
672 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
674 def save_settings(self, config, section):
676 @note Thread Agnostic
678 callback = self.get_selected_callback_number()
679 config.set(section, "callback", callback)
680 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
681 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
682 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
684 def _populate_callback_combo(self):
685 self._isPopulated = True
686 self._callbackList.clear()
688 callbackNumbers = self._backend.get_callback_numbers()
689 except StandardError, e:
690 self._errorDisplay.push_exception()
691 self._isPopulated = False
694 for number, description in callbackNumbers.iteritems():
695 self._callbackList.append((make_pretty(number),))
697 self._callbackCombo.set_model(self._callbackList)
698 self._callbackCombo.set_text_column(0)
699 #callbackNumber = self._backend.get_callback_number()
700 callbackNumber = self._defaultCallback
701 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
703 def _set_callback_number(self, number):
705 if not self._backend.is_valid_syntax(number):
706 self._errorDisplay.push_message("%s is not a valid callback number" % number)
707 elif number == self._backend.get_callback_number():
709 "Callback number already is %s" % (
710 self._backend.get_callback_number(),
716 self._backend.set_callback_number(number)
717 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
718 make_pretty(number), make_pretty(self._backend.get_callback_number())
721 "Callback number set to %s" % (
722 self._backend.get_callback_number(),
726 except StandardError, e:
727 self._errorDisplay.push_exception()
729 def _update_alarm_settings(self):
731 isEnabled = self._notifyCheckbox.get_active()
732 recurrence = self._minutesEntry.get_value_as_int()
733 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
734 self._alarmHandler.apply_settings(isEnabled, recurrence)
736 self.save_everything()
737 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
738 self._minutesEntry.set_value(self._alarmHandler.recurrence)
740 def _on_callbackentry_changed(self, *args):
741 text = self.get_selected_callback_number()
742 number = make_ugly(text)
743 self._set_callback_number(number)
745 self.save_everything()
747 def _on_notify_toggled(self, *args):
748 self._update_alarm_settings()
750 def _on_minutes_changed(self, *args):
751 self._update_alarm_settings()
753 def _on_missed_toggled(self, *args):
754 self._notifyOnMissed = self._missedCheckbox.get_active()
755 self.save_everything()
757 def _on_voicemail_toggled(self, *args):
758 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
759 self.save_everything()
761 def _on_sms_toggled(self, *args):
762 self._notifyOnSms = self._smsCheckbox.get_active()
763 self.save_everything()
766 class RecentCallsView(object):
773 def __init__(self, widgetTree, backend, errorDisplay):
774 self._errorDisplay = errorDisplay
775 self._backend = backend
777 self._isPopulated = False
778 self._recentmodel = gtk.ListStore(
779 gobject.TYPE_STRING, # number
780 gobject.TYPE_STRING, # date
781 gobject.TYPE_STRING, # action
782 gobject.TYPE_STRING, # from
784 self._recentview = widgetTree.get_widget("recentview")
785 self._recentviewselection = None
786 self._onRecentviewRowActivatedId = 0
788 textrenderer = gtk.CellRendererText()
789 textrenderer.set_property("yalign", 0)
790 self._dateColumn = gtk.TreeViewColumn("Date")
791 self._dateColumn.pack_start(textrenderer, expand=True)
792 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
794 textrenderer = gtk.CellRendererText()
795 textrenderer.set_property("yalign", 0)
796 self._actionColumn = gtk.TreeViewColumn("Action")
797 self._actionColumn.pack_start(textrenderer, expand=True)
798 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
800 textrenderer = gtk.CellRendererText()
801 textrenderer.set_property("yalign", 0)
802 self._fromColumn = gtk.TreeViewColumn("From")
803 self._fromColumn.pack_start(textrenderer, expand=True)
804 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
805 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
807 self._window = gtk_toolbox.find_parent_window(self._recentview)
808 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
810 self._updateSink = gtk_toolbox.threaded_stage(
812 self._idly_populate_recentview,
813 gtk_toolbox.null_sink(),
818 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
819 self._recentview.set_model(self._recentmodel)
821 self._recentview.append_column(self._dateColumn)
822 self._recentview.append_column(self._actionColumn)
823 self._recentview.append_column(self._fromColumn)
824 self._recentviewselection = self._recentview.get_selection()
825 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
827 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
830 self._recentview.disconnect(self._onRecentviewRowActivatedId)
834 self._recentview.remove_column(self._dateColumn)
835 self._recentview.remove_column(self._actionColumn)
836 self._recentview.remove_column(self._fromColumn)
837 self._recentview.set_model(None)
839 def number_selected(self, action, number, message):
841 @note Actual dial function is patched in later
843 raise NotImplementedError("Horrible unknown error has occurred")
845 def update(self, force = False):
846 if not force and self._isPopulated:
848 self._updateSink.send(())
852 self._isPopulated = False
853 self._recentmodel.clear()
857 return "Recent Calls"
859 def load_settings(self, config, section):
862 def save_settings(self, config, section):
864 @note Thread Agnostic
868 def _idly_populate_recentview(self):
869 self._recentmodel.clear()
870 self._isPopulated = True
873 recentItems = self._backend.get_recent()
874 except StandardError, e:
875 self._errorDisplay.push_exception_with_lock()
876 self._isPopulated = False
879 for personName, phoneNumber, date, action in recentItems:
881 personName = "Unknown"
882 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
883 prettyNumber = make_pretty(prettyNumber)
884 description = "%s - %s" % (personName, prettyNumber)
885 item = (phoneNumber, date, action.capitalize(), description)
886 with gtk_toolbox.gtk_lock():
887 self._recentmodel.append(item)
891 def _on_recentview_row_activated(self, treeview, path, view_column):
892 model, itr = self._recentviewselection.get_selected()
896 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
897 number = make_ugly(number)
898 contactPhoneNumbers = [("Phone", number)]
899 description = self._recentmodel.get_value(itr, self.FROM_IDX)
901 action, phoneNumber, message = self._phoneTypeSelector.run(
903 message = description,
904 parent = self._window,
906 if action == PhoneTypeSelector.ACTION_CANCEL:
908 assert phoneNumber, "A lack of phone number exists"
910 self.number_selected(action, phoneNumber, message)
911 self._recentviewselection.unselect_all()
914 class MessagesView(object):
921 def __init__(self, widgetTree, backend, errorDisplay):
922 self._errorDisplay = errorDisplay
923 self._backend = backend
925 self._isPopulated = False
926 self._messagemodel = gtk.ListStore(
927 gobject.TYPE_STRING, # number
928 gobject.TYPE_STRING, # date
929 gobject.TYPE_STRING, # header
930 gobject.TYPE_STRING, # message
932 self._messageview = widgetTree.get_widget("messages_view")
933 self._messageviewselection = None
934 self._onMessageviewRowActivatedId = 0
936 textrenderer = gtk.CellRendererText()
937 textrenderer.set_property("yalign", 0)
938 self._dateColumn = gtk.TreeViewColumn("Date")
939 self._dateColumn.pack_start(textrenderer, expand=True)
940 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
942 textrenderer = gtk.CellRendererText()
943 textrenderer.set_property("yalign", 0)
944 self._headerColumn = gtk.TreeViewColumn("From")
945 self._headerColumn.pack_start(textrenderer, expand=True)
946 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
948 textrenderer = gtk.CellRendererText()
949 textrenderer.set_property("yalign", 0)
950 self._messageColumn = gtk.TreeViewColumn("Messages")
951 self._messageColumn.pack_start(textrenderer, expand=True)
952 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
953 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
955 self._window = gtk_toolbox.find_parent_window(self._messageview)
956 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
958 self._updateSink = gtk_toolbox.threaded_stage(
960 self._idly_populate_messageview,
961 gtk_toolbox.null_sink(),
966 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
967 self._messageview.set_model(self._messagemodel)
969 self._messageview.append_column(self._dateColumn)
970 self._messageview.append_column(self._headerColumn)
971 self._messageview.append_column(self._messageColumn)
972 self._messageviewselection = self._messageview.get_selection()
973 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
975 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
978 self._messageview.disconnect(self._onMessageviewRowActivatedId)
982 self._messageview.remove_column(self._dateColumn)
983 self._messageview.remove_column(self._headerColumn)
984 self._messageview.remove_column(self._messageColumn)
985 self._messageview.set_model(None)
987 def number_selected(self, action, number, message):
989 @note Actual dial function is patched in later
991 raise NotImplementedError("Horrible unknown error has occurred")
993 def update(self, force = False):
994 if not force and self._isPopulated:
996 self._updateSink.send(())
1000 self._isPopulated = False
1001 self._messagemodel.clear()
1007 def load_settings(self, config, section):
1010 def save_settings(self, config, section):
1012 @note Thread Agnostic
1016 def _idly_populate_messageview(self):
1017 self._messagemodel.clear()
1018 self._isPopulated = True
1021 messageItems = self._backend.get_messages()
1022 except StandardError, e:
1023 self._errorDisplay.push_exception_with_lock()
1024 self._isPopulated = False
1027 for header, number, relativeDate, message in messageItems:
1028 number = make_ugly(number)
1029 row = (number, relativeDate, header, message)
1030 with gtk_toolbox.gtk_lock():
1031 self._messagemodel.append(row)
1035 def _on_messageview_row_activated(self, treeview, path, view_column):
1036 model, itr = self._messageviewselection.get_selected()
1040 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1041 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1043 action, phoneNumber, message = self._phoneTypeSelector.run(
1044 contactPhoneNumbers,
1045 message = description,
1046 parent = self._window,
1048 if action == PhoneTypeSelector.ACTION_CANCEL:
1050 assert phoneNumber, "A lock of phone number exists"
1052 self.number_selected(action, phoneNumber, message)
1053 self._messageviewselection.unselect_all()
1056 class ContactsView(object):
1058 def __init__(self, widgetTree, backend, errorDisplay):
1059 self._errorDisplay = errorDisplay
1060 self._backend = backend
1062 self._addressBook = None
1063 self._addressBookFactories = [null_backend.NullAddressBook()]
1065 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1066 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1068 self._isPopulated = False
1069 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1070 self._contactsviewselection = None
1071 self._contactsview = widgetTree.get_widget("contactsview")
1073 self._contactColumn = gtk.TreeViewColumn("Contact")
1074 displayContactSource = False
1075 if displayContactSource:
1076 textrenderer = gtk.CellRendererText()
1077 self._contactColumn.pack_start(textrenderer, expand=False)
1078 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1079 textrenderer = gtk.CellRendererText()
1080 self._contactColumn.pack_start(textrenderer, expand=True)
1081 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1082 textrenderer = gtk.CellRendererText()
1083 self._contactColumn.pack_start(textrenderer, expand=True)
1084 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1085 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1086 self._contactColumn.set_sort_column_id(1)
1087 self._contactColumn.set_visible(True)
1089 self._onContactsviewRowActivatedId = 0
1090 self._onAddressbookComboChangedId = 0
1091 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1092 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1094 self._updateSink = gtk_toolbox.threaded_stage(
1096 self._idly_populate_contactsview,
1097 gtk_toolbox.null_sink(),
1102 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1104 self._contactsview.set_model(self._contactsmodel)
1105 self._contactsview.append_column(self._contactColumn)
1106 self._contactsviewselection = self._contactsview.get_selection()
1107 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1109 self._booksList.clear()
1110 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1111 if factoryName and bookName:
1112 entryName = "%s: %s" % (factoryName, bookName)
1114 entryName = factoryName
1116 entryName = bookName
1118 entryName = "Bad name (%d)" % factoryId
1119 row = (str(factoryId), bookId, entryName)
1120 self._booksList.append(row)
1122 self._booksSelectionBox.set_model(self._booksList)
1123 cell = gtk.CellRendererText()
1124 self._booksSelectionBox.pack_start(cell, True)
1125 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1126 self._booksSelectionBox.set_active(0)
1128 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1129 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1132 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1133 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1137 self._booksSelectionBox.clear()
1138 self._booksSelectionBox.set_model(None)
1139 self._contactsview.set_model(None)
1140 self._contactsview.remove_column(self._contactColumn)
1142 def number_selected(self, action, number, message):
1144 @note Actual dial function is patched in later
1146 raise NotImplementedError("Horrible unknown error has occurred")
1148 def get_addressbooks(self):
1150 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1152 for i, factory in enumerate(self._addressBookFactories):
1153 for bookFactory, bookId, bookName in factory.get_addressbooks():
1154 yield (i, bookId), (factory.factory_name(), bookName)
1156 def open_addressbook(self, bookFactoryId, bookId):
1157 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1158 self.update(force=True)
1160 def update(self, force = False):
1161 if not force and self._isPopulated:
1163 self._updateSink.send(())
1167 self._isPopulated = False
1168 self._contactsmodel.clear()
1169 for factory in self._addressBookFactories:
1170 factory.clear_caches()
1171 self._addressBook.clear_caches()
1173 def append(self, book):
1174 self._addressBookFactories.append(book)
1176 def extend(self, books):
1177 self._addressBookFactories.extend(books)
1183 def load_settings(self, config, section):
1186 def save_settings(self, config, section):
1188 @note Thread Agnostic
1192 def _idly_populate_contactsview(self):
1194 self._isPopulated = True
1196 # completely disable updating the treeview while we populate the data
1197 self._contactsview.freeze_child_notify()
1199 self._contactsview.set_model(None)
1201 addressBook = self._addressBook
1203 contacts = addressBook.get_contacts()
1204 except StandardError, e:
1206 self._isPopulated = False
1207 self._errorDisplay.push_exception_with_lock()
1208 for contactId, contactName in contacts:
1209 contactType = (addressBook.contact_source_short_name(contactId), )
1210 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1212 # restart the treeview data rendering
1213 self._contactsview.set_model(self._contactsmodel)
1215 self._contactsview.thaw_child_notify()
1218 def _on_addressbook_combo_changed(self, *args, **kwds):
1219 itr = self._booksSelectionBox.get_active_iter()
1222 factoryId = int(self._booksList.get_value(itr, 0))
1223 bookId = self._booksList.get_value(itr, 1)
1224 self.open_addressbook(factoryId, bookId)
1226 def _on_contactsview_row_activated(self, treeview, path, view_column):
1227 model, itr = self._contactsviewselection.get_selected()
1231 contactId = self._contactsmodel.get_value(itr, 3)
1232 contactName = self._contactsmodel.get_value(itr, 1)
1234 contactDetails = self._addressBook.get_contact_details(contactId)
1235 except StandardError, e:
1237 self._errorDisplay.push_exception()
1238 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1240 if len(contactPhoneNumbers) == 0:
1243 action, phoneNumber, message = self._phoneTypeSelector.run(
1244 contactPhoneNumbers,
1245 message = contactName,
1246 parent = self._window,
1248 if action == PhoneTypeSelector.ACTION_CANCEL:
1250 assert phoneNumber, "A lack of phone number exists"
1252 self.number_selected(action, phoneNumber, message)
1253 self._contactsviewselection.unselect_all()