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
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._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
279 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
280 self._typeview = self._widgetTree.get_widget("phonetypes")
281 self._typeview.connect("row-activated", self._on_phonetype_select)
283 self._action = self.ACTION_CANCEL
285 def run(self, contactDetails, message = "", parent = None):
286 self._action = self.ACTION_CANCEL
287 self._typemodel.clear()
288 self._typeview.set_model(self._typemodel)
290 # Add the column to the treeview
291 textrenderer = gtk.CellRendererText()
292 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
293 self._typeview.append_column(numberColumn)
295 textrenderer = gtk.CellRendererText()
296 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
297 self._typeview.append_column(typeColumn)
299 self._typeviewselection = self._typeview.get_selection()
300 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
302 for phoneType, phoneNumber in contactDetails:
303 display = " - ".join((phoneNumber, phoneType))
305 row = (phoneNumber, display)
306 self._typemodel.append(row)
308 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
310 self._message.set_markup(message)
313 self._message.set_markup("")
316 if parent is not None:
317 self._dialog.set_transient_for(parent)
321 adjustment = self._scrollWindow.get_vadjustment()
322 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
324 adjustment.value = dx
326 userResponse = self._dialog.run()
330 if userResponse == gtk.RESPONSE_OK:
331 phoneNumber = self._get_number()
332 phoneNumber = make_ugly(phoneNumber)
336 self._action = self.ACTION_CANCEL
338 if self._action == self.ACTION_SEND_SMS:
339 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
342 self._action = self.ACTION_CANCEL
346 self._typeviewselection.unselect_all()
347 self._typeview.remove_column(numberColumn)
348 self._typeview.remove_column(typeColumn)
349 self._typeview.set_model(None)
351 return self._action, phoneNumber, smsMessage
353 def _get_number(self):
354 model, itr = self._typeviewselection.get_selected()
358 phoneNumber = self._typemodel.get_value(itr, 0)
361 def _on_phonetype_dial(self, *args):
362 self._dialog.response(gtk.RESPONSE_OK)
363 self._action = self.ACTION_DIAL
365 def _on_phonetype_send_sms(self, *args):
366 self._dialog.response(gtk.RESPONSE_OK)
367 self._action = self.ACTION_SEND_SMS
369 def _on_phonetype_select(self, *args):
370 self._dialog.response(gtk.RESPONSE_OK)
371 self._action = self.ACTION_SELECT
373 def _on_phonetype_cancel(self, *args):
374 self._dialog.response(gtk.RESPONSE_CANCEL)
375 self._action = self.ACTION_CANCEL
378 class SmsEntryDialog(object):
381 @todo Add multi-SMS messages like GoogleVoice
386 def __init__(self, widgetTree):
387 self._widgetTree = widgetTree
388 self._dialog = self._widgetTree.get_widget("smsDialog")
390 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
391 self._smsButton.connect("clicked", self._on_send)
393 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
394 self._cancelButton.connect("clicked", self._on_cancel)
396 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
397 self._message = self._widgetTree.get_widget("smsMessage")
398 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
399 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
400 self._smsEntry = self._widgetTree.get_widget("smsEntry")
401 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
403 def run(self, number, message = "", parent = None):
405 self._message.set_markup(message)
408 self._message.set_markup("")
410 self._smsEntry.get_buffer().set_text("")
411 self._update_letter_count()
413 if parent is not None:
414 self._dialog.set_transient_for(parent)
418 adjustment = self._scrollWindow.get_vadjustment()
419 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
421 adjustment.value = dx
423 userResponse = self._dialog.run()
427 if userResponse == gtk.RESPONSE_OK:
428 entryBuffer = self._smsEntry.get_buffer()
429 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
430 enteredMessage = enteredMessage[0:self.MAX_CHAR]
434 return enteredMessage.strip()
436 def _update_letter_count(self, *args):
437 entryLength = self._smsEntry.get_buffer().get_char_count()
438 charsLeft = self.MAX_CHAR - entryLength
439 self._letterCountLabel.set_text(str(charsLeft))
441 self._smsButton.set_sensitive(False)
443 self._smsButton.set_sensitive(True)
445 def _on_entry_changed(self, *args):
446 self._update_letter_count()
448 def _on_send(self, *args):
449 self._dialog.response(gtk.RESPONSE_OK)
451 def _on_cancel(self, *args):
452 self._dialog.response(gtk.RESPONSE_CANCEL)
455 class Dialpad(object):
457 def __init__(self, widgetTree, errorDisplay):
458 self._errorDisplay = errorDisplay
459 self._smsDialog = SmsEntryDialog(widgetTree)
461 self._numberdisplay = widgetTree.get_widget("numberdisplay")
462 self._dialButton = widgetTree.get_widget("dial")
463 self._backButton = widgetTree.get_widget("back")
464 self._phonenumber = ""
465 self._prettynumber = ""
468 "on_dial_clicked": self._on_dial_clicked,
469 "on_sms_clicked": self._on_sms_clicked,
470 "on_digit_clicked": self._on_digit_clicked,
471 "on_clear_number": self._on_clear_number,
473 widgetTree.signal_autoconnect(callbackMapping)
475 self._originalLabel = self._backButton.get_label()
476 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
477 self._backTapHandler.on_tap = self._on_backspace
478 self._backTapHandler.on_hold = self._on_clearall
479 self._backTapHandler.on_holding = self._set_clear_button
480 self._backTapHandler.on_cancel = self._reset_back_button
482 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
485 self._dialButton.grab_focus()
486 self._backTapHandler.enable()
489 self._reset_back_button()
490 self._backTapHandler.disable()
492 def number_selected(self, action, number, message):
494 @note Actual dial function is patched in later
496 raise NotImplementedError("Horrible unknown error has occurred")
498 def get_number(self):
499 return self._phonenumber
501 def set_number(self, number):
503 Set the number to dial
506 self._phonenumber = make_ugly(number)
507 self._prettynumber = make_pretty(self._phonenumber)
508 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
510 self._errorDisplay.push_exception()
519 def load_settings(self, config, section):
522 def save_settings(self, config, section):
524 @note Thread Agnostic
528 def _on_sms_clicked(self, widget):
529 action = PhoneTypeSelector.ACTION_SEND_SMS
530 phoneNumber = self.get_number()
532 message = self._smsDialog.run(phoneNumber, "", self._window)
535 action = PhoneTypeSelector.ACTION_CANCEL
537 if action == PhoneTypeSelector.ACTION_CANCEL:
539 self.number_selected(action, phoneNumber, message)
541 def _on_dial_clicked(self, widget):
542 action = PhoneTypeSelector.ACTION_DIAL
543 phoneNumber = self.get_number()
545 self.number_selected(action, phoneNumber, message)
547 def _on_clear_number(self, *args):
550 def _on_digit_clicked(self, widget):
551 self.set_number(self._phonenumber + widget.get_name()[-1])
553 def _on_backspace(self, taps):
554 self.set_number(self._phonenumber[:-taps])
555 self._reset_back_button()
557 def _on_clearall(self, taps):
559 self._reset_back_button()
562 def _set_clear_button(self):
563 self._backButton.set_label("gtk-clear")
565 def _reset_back_button(self):
566 self._backButton.set_label(self._originalLabel)
569 class AccountInfo(object):
571 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
572 self._errorDisplay = errorDisplay
573 self._backend = backend
574 self._isPopulated = False
575 self._alarmHandler = alarmHandler
576 self._notifyOnMissed = False
577 self._notifyOnVoicemail = False
578 self._notifyOnSms = False
580 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
581 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
582 self._callbackCombo = widgetTree.get_widget("callbackcombo")
583 self._onCallbackentryChangedId = 0
585 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
586 self._minutesEntry = widgetTree.get_widget("minutesEntry")
587 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
588 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
589 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
590 self._onNotifyToggled = 0
591 self._onMinutesChanged = 0
592 self._onMissedToggled = 0
593 self._onVoicemailToggled = 0
594 self._onSmsToggled = 0
595 self._applyAlarmTimeoutId = None
597 self._defaultCallback = ""
600 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
602 self._accountViewNumberDisplay.set_use_markup(True)
603 self.set_account_number("")
605 self._callbackList.clear()
606 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
608 if self._alarmHandler is not None:
609 self._minutesEntry.set_range(1, 60)
611 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
612 self._minutesEntry.set_value(self._alarmHandler.recurrence)
613 self._missedCheckbox.set_active(self._notifyOnMissed)
614 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
615 self._smsCheckbox.set_active(self._notifyOnSms)
617 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
618 self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
619 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
620 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
621 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
623 self._notifyCheckbox.set_sensitive(False)
624 self._minutesEntry.set_sensitive(False)
625 self._missedCheckbox.set_sensitive(False)
626 self._voicemailCheckbox.set_sensitive(False)
627 self._smsCheckbox.set_sensitive(False)
629 self.update(force=True)
632 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
633 self._onCallbackentryChangedId = 0
635 if self._alarmHandler is not None:
636 self._notifyCheckbox.disconnect(self._onNotifyToggled)
637 self._minutesEntry.disconnect(self._onMinutesChanged)
638 self._missedCheckbox.disconnect(self._onNotifyToggled)
639 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
640 self._smsCheckbox.disconnect(self._onNotifyToggled)
641 self._onNotifyToggled = 0
642 self._onMinutesChanged = 0
643 self._onMissedToggled = 0
644 self._onVoicemailToggled = 0
645 self._onSmsToggled = 0
647 self._notifyCheckbox.set_sensitive(True)
648 self._minutesEntry.set_sensitive(True)
649 self._missedCheckbox.set_sensitive(True)
650 self._voicemailCheckbox.set_sensitive(True)
651 self._smsCheckbox.set_sensitive(True)
654 self._callbackList.clear()
656 def get_selected_callback_number(self):
657 return make_ugly(self._callbackCombo.get_child().get_text())
659 def set_account_number(self, number):
661 Displays current account number
663 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
665 def update(self, force = False):
666 if not force and self._isPopulated:
668 self._populate_callback_combo()
669 self.set_account_number(self._backend.get_account_number())
673 self._callbackCombo.get_child().set_text("")
674 self.set_account_number("")
675 self._isPopulated = False
677 def save_everything(self):
678 raise NotImplementedError
682 return "Account Info"
684 def load_settings(self, config, section):
685 self._defaultCallback = config.get(section, "callback")
686 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
687 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
688 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
690 def save_settings(self, config, section):
692 @note Thread Agnostic
694 callback = self.get_selected_callback_number()
695 config.set(section, "callback", callback)
696 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
697 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
698 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
700 def _populate_callback_combo(self):
701 self._isPopulated = True
702 self._callbackList.clear()
704 callbackNumbers = self._backend.get_callback_numbers()
705 except StandardError, e:
706 self._errorDisplay.push_exception()
707 self._isPopulated = False
710 for number, description in callbackNumbers.iteritems():
711 self._callbackList.append((make_pretty(number),))
713 self._callbackCombo.set_model(self._callbackList)
714 self._callbackCombo.set_text_column(0)
715 #callbackNumber = self._backend.get_callback_number()
716 callbackNumber = self._defaultCallback
717 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
719 def _set_callback_number(self, number):
721 if not self._backend.is_valid_syntax(number):
722 self._errorDisplay.push_message("%s is not a valid callback number" % number)
723 elif number == self._backend.get_callback_number():
725 "Callback number already is %s" % (
726 self._backend.get_callback_number(),
732 self._backend.set_callback_number(number)
733 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
734 make_pretty(number), make_pretty(self._backend.get_callback_number())
737 "Callback number set to %s" % (
738 self._backend.get_callback_number(),
742 except StandardError, e:
743 self._errorDisplay.push_exception()
745 def _update_alarm_settings(self):
747 isEnabled = self._notifyCheckbox.get_active()
748 recurrence = self._minutesEntry.get_value_as_int()
749 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
750 self._alarmHandler.apply_settings(isEnabled, recurrence)
752 self.save_everything()
753 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
754 self._minutesEntry.set_value(self._alarmHandler.recurrence)
756 def _on_callbackentry_changed(self, *args):
757 text = self.get_selected_callback_number()
758 number = make_ugly(text)
759 self._set_callback_number(number)
761 def _on_notify_toggled(self, *args):
762 if self._applyAlarmTimeoutId is not None:
763 gobject.source_remove(self._applyAlarmTimeoutId)
764 self._applyAlarmTimeoutId = None
765 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
767 def _on_minutes_changed(self, *args):
768 if self._applyAlarmTimeoutId is not None:
769 gobject.source_remove(self._applyAlarmTimeoutId)
770 self._applyAlarmTimeoutId = None
771 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
773 def _on_apply_timeout(self, *args):
774 self._applyAlarmTimeoutId = None
776 self._update_alarm_settings()
779 def _on_missed_toggled(self, *args):
780 self._notifyOnMissed = self._missedCheckbox.get_active()
781 self.save_everything()
783 def _on_voicemail_toggled(self, *args):
784 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
785 self.save_everything()
787 def _on_sms_toggled(self, *args):
788 self._notifyOnSms = self._smsCheckbox.get_active()
789 self.save_everything()
792 class RecentCallsView(object):
799 def __init__(self, widgetTree, backend, errorDisplay):
800 self._errorDisplay = errorDisplay
801 self._backend = backend
803 self._isPopulated = False
804 self._recentmodel = gtk.ListStore(
805 gobject.TYPE_STRING, # number
806 gobject.TYPE_STRING, # date
807 gobject.TYPE_STRING, # action
808 gobject.TYPE_STRING, # from
810 self._recentview = widgetTree.get_widget("recentview")
811 self._recentviewselection = None
812 self._onRecentviewRowActivatedId = 0
814 textrenderer = gtk.CellRendererText()
815 textrenderer.set_property("yalign", 0)
816 hildonize.set_cell_thumb_selectable(textrenderer)
817 self._dateColumn = gtk.TreeViewColumn("Date")
818 self._dateColumn.pack_start(textrenderer, expand=True)
819 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
821 textrenderer = gtk.CellRendererText()
822 textrenderer.set_property("yalign", 0)
823 hildonize.set_cell_thumb_selectable(textrenderer)
824 self._actionColumn = gtk.TreeViewColumn("Action")
825 self._actionColumn.pack_start(textrenderer, expand=True)
826 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
828 textrenderer = gtk.CellRendererText()
829 textrenderer.set_property("yalign", 0)
830 hildonize.set_cell_thumb_selectable(textrenderer)
831 self._nameColumn = gtk.TreeViewColumn("From")
832 self._nameColumn.pack_start(textrenderer, expand=True)
833 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
834 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
836 textrenderer = gtk.CellRendererText()
837 textrenderer.set_property("yalign", 0)
838 hildonize.set_cell_thumb_selectable(textrenderer)
839 self._numberColumn = gtk.TreeViewColumn("Number")
840 self._numberColumn.pack_start(textrenderer, expand=True)
841 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
843 self._window = gtk_toolbox.find_parent_window(self._recentview)
844 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
846 self._updateSink = gtk_toolbox.threaded_stage(
848 self._idly_populate_recentview,
849 gtk_toolbox.null_sink(),
854 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
855 self._recentview.set_model(self._recentmodel)
857 self._recentview.append_column(self._dateColumn)
858 self._recentview.append_column(self._actionColumn)
859 self._recentview.append_column(self._numberColumn)
860 self._recentview.append_column(self._nameColumn)
861 self._recentviewselection = self._recentview.get_selection()
862 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
864 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
867 self._recentview.disconnect(self._onRecentviewRowActivatedId)
871 self._recentview.remove_column(self._dateColumn)
872 self._recentview.remove_column(self._actionColumn)
873 self._recentview.remove_column(self._nameColumn)
874 self._recentview.remove_column(self._numberColumn)
875 self._recentview.set_model(None)
877 def number_selected(self, action, number, message):
879 @note Actual dial function is patched in later
881 raise NotImplementedError("Horrible unknown error has occurred")
883 def update(self, force = False):
884 if not force and self._isPopulated:
886 self._updateSink.send(())
890 self._isPopulated = False
891 self._recentmodel.clear()
895 return "Recent Calls"
897 def load_settings(self, config, section):
900 def save_settings(self, config, section):
902 @note Thread Agnostic
906 def _idly_populate_recentview(self):
907 self._recentmodel.clear()
908 self._isPopulated = True
911 recentItems = self._backend.get_recent()
912 except StandardError, e:
913 self._errorDisplay.push_exception_with_lock()
914 self._isPopulated = False
917 for personName, phoneNumber, date, action in recentItems:
919 personName = "Unknown"
920 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
921 prettyNumber = make_pretty(prettyNumber)
922 item = (prettyNumber, date, action.capitalize(), personName)
923 with gtk_toolbox.gtk_lock():
924 self._recentmodel.append(item)
928 def _on_recentview_row_activated(self, treeview, path, view_column):
929 model, itr = self._recentviewselection.get_selected()
933 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
934 number = make_ugly(number)
935 contactPhoneNumbers = [("Phone", number)]
936 description = self._recentmodel.get_value(itr, self.FROM_IDX)
938 action, phoneNumber, message = self._phoneTypeSelector.run(
940 message = description,
941 parent = self._window,
943 if action == PhoneTypeSelector.ACTION_CANCEL:
945 assert phoneNumber, "A lack of phone number exists"
947 self.number_selected(action, phoneNumber, message)
948 self._recentviewselection.unselect_all()
951 class MessagesView(object):
958 def __init__(self, widgetTree, backend, errorDisplay):
959 self._errorDisplay = errorDisplay
960 self._backend = backend
962 self._isPopulated = False
963 self._messagemodel = gtk.ListStore(
964 gobject.TYPE_STRING, # number
965 gobject.TYPE_STRING, # date
966 gobject.TYPE_STRING, # header
967 gobject.TYPE_STRING, # message
969 self._messageview = widgetTree.get_widget("messages_view")
970 self._messageviewselection = None
971 self._onMessageviewRowActivatedId = 0
973 self._messageRenderer = gtk.CellRendererText()
974 hildonize.set_cell_thumb_selectable(self._messageRenderer)
975 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
976 self._messageRenderer.set_property("wrap-width", 500)
977 self._messageColumn = gtk.TreeViewColumn("Messages")
978 self._messageColumn.pack_start(self._messageRenderer, expand=True)
979 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
980 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
982 self._window = gtk_toolbox.find_parent_window(self._messageview)
983 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
985 self._updateSink = gtk_toolbox.threaded_stage(
987 self._idly_populate_messageview,
988 gtk_toolbox.null_sink(),
993 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
994 self._messageview.set_model(self._messagemodel)
996 self._messageview.append_column(self._messageColumn)
997 self._messageviewselection = self._messageview.get_selection()
998 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1000 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1003 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1007 self._messageview.remove_column(self._messageColumn)
1008 self._messageview.set_model(None)
1010 def number_selected(self, action, number, message):
1012 @note Actual dial function is patched in later
1014 raise NotImplementedError("Horrible unknown error has occurred")
1016 def update(self, force = False):
1017 if not force and self._isPopulated:
1019 self._updateSink.send(())
1023 self._isPopulated = False
1024 self._messagemodel.clear()
1030 def load_settings(self, config, section):
1033 def save_settings(self, config, section):
1035 @note Thread Agnostic
1039 def _idly_populate_messageview(self):
1040 self._messagemodel.clear()
1041 self._isPopulated = True
1044 messageItems = self._backend.get_messages()
1045 except StandardError, e:
1046 self._errorDisplay.push_exception_with_lock()
1047 self._isPopulated = False
1050 for header, number, relativeDate, message in messageItems:
1051 prettyNumber = number[2:] if number.startswith("+1") else number
1052 prettyNumber = make_pretty(prettyNumber)
1053 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1054 number = make_ugly(number)
1055 row = (number, relativeDate, header, message)
1056 with gtk_toolbox.gtk_lock():
1057 self._messagemodel.append(row)
1061 def _on_messageview_row_activated(self, treeview, path, view_column):
1062 model, itr = self._messageviewselection.get_selected()
1066 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1067 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1069 action, phoneNumber, message = self._phoneTypeSelector.run(
1070 contactPhoneNumbers,
1071 message = description,
1072 parent = self._window,
1074 if action == PhoneTypeSelector.ACTION_CANCEL:
1076 assert phoneNumber, "A lock of phone number exists"
1078 self.number_selected(action, phoneNumber, message)
1079 self._messageviewselection.unselect_all()
1082 class ContactsView(object):
1084 def __init__(self, widgetTree, backend, errorDisplay):
1085 self._errorDisplay = errorDisplay
1086 self._backend = backend
1088 self._addressBook = None
1089 self._selectedComboIndex = 0
1090 self._addressBookFactories = [null_backend.NullAddressBook()]
1092 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1093 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1095 self._isPopulated = False
1096 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1097 self._contactsviewselection = None
1098 self._contactsview = widgetTree.get_widget("contactsview")
1100 self._contactColumn = gtk.TreeViewColumn("Contact")
1101 displayContactSource = False
1102 if displayContactSource:
1103 textrenderer = gtk.CellRendererText()
1104 self._contactColumn.pack_start(textrenderer, expand=False)
1105 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1106 textrenderer = gtk.CellRendererText()
1107 hildonize.set_cell_thumb_selectable(textrenderer)
1108 self._contactColumn.pack_start(textrenderer, expand=True)
1109 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1110 textrenderer = gtk.CellRendererText()
1111 self._contactColumn.pack_start(textrenderer, expand=True)
1112 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1113 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1114 self._contactColumn.set_sort_column_id(1)
1115 self._contactColumn.set_visible(True)
1117 self._onContactsviewRowActivatedId = 0
1118 self._onAddressbookComboChangedId = 0
1119 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1120 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1122 self._updateSink = gtk_toolbox.threaded_stage(
1124 self._idly_populate_contactsview,
1125 gtk_toolbox.null_sink(),
1130 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1132 self._contactsview.set_model(self._contactsmodel)
1133 self._contactsview.append_column(self._contactColumn)
1134 self._contactsviewselection = self._contactsview.get_selection()
1135 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1137 self._booksList.clear()
1138 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1139 if factoryName and bookName:
1140 entryName = "%s: %s" % (factoryName, bookName)
1142 entryName = factoryName
1144 entryName = bookName
1146 entryName = "Bad name (%d)" % factoryId
1147 row = (str(factoryId), bookId, entryName)
1148 self._booksList.append(row)
1150 self._booksSelectionBox.set_model(self._booksList)
1151 cell = gtk.CellRendererText()
1152 self._booksSelectionBox.pack_start(cell, True)
1153 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1155 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1156 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1158 if len(self._booksList) <= self._selectedComboIndex:
1159 self._selectedComboIndex = 0
1160 self._booksSelectionBox.set_active(self._selectedComboIndex)
1163 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1164 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1168 self._booksSelectionBox.clear()
1169 self._booksSelectionBox.set_model(None)
1170 self._contactsview.set_model(None)
1171 self._contactsview.remove_column(self._contactColumn)
1173 def number_selected(self, action, number, message):
1175 @note Actual dial function is patched in later
1177 raise NotImplementedError("Horrible unknown error has occurred")
1179 def get_addressbooks(self):
1181 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1183 for i, factory in enumerate(self._addressBookFactories):
1184 for bookFactory, bookId, bookName in factory.get_addressbooks():
1185 yield (str(i), bookId), (factory.factory_name(), bookName)
1187 def open_addressbook(self, bookFactoryId, bookId):
1188 bookFactoryIndex = int(bookFactoryId)
1189 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1191 forceUpdate = True if addressBook is not self._addressBook else False
1193 self._addressBook = addressBook
1194 self.update(force=forceUpdate)
1196 def update(self, force = False):
1197 if not force and self._isPopulated:
1199 self._updateSink.send(())
1203 self._isPopulated = False
1204 self._contactsmodel.clear()
1205 for factory in self._addressBookFactories:
1206 factory.clear_caches()
1207 self._addressBook.clear_caches()
1209 def append(self, book):
1210 self._addressBookFactories.append(book)
1212 def extend(self, books):
1213 self._addressBookFactories.extend(books)
1219 def load_settings(self, config, sectionName):
1221 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1222 except ConfigParser.NoOptionError:
1223 self._selectedComboIndex = 0
1225 def save_settings(self, config, sectionName):
1226 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1228 def _idly_populate_contactsview(self):
1230 while addressBook is not self._addressBook:
1231 addressBook = self._addressBook
1232 with gtk_toolbox.gtk_lock():
1233 self._contactsview.set_model(None)
1237 contacts = addressBook.get_contacts()
1238 except StandardError, e:
1240 self._isPopulated = False
1241 self._errorDisplay.push_exception_with_lock()
1242 for contactId, contactName in contacts:
1243 contactType = (addressBook.contact_source_short_name(contactId), )
1244 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1246 with gtk_toolbox.gtk_lock():
1247 self._contactsview.set_model(self._contactsmodel)
1249 self._isPopulated = True
1252 def _on_addressbook_combo_changed(self, *args, **kwds):
1253 itr = self._booksSelectionBox.get_active_iter()
1256 self._selectedComboIndex = self._booksSelectionBox.get_active()
1257 selectedFactoryId = self._booksList.get_value(itr, 0)
1258 selectedBookId = self._booksList.get_value(itr, 1)
1259 self.open_addressbook(selectedFactoryId, selectedBookId)
1261 def _on_contactsview_row_activated(self, treeview, path, view_column):
1262 model, itr = self._contactsviewselection.get_selected()
1266 contactId = self._contactsmodel.get_value(itr, 3)
1267 contactName = self._contactsmodel.get_value(itr, 1)
1269 contactDetails = self._addressBook.get_contact_details(contactId)
1270 except StandardError, e:
1272 self._errorDisplay.push_exception()
1273 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1275 if len(contactPhoneNumbers) == 0:
1278 action, phoneNumber, message = self._phoneTypeSelector.run(
1279 contactPhoneNumbers,
1280 message = contactName,
1281 parent = self._window,
1283 if action == PhoneTypeSelector.ACTION_CANCEL:
1285 assert phoneNumber, "A lack of phone number exists"
1287 self.number_selected(action, phoneNumber, message)
1288 self._contactsviewselection.unselect_all()