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.set_number(self._phonenumber)
486 self._dialButton.grab_focus()
487 self._backTapHandler.enable()
490 self._reset_back_button()
491 self._backTapHandler.disable()
494 def number_selected(self, action, number, message):
496 @note Actual dial function is patched in later
498 raise NotImplementedError("Horrible unknown error has occurred")
500 def get_number(self):
501 return self._phonenumber
503 def set_number(self, number):
505 Set the number to dial
508 self._phonenumber = make_ugly(number)
509 self._prettynumber = make_pretty(self._phonenumber)
510 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
512 self._errorDisplay.push_exception()
521 def load_settings(self, config, section):
524 def save_settings(self, config, section):
526 @note Thread Agnostic
530 def _on_sms_clicked(self, widget):
531 action = PhoneTypeSelector.ACTION_SEND_SMS
532 phoneNumber = self.get_number()
534 message = self._smsDialog.run(phoneNumber, "", self._window)
537 action = PhoneTypeSelector.ACTION_CANCEL
539 if action == PhoneTypeSelector.ACTION_CANCEL:
541 self.number_selected(action, phoneNumber, message)
543 def _on_dial_clicked(self, widget):
544 action = PhoneTypeSelector.ACTION_DIAL
545 phoneNumber = self.get_number()
547 self.number_selected(action, phoneNumber, message)
549 def _on_clear_number(self, *args):
552 def _on_digit_clicked(self, widget):
553 self.set_number(self._phonenumber + widget.get_name()[-1])
555 def _on_backspace(self, taps):
556 self.set_number(self._phonenumber[:-taps])
557 self._reset_back_button()
559 def _on_clearall(self, taps):
561 self._reset_back_button()
564 def _set_clear_button(self):
565 self._backButton.set_label("gtk-clear")
567 def _reset_back_button(self):
568 self._backButton.set_label(self._originalLabel)
571 class AccountInfo(object):
573 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
574 self._errorDisplay = errorDisplay
575 self._backend = backend
576 self._isPopulated = False
577 self._alarmHandler = alarmHandler
578 self._notifyOnMissed = False
579 self._notifyOnVoicemail = False
580 self._notifyOnSms = False
582 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
583 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
584 self._callbackCombo = widgetTree.get_widget("callbackcombo")
585 self._onCallbackentryChangedId = 0
587 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
588 self._minutesEntry = widgetTree.get_widget("minutesEntry")
589 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
590 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
591 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
592 self._onNotifyToggled = 0
593 self._onMinutesChanged = 0
594 self._onMissedToggled = 0
595 self._onVoicemailToggled = 0
596 self._onSmsToggled = 0
597 self._applyAlarmTimeoutId = None
599 self._defaultCallback = ""
602 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
604 self._accountViewNumberDisplay.set_use_markup(True)
605 self.set_account_number("")
607 self._callbackList.clear()
608 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
610 if self._alarmHandler is not None:
611 self._minutesEntry.set_range(1, 60)
613 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
614 self._minutesEntry.set_value(self._alarmHandler.recurrence)
615 self._missedCheckbox.set_active(self._notifyOnMissed)
616 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
617 self._smsCheckbox.set_active(self._notifyOnSms)
619 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
620 self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
621 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
622 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
623 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
625 self._notifyCheckbox.set_sensitive(False)
626 self._minutesEntry.set_sensitive(False)
627 self._missedCheckbox.set_sensitive(False)
628 self._voicemailCheckbox.set_sensitive(False)
629 self._smsCheckbox.set_sensitive(False)
631 self.update(force=True)
634 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
635 self._onCallbackentryChangedId = 0
637 if self._alarmHandler is not None:
638 self._notifyCheckbox.disconnect(self._onNotifyToggled)
639 self._minutesEntry.disconnect(self._onMinutesChanged)
640 self._missedCheckbox.disconnect(self._onNotifyToggled)
641 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
642 self._smsCheckbox.disconnect(self._onNotifyToggled)
643 self._onNotifyToggled = 0
644 self._onMinutesChanged = 0
645 self._onMissedToggled = 0
646 self._onVoicemailToggled = 0
647 self._onSmsToggled = 0
649 self._notifyCheckbox.set_sensitive(True)
650 self._minutesEntry.set_sensitive(True)
651 self._missedCheckbox.set_sensitive(True)
652 self._voicemailCheckbox.set_sensitive(True)
653 self._smsCheckbox.set_sensitive(True)
656 self._callbackList.clear()
658 def get_selected_callback_number(self):
659 return make_ugly(self._callbackCombo.get_child().get_text())
661 def set_account_number(self, number):
663 Displays current account number
665 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
667 def update(self, force = False):
668 if not force and self._isPopulated:
670 self._populate_callback_combo()
671 self.set_account_number(self._backend.get_account_number())
675 self._callbackCombo.get_child().set_text("")
676 self.set_account_number("")
677 self._isPopulated = False
679 def save_everything(self):
680 raise NotImplementedError
684 return "Account Info"
686 def load_settings(self, config, section):
687 self._defaultCallback = config.get(section, "callback")
688 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
689 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
690 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
692 def save_settings(self, config, section):
694 @note Thread Agnostic
696 callback = self.get_selected_callback_number()
697 config.set(section, "callback", callback)
698 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
699 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
700 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
702 def _populate_callback_combo(self):
703 self._isPopulated = True
704 self._callbackList.clear()
706 callbackNumbers = self._backend.get_callback_numbers()
707 except StandardError, e:
708 self._errorDisplay.push_exception()
709 self._isPopulated = False
712 for number, description in callbackNumbers.iteritems():
713 self._callbackList.append((make_pretty(number),))
715 self._callbackCombo.set_model(self._callbackList)
716 self._callbackCombo.set_text_column(0)
717 #callbackNumber = self._backend.get_callback_number()
718 callbackNumber = self._defaultCallback
719 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
721 def _set_callback_number(self, number):
723 if not self._backend.is_valid_syntax(number):
724 self._errorDisplay.push_message("%s is not a valid callback number" % number)
725 elif number == self._backend.get_callback_number():
727 "Callback number already is %s" % (
728 self._backend.get_callback_number(),
734 self._backend.set_callback_number(number)
735 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
736 make_pretty(number), make_pretty(self._backend.get_callback_number())
739 "Callback number set to %s" % (
740 self._backend.get_callback_number(),
744 except StandardError, e:
745 self._errorDisplay.push_exception()
747 def _update_alarm_settings(self):
749 isEnabled = self._notifyCheckbox.get_active()
750 recurrence = self._minutesEntry.get_value_as_int()
751 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
752 self._alarmHandler.apply_settings(isEnabled, recurrence)
754 self.save_everything()
755 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
756 self._minutesEntry.set_value(self._alarmHandler.recurrence)
758 def _on_callbackentry_changed(self, *args):
759 text = self.get_selected_callback_number()
760 number = make_ugly(text)
761 self._set_callback_number(number)
763 def _on_notify_toggled(self, *args):
764 if self._applyAlarmTimeoutId is not None:
765 gobject.source_remove(self._applyAlarmTimeoutId)
766 self._applyAlarmTimeoutId = None
767 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
769 def _on_minutes_changed(self, *args):
770 if self._applyAlarmTimeoutId is not None:
771 gobject.source_remove(self._applyAlarmTimeoutId)
772 self._applyAlarmTimeoutId = None
773 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
775 def _on_apply_timeout(self, *args):
776 self._applyAlarmTimeoutId = None
778 self._update_alarm_settings()
781 def _on_missed_toggled(self, *args):
782 self._notifyOnMissed = self._missedCheckbox.get_active()
783 self.save_everything()
785 def _on_voicemail_toggled(self, *args):
786 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
787 self.save_everything()
789 def _on_sms_toggled(self, *args):
790 self._notifyOnSms = self._smsCheckbox.get_active()
791 self.save_everything()
794 class RecentCallsView(object):
801 def __init__(self, widgetTree, backend, errorDisplay):
802 self._errorDisplay = errorDisplay
803 self._backend = backend
805 self._isPopulated = False
806 self._recentmodel = gtk.ListStore(
807 gobject.TYPE_STRING, # number
808 gobject.TYPE_STRING, # date
809 gobject.TYPE_STRING, # action
810 gobject.TYPE_STRING, # from
812 self._recentview = widgetTree.get_widget("recentview")
813 self._recentviewselection = None
814 self._onRecentviewRowActivatedId = 0
816 textrenderer = gtk.CellRendererText()
817 textrenderer.set_property("yalign", 0)
818 hildonize.set_cell_thumb_selectable(textrenderer)
819 self._dateColumn = gtk.TreeViewColumn("Date")
820 self._dateColumn.pack_start(textrenderer, expand=True)
821 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
823 textrenderer = gtk.CellRendererText()
824 textrenderer.set_property("yalign", 0)
825 hildonize.set_cell_thumb_selectable(textrenderer)
826 self._actionColumn = gtk.TreeViewColumn("Action")
827 self._actionColumn.pack_start(textrenderer, expand=True)
828 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
830 textrenderer = gtk.CellRendererText()
831 textrenderer.set_property("yalign", 0)
832 hildonize.set_cell_thumb_selectable(textrenderer)
833 self._nameColumn = gtk.TreeViewColumn("From")
834 self._nameColumn.pack_start(textrenderer, expand=True)
835 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
836 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
838 textrenderer = gtk.CellRendererText()
839 textrenderer.set_property("yalign", 0)
840 hildonize.set_cell_thumb_selectable(textrenderer)
841 self._numberColumn = gtk.TreeViewColumn("Number")
842 self._numberColumn.pack_start(textrenderer, expand=True)
843 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
845 self._window = gtk_toolbox.find_parent_window(self._recentview)
846 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
848 self._updateSink = gtk_toolbox.threaded_stage(
850 self._idly_populate_recentview,
851 gtk_toolbox.null_sink(),
856 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
857 self._recentview.set_model(self._recentmodel)
859 self._recentview.append_column(self._dateColumn)
860 self._recentview.append_column(self._actionColumn)
861 self._recentview.append_column(self._numberColumn)
862 self._recentview.append_column(self._nameColumn)
863 self._recentviewselection = self._recentview.get_selection()
864 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
866 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
869 self._recentview.disconnect(self._onRecentviewRowActivatedId)
873 self._recentview.remove_column(self._dateColumn)
874 self._recentview.remove_column(self._actionColumn)
875 self._recentview.remove_column(self._nameColumn)
876 self._recentview.remove_column(self._numberColumn)
877 self._recentview.set_model(None)
879 def number_selected(self, action, number, message):
881 @note Actual dial function is patched in later
883 raise NotImplementedError("Horrible unknown error has occurred")
885 def update(self, force = False):
886 if not force and self._isPopulated:
888 self._updateSink.send(())
892 self._isPopulated = False
893 self._recentmodel.clear()
897 return "Recent Calls"
899 def load_settings(self, config, section):
902 def save_settings(self, config, section):
904 @note Thread Agnostic
908 def _idly_populate_recentview(self):
909 self._recentmodel.clear()
910 self._isPopulated = True
913 recentItems = self._backend.get_recent()
914 except StandardError, e:
915 self._errorDisplay.push_exception_with_lock()
916 self._isPopulated = False
919 for personName, phoneNumber, date, action in recentItems:
921 personName = "Unknown"
922 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
923 prettyNumber = make_pretty(prettyNumber)
924 item = (prettyNumber, date, action.capitalize(), personName)
925 with gtk_toolbox.gtk_lock():
926 self._recentmodel.append(item)
930 def _on_recentview_row_activated(self, treeview, path, view_column):
931 model, itr = self._recentviewselection.get_selected()
935 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
936 number = make_ugly(number)
937 contactPhoneNumbers = [("Phone", number)]
938 description = self._recentmodel.get_value(itr, self.FROM_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 lack of phone number exists"
949 self.number_selected(action, phoneNumber, message)
950 self._recentviewselection.unselect_all()
953 class MessagesView(object):
960 def __init__(self, widgetTree, backend, errorDisplay):
961 self._errorDisplay = errorDisplay
962 self._backend = backend
964 self._isPopulated = False
965 self._messagemodel = gtk.ListStore(
966 gobject.TYPE_STRING, # number
967 gobject.TYPE_STRING, # date
968 gobject.TYPE_STRING, # header
969 gobject.TYPE_STRING, # message
971 self._messageview = widgetTree.get_widget("messages_view")
972 self._messageviewselection = None
973 self._onMessageviewRowActivatedId = 0
975 self._messageRenderer = gtk.CellRendererText()
976 hildonize.set_cell_thumb_selectable(self._messageRenderer)
977 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
978 self._messageRenderer.set_property("wrap-width", 650)
979 self._messageColumn = gtk.TreeViewColumn("Messages")
980 self._messageColumn.pack_start(self._messageRenderer, expand=True)
981 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
982 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
984 self._window = gtk_toolbox.find_parent_window(self._messageview)
985 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
987 self._updateSink = gtk_toolbox.threaded_stage(
989 self._idly_populate_messageview,
990 gtk_toolbox.null_sink(),
995 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
996 self._messageview.set_model(self._messagemodel)
998 self._messageview.append_column(self._messageColumn)
999 self._messageviewselection = self._messageview.get_selection()
1000 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1002 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1005 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1009 self._messageview.remove_column(self._messageColumn)
1010 self._messageview.set_model(None)
1012 def number_selected(self, action, number, message):
1014 @note Actual dial function is patched in later
1016 raise NotImplementedError("Horrible unknown error has occurred")
1018 def update(self, force = False):
1019 if not force and self._isPopulated:
1021 self._updateSink.send(())
1025 self._isPopulated = False
1026 self._messagemodel.clear()
1032 def load_settings(self, config, section):
1035 def save_settings(self, config, section):
1037 @note Thread Agnostic
1041 def _idly_populate_messageview(self):
1042 self._messagemodel.clear()
1043 self._isPopulated = True
1046 messageItems = self._backend.get_messages()
1047 except StandardError, e:
1048 self._errorDisplay.push_exception_with_lock()
1049 self._isPopulated = False
1052 for header, number, relativeDate, message in messageItems:
1053 prettyNumber = number[2:] if number.startswith("+1") else number
1054 prettyNumber = make_pretty(prettyNumber)
1055 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1056 number = make_ugly(number)
1057 row = (number, relativeDate, header, message)
1058 with gtk_toolbox.gtk_lock():
1059 self._messagemodel.append(row)
1063 def _on_messageview_row_activated(self, treeview, path, view_column):
1064 model, itr = self._messageviewselection.get_selected()
1068 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1069 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1071 action, phoneNumber, message = self._phoneTypeSelector.run(
1072 contactPhoneNumbers,
1073 message = description,
1074 parent = self._window,
1076 if action == PhoneTypeSelector.ACTION_CANCEL:
1078 assert phoneNumber, "A lock of phone number exists"
1080 self.number_selected(action, phoneNumber, message)
1081 self._messageviewselection.unselect_all()
1084 class ContactsView(object):
1086 def __init__(self, widgetTree, backend, errorDisplay):
1087 self._errorDisplay = errorDisplay
1088 self._backend = backend
1090 self._addressBook = None
1091 self._selectedComboIndex = 0
1092 self._addressBookFactories = [null_backend.NullAddressBook()]
1094 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1095 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1097 self._isPopulated = False
1098 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1099 self._contactsviewselection = None
1100 self._contactsview = widgetTree.get_widget("contactsview")
1102 self._contactColumn = gtk.TreeViewColumn("Contact")
1103 displayContactSource = False
1104 if displayContactSource:
1105 textrenderer = gtk.CellRendererText()
1106 self._contactColumn.pack_start(textrenderer, expand=False)
1107 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1108 textrenderer = gtk.CellRendererText()
1109 hildonize.set_cell_thumb_selectable(textrenderer)
1110 self._contactColumn.pack_start(textrenderer, expand=True)
1111 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1112 textrenderer = gtk.CellRendererText()
1113 self._contactColumn.pack_start(textrenderer, expand=True)
1114 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1115 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1116 self._contactColumn.set_sort_column_id(1)
1117 self._contactColumn.set_visible(True)
1119 self._onContactsviewRowActivatedId = 0
1120 self._onAddressbookComboChangedId = 0
1121 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1122 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1124 self._updateSink = gtk_toolbox.threaded_stage(
1126 self._idly_populate_contactsview,
1127 gtk_toolbox.null_sink(),
1132 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1134 self._contactsview.set_model(self._contactsmodel)
1135 self._contactsview.append_column(self._contactColumn)
1136 self._contactsviewselection = self._contactsview.get_selection()
1137 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1139 self._booksList.clear()
1140 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1141 if factoryName and bookName:
1142 entryName = "%s: %s" % (factoryName, bookName)
1144 entryName = factoryName
1146 entryName = bookName
1148 entryName = "Bad name (%d)" % factoryId
1149 row = (str(factoryId), bookId, entryName)
1150 self._booksList.append(row)
1152 self._booksSelectionBox.set_model(self._booksList)
1153 cell = gtk.CellRendererText()
1154 self._booksSelectionBox.pack_start(cell, True)
1155 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1157 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1158 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1160 if len(self._booksList) <= self._selectedComboIndex:
1161 self._selectedComboIndex = 0
1162 self._booksSelectionBox.set_active(self._selectedComboIndex)
1165 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1166 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1170 self._booksSelectionBox.clear()
1171 self._booksSelectionBox.set_model(None)
1172 self._contactsview.set_model(None)
1173 self._contactsview.remove_column(self._contactColumn)
1175 def number_selected(self, action, number, message):
1177 @note Actual dial function is patched in later
1179 raise NotImplementedError("Horrible unknown error has occurred")
1181 def get_addressbooks(self):
1183 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1185 for i, factory in enumerate(self._addressBookFactories):
1186 for bookFactory, bookId, bookName in factory.get_addressbooks():
1187 yield (str(i), bookId), (factory.factory_name(), bookName)
1189 def open_addressbook(self, bookFactoryId, bookId):
1190 bookFactoryIndex = int(bookFactoryId)
1191 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1193 forceUpdate = True if addressBook is not self._addressBook else False
1195 self._addressBook = addressBook
1196 self.update(force=forceUpdate)
1198 def update(self, force = False):
1199 if not force and self._isPopulated:
1201 self._updateSink.send(())
1205 self._isPopulated = False
1206 self._contactsmodel.clear()
1207 for factory in self._addressBookFactories:
1208 factory.clear_caches()
1209 self._addressBook.clear_caches()
1211 def append(self, book):
1212 self._addressBookFactories.append(book)
1214 def extend(self, books):
1215 self._addressBookFactories.extend(books)
1221 def load_settings(self, config, sectionName):
1223 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1224 except ConfigParser.NoOptionError:
1225 self._selectedComboIndex = 0
1227 def save_settings(self, config, sectionName):
1228 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1230 def _idly_populate_contactsview(self):
1232 while addressBook is not self._addressBook:
1233 addressBook = self._addressBook
1234 with gtk_toolbox.gtk_lock():
1235 self._contactsview.set_model(None)
1239 contacts = addressBook.get_contacts()
1240 except StandardError, e:
1242 self._isPopulated = False
1243 self._errorDisplay.push_exception_with_lock()
1244 for contactId, contactName in contacts:
1245 contactType = (addressBook.contact_source_short_name(contactId), )
1246 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1248 with gtk_toolbox.gtk_lock():
1249 self._contactsview.set_model(self._contactsmodel)
1251 self._isPopulated = True
1254 def _on_addressbook_combo_changed(self, *args, **kwds):
1255 itr = self._booksSelectionBox.get_active_iter()
1258 self._selectedComboIndex = self._booksSelectionBox.get_active()
1259 selectedFactoryId = self._booksList.get_value(itr, 0)
1260 selectedBookId = self._booksList.get_value(itr, 1)
1261 self.open_addressbook(selectedFactoryId, selectedBookId)
1263 def _on_contactsview_row_activated(self, treeview, path, view_column):
1264 model, itr = self._contactsviewselection.get_selected()
1268 contactId = self._contactsmodel.get_value(itr, 3)
1269 contactName = self._contactsmodel.get_value(itr, 1)
1271 contactDetails = self._addressBook.get_contact_details(contactId)
1272 except StandardError, e:
1274 self._errorDisplay.push_exception()
1275 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1277 if len(contactPhoneNumbers) == 0:
1280 action, phoneNumber, message = self._phoneTypeSelector.run(
1281 contactPhoneNumbers,
1282 message = contactName,
1283 parent = self._window,
1285 if action == PhoneTypeSelector.ACTION_CANCEL:
1287 assert phoneNumber, "A lack of phone number exists"
1289 self.number_selected(action, phoneNumber, message)
1290 self._contactsviewselection.unselect_all()