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 def abbrev_relative_date(date):
101 >>> abbrev_relative_date("42 hours ago")
103 >>> abbrev_relative_date("2 days ago")
105 >>> abbrev_relative_date("4 weeks ago")
108 parts = date.split(" ")
109 return "%s %s" % (parts[0], parts[1][0])
112 class MergedAddressBook(object):
114 Merger of all addressbooks
117 def __init__(self, addressbookFactories, sorter = None):
118 self.__addressbookFactories = addressbookFactories
119 self.__addressbooks = None
120 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
122 def clear_caches(self):
123 self.__addressbooks = None
124 for factory in self.__addressbookFactories:
125 factory.clear_caches()
127 def get_addressbooks(self):
129 @returns Iterable of (Address Book Factory, Book Id, Book Name)
133 def open_addressbook(self, bookId):
136 def contact_source_short_name(self, contactId):
137 if self.__addressbooks is None:
139 bookIndex, originalId = contactId.split("-", 1)
140 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
144 return "All Contacts"
146 def get_contacts(self):
148 @returns Iterable of (contact id, contact name)
150 if self.__addressbooks is None:
151 self.__addressbooks = list(
152 factory.open_addressbook(id)
153 for factory in self.__addressbookFactories
154 for (f, id, name) in factory.get_addressbooks()
157 ("-".join([str(bookIndex), contactId]), contactName)
158 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
159 for (contactId, contactName) in addressbook.get_contacts()
161 sortedContacts = self.__sort_contacts(contacts)
162 return sortedContacts
164 def get_contact_details(self, contactId):
166 @returns Iterable of (Phone Type, Phone Number)
168 if self.__addressbooks is None:
170 bookIndex, originalId = contactId.split("-", 1)
171 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
174 def null_sorter(contacts):
176 Good for speed/low memory
181 def basic_firtname_sorter(contacts):
183 Expects names in "First Last" format
186 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
187 for (contactId, contactName) in contacts
189 contactsWithKey.sort()
190 return (contactData for (lastName, contactData) in contactsWithKey)
193 def basic_lastname_sorter(contacts):
195 Expects names in "First Last" format
198 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
199 for (contactId, contactName) in contacts
201 contactsWithKey.sort()
202 return (contactData for (lastName, contactData) in contactsWithKey)
205 def reversed_firtname_sorter(contacts):
207 Expects names in "Last, First" format
210 (contactName.split(", ", 1)[-1], (contactId, contactName))
211 for (contactId, contactName) in contacts
213 contactsWithKey.sort()
214 return (contactData for (lastName, contactData) in contactsWithKey)
217 def reversed_lastname_sorter(contacts):
219 Expects names in "Last, First" format
222 (contactName.split(", ", 1)[0], (contactId, contactName))
223 for (contactId, contactName) in contacts
225 contactsWithKey.sort()
226 return (contactData for (lastName, contactData) in contactsWithKey)
229 def guess_firstname(name):
231 return name.split(", ", 1)[-1]
233 return name.rsplit(" ", 1)[0]
236 def guess_lastname(name):
238 return name.split(", ", 1)[0]
240 return name.rsplit(" ", 1)[-1]
243 def advanced_firstname_sorter(cls, contacts):
245 (cls.guess_firstname(contactName), (contactId, contactName))
246 for (contactId, contactName) in contacts
248 contactsWithKey.sort()
249 return (contactData for (lastName, contactData) in contactsWithKey)
252 def advanced_lastname_sorter(cls, contacts):
254 (cls.guess_lastname(contactName), (contactId, contactName))
255 for (contactId, contactName) in contacts
257 contactsWithKey.sort()
258 return (contactData for (lastName, contactData) in contactsWithKey)
261 class PhoneTypeSelector(object):
263 ACTION_CANCEL = "cancel"
264 ACTION_SELECT = "select"
266 ACTION_SEND_SMS = "sms"
268 def __init__(self, widgetTree, gcBackend):
269 self._gcBackend = gcBackend
270 self._widgetTree = widgetTree
272 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
273 self._smsDialog = SmsEntryDialog(self._widgetTree)
275 self._smsButton = self._widgetTree.get_widget("sms_button")
276 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
278 self._dialButton = self._widgetTree.get_widget("dial_button")
279 self._dialButton.connect("clicked", self._on_phonetype_dial)
281 self._selectButton = self._widgetTree.get_widget("select_button")
282 self._selectButton.connect("clicked", self._on_phonetype_select)
284 self._cancelButton = self._widgetTree.get_widget("cancel_button")
285 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
287 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
288 self._typeviewselection = None
290 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
291 self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
292 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
293 self._typeview = self._widgetTree.get_widget("phonetypes")
294 self._typeview.connect("row-activated", self._on_phonetype_select)
296 self._action = self.ACTION_CANCEL
298 def run(self, contactDetails, message = "", parent = None):
299 self._action = self.ACTION_CANCEL
300 self._typemodel.clear()
301 self._typeview.set_model(self._typemodel)
303 # Add the column to the treeview
304 textrenderer = gtk.CellRendererText()
305 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
306 self._typeview.append_column(numberColumn)
308 textrenderer = gtk.CellRendererText()
309 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
310 self._typeview.append_column(typeColumn)
312 self._typeviewselection = self._typeview.get_selection()
313 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
315 for phoneType, phoneNumber in contactDetails:
316 display = " - ".join((phoneNumber, phoneType))
318 row = (phoneNumber, display)
319 self._typemodel.append(row)
321 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
323 self._message.set_markup(message)
326 self._message.set_markup("")
329 if parent is not None:
330 self._dialog.set_transient_for(parent)
334 adjustment = self._scrollWindow.get_vadjustment()
335 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
337 adjustment.value = dx
339 userResponse = self._dialog.run()
343 if userResponse == gtk.RESPONSE_OK:
344 phoneNumber = self._get_number()
345 phoneNumber = make_ugly(phoneNumber)
349 self._action = self.ACTION_CANCEL
351 if self._action == self.ACTION_SEND_SMS:
352 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
355 self._action = self.ACTION_CANCEL
359 self._typeviewselection.unselect_all()
360 self._typeview.remove_column(numberColumn)
361 self._typeview.remove_column(typeColumn)
362 self._typeview.set_model(None)
364 return self._action, phoneNumber, smsMessage
366 def _get_number(self):
367 model, itr = self._typeviewselection.get_selected()
371 phoneNumber = self._typemodel.get_value(itr, 0)
374 def _on_phonetype_dial(self, *args):
375 self._dialog.response(gtk.RESPONSE_OK)
376 self._action = self.ACTION_DIAL
378 def _on_phonetype_send_sms(self, *args):
379 self._dialog.response(gtk.RESPONSE_OK)
380 self._action = self.ACTION_SEND_SMS
382 def _on_phonetype_select(self, *args):
383 self._dialog.response(gtk.RESPONSE_OK)
384 self._action = self.ACTION_SELECT
386 def _on_phonetype_cancel(self, *args):
387 self._dialog.response(gtk.RESPONSE_CANCEL)
388 self._action = self.ACTION_CANCEL
391 class SmsEntryDialog(object):
394 @todo Add multi-SMS messages like GoogleVoice
399 def __init__(self, widgetTree):
400 self._widgetTree = widgetTree
401 self._dialog = self._widgetTree.get_widget("smsDialog")
403 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
404 self._smsButton.connect("clicked", self._on_send)
406 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
407 self._cancelButton.connect("clicked", self._on_cancel)
409 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
410 self._message = self._widgetTree.get_widget("smsMessage")
411 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
412 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
413 self._smsEntry = self._widgetTree.get_widget("smsEntry")
414 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
416 def run(self, number, message = "", parent = None):
418 self._message.set_markup(message)
421 self._message.set_markup("")
423 self._smsEntry.get_buffer().set_text("")
424 self._update_letter_count()
426 if parent is not None:
427 self._dialog.set_transient_for(parent)
431 adjustment = self._scrollWindow.get_vadjustment()
432 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
434 adjustment.value = dx
436 userResponse = self._dialog.run()
440 if userResponse == gtk.RESPONSE_OK:
441 entryBuffer = self._smsEntry.get_buffer()
442 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
443 enteredMessage = enteredMessage[0:self.MAX_CHAR]
447 return enteredMessage.strip()
449 def _update_letter_count(self, *args):
450 entryLength = self._smsEntry.get_buffer().get_char_count()
451 charsLeft = self.MAX_CHAR - entryLength
452 self._letterCountLabel.set_text(str(charsLeft))
454 self._smsButton.set_sensitive(False)
456 self._smsButton.set_sensitive(True)
458 def _on_entry_changed(self, *args):
459 self._update_letter_count()
461 def _on_send(self, *args):
462 self._dialog.response(gtk.RESPONSE_OK)
464 def _on_cancel(self, *args):
465 self._dialog.response(gtk.RESPONSE_CANCEL)
468 class Dialpad(object):
470 def __init__(self, widgetTree, errorDisplay):
471 self._errorDisplay = errorDisplay
472 self._smsDialog = SmsEntryDialog(widgetTree)
474 self._numberdisplay = widgetTree.get_widget("numberdisplay")
475 self._dialButton = widgetTree.get_widget("dial")
476 self._backButton = widgetTree.get_widget("back")
477 self._phonenumber = ""
478 self._prettynumber = ""
481 "on_dial_clicked": self._on_dial_clicked,
482 "on_sms_clicked": self._on_sms_clicked,
483 "on_digit_clicked": self._on_digit_clicked,
484 "on_clear_number": self._on_clear_number,
486 widgetTree.signal_autoconnect(callbackMapping)
488 self._originalLabel = self._backButton.get_label()
489 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
490 self._backTapHandler.on_tap = self._on_backspace
491 self._backTapHandler.on_hold = self._on_clearall
492 self._backTapHandler.on_holding = self._set_clear_button
493 self._backTapHandler.on_cancel = self._reset_back_button
495 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
498 self._dialButton.grab_focus()
499 self._backTapHandler.enable()
502 self._reset_back_button()
503 self._backTapHandler.disable()
505 def number_selected(self, action, number, message):
507 @note Actual dial function is patched in later
509 raise NotImplementedError("Horrible unknown error has occurred")
511 def get_number(self):
512 return self._phonenumber
514 def set_number(self, number):
516 Set the number to dial
519 self._phonenumber = make_ugly(number)
520 self._prettynumber = make_pretty(self._phonenumber)
521 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
523 self._errorDisplay.push_exception()
532 def load_settings(self, config, section):
535 def save_settings(self, config, section):
537 @note Thread Agnostic
541 def _on_sms_clicked(self, widget):
542 action = PhoneTypeSelector.ACTION_SEND_SMS
543 phoneNumber = self.get_number()
545 message = self._smsDialog.run(phoneNumber, "", self._window)
548 action = PhoneTypeSelector.ACTION_CANCEL
550 if action == PhoneTypeSelector.ACTION_CANCEL:
552 self.number_selected(action, phoneNumber, message)
554 def _on_dial_clicked(self, widget):
555 action = PhoneTypeSelector.ACTION_DIAL
556 phoneNumber = self.get_number()
558 self.number_selected(action, phoneNumber, message)
560 def _on_clear_number(self, *args):
563 def _on_digit_clicked(self, widget):
564 self.set_number(self._phonenumber + widget.get_name()[-1])
566 def _on_backspace(self, taps):
567 self.set_number(self._phonenumber[:-taps])
568 self._reset_back_button()
570 def _on_clearall(self, taps):
572 self._reset_back_button()
575 def _set_clear_button(self):
576 self._backButton.set_label("gtk-clear")
578 def _reset_back_button(self):
579 self._backButton.set_label(self._originalLabel)
582 class AccountInfo(object):
584 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
585 self._errorDisplay = errorDisplay
586 self._backend = backend
587 self._isPopulated = False
588 self._alarmHandler = alarmHandler
589 self._notifyOnMissed = False
590 self._notifyOnVoicemail = False
591 self._notifyOnSms = False
593 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
594 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
595 self._callbackCombo = widgetTree.get_widget("callbackcombo")
596 self._onCallbackentryChangedId = 0
598 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
599 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
600 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
601 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
602 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
603 self._onNotifyToggled = 0
604 self._onMinutesChanged = 0
605 self._onMissedToggled = 0
606 self._onVoicemailToggled = 0
607 self._onSmsToggled = 0
608 self._applyAlarmTimeoutId = None
610 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
611 self._defaultCallback = ""
614 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
616 self._accountViewNumberDisplay.set_use_markup(True)
617 self.set_account_number("")
619 self._callbackList.clear()
620 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
622 if self._alarmHandler is not None:
623 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
624 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
625 self._missedCheckbox.set_active(self._notifyOnMissed)
626 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
627 self._smsCheckbox.set_active(self._notifyOnSms)
629 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
630 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_changed)
631 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
632 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
633 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
635 self._notifyCheckbox.set_sensitive(False)
636 self._minutesEntryButton.set_sensitive(False)
637 self._missedCheckbox.set_sensitive(False)
638 self._voicemailCheckbox.set_sensitive(False)
639 self._smsCheckbox.set_sensitive(False)
641 self.update(force=True)
644 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
645 self._onCallbackentryChangedId = 0
647 if self._alarmHandler is not None:
648 self._notifyCheckbox.disconnect(self._onNotifyToggled)
649 self._minutesEntryButton.disconnect(self._onMinutesChanged)
650 self._missedCheckbox.disconnect(self._onNotifyToggled)
651 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
652 self._smsCheckbox.disconnect(self._onNotifyToggled)
653 self._onNotifyToggled = 0
654 self._onMinutesChanged = 0
655 self._onMissedToggled = 0
656 self._onVoicemailToggled = 0
657 self._onSmsToggled = 0
659 self._notifyCheckbox.set_sensitive(True)
660 self._minutesEntryButton.set_sensitive(True)
661 self._missedCheckbox.set_sensitive(True)
662 self._voicemailCheckbox.set_sensitive(True)
663 self._smsCheckbox.set_sensitive(True)
666 self._callbackList.clear()
668 def get_selected_callback_number(self):
669 return make_ugly(self._callbackCombo.get_child().get_text())
671 def set_account_number(self, number):
673 Displays current account number
675 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
677 def update(self, force = False):
678 if not force and self._isPopulated:
680 self._populate_callback_combo()
681 self.set_account_number(self._backend.get_account_number())
685 self._callbackCombo.get_child().set_text("")
686 self.set_account_number("")
687 self._isPopulated = False
689 def save_everything(self):
690 raise NotImplementedError
694 return "Account Info"
696 def load_settings(self, config, section):
697 self._defaultCallback = config.get(section, "callback")
698 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
699 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
700 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
702 def save_settings(self, config, section):
704 @note Thread Agnostic
706 callback = self.get_selected_callback_number()
707 config.set(section, "callback", callback)
708 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
709 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
710 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
712 def _populate_callback_combo(self):
713 self._isPopulated = True
714 self._callbackList.clear()
716 callbackNumbers = self._backend.get_callback_numbers()
717 except StandardError, e:
718 self._errorDisplay.push_exception()
719 self._isPopulated = False
722 for number, description in callbackNumbers.iteritems():
723 self._callbackList.append((make_pretty(number),))
725 self._callbackCombo.set_model(self._callbackList)
726 self._callbackCombo.set_text_column(0)
727 #callbackNumber = self._backend.get_callback_number()
728 callbackNumber = self._defaultCallback
729 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
731 def _set_callback_number(self, number):
733 if not self._backend.is_valid_syntax(number):
734 self._errorDisplay.push_message("%s is not a valid callback number" % number)
735 elif number == self._backend.get_callback_number():
737 "Callback number already is %s" % (
738 self._backend.get_callback_number(),
742 self._backend.set_callback_number(number)
743 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
744 make_pretty(number), make_pretty(self._backend.get_callback_number())
747 "Callback number set to %s" % (
748 self._backend.get_callback_number(),
751 except StandardError, e:
752 self._errorDisplay.push_exception()
754 def _update_alarm_settings(self, recurrence):
756 isEnabled = self._notifyCheckbox.get_active()
757 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
758 self._alarmHandler.apply_settings(isEnabled, recurrence)
760 self.save_everything()
761 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
762 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
764 def _on_callbackentry_changed(self, *args):
765 text = self.get_selected_callback_number()
766 number = make_ugly(text)
767 self._set_callback_number(number)
769 def _on_notify_toggled(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_minutes_changed(self, *args):
776 recurrence = hildonize.request_number(
777 self._window, "Minutes", (1, 50), self._alarmHandler.recurrence
779 self._update_alarm_settings(recurrence)
781 def _on_apply_timeout(self, *args):
782 self._applyAlarmTimeoutId = None
784 self._update_alarm_settings(self._alarmHandler.recurrence)
787 def _on_missed_toggled(self, *args):
788 self._notifyOnMissed = self._missedCheckbox.get_active()
789 self.save_everything()
791 def _on_voicemail_toggled(self, *args):
792 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
793 self.save_everything()
795 def _on_sms_toggled(self, *args):
796 self._notifyOnSms = self._smsCheckbox.get_active()
797 self.save_everything()
800 class RecentCallsView(object):
807 def __init__(self, widgetTree, backend, errorDisplay):
808 self._errorDisplay = errorDisplay
809 self._backend = backend
811 self._isPopulated = False
812 self._recentmodel = gtk.ListStore(
813 gobject.TYPE_STRING, # number
814 gobject.TYPE_STRING, # date
815 gobject.TYPE_STRING, # action
816 gobject.TYPE_STRING, # from
818 self._recentview = widgetTree.get_widget("recentview")
819 self._recentviewselection = None
820 self._onRecentviewRowActivatedId = 0
822 textrenderer = gtk.CellRendererText()
823 textrenderer.set_property("yalign", 0)
824 self._dateColumn = gtk.TreeViewColumn("Date")
825 self._dateColumn.pack_start(textrenderer, expand=True)
826 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
828 textrenderer = gtk.CellRendererText()
829 textrenderer.set_property("yalign", 0)
830 self._actionColumn = gtk.TreeViewColumn("Action")
831 self._actionColumn.pack_start(textrenderer, expand=True)
832 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
834 textrenderer = gtk.CellRendererText()
835 textrenderer.set_property("yalign", 0)
836 hildonize.set_cell_thumb_selectable(textrenderer)
837 self._nameColumn = gtk.TreeViewColumn("From")
838 self._nameColumn.pack_start(textrenderer, expand=True)
839 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
840 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
842 textrenderer = gtk.CellRendererText()
843 textrenderer.set_property("yalign", 0)
844 hildonize.set_cell_thumb_selectable(textrenderer)
845 self._numberColumn = gtk.TreeViewColumn("Number")
846 self._numberColumn.pack_start(textrenderer, expand=True)
847 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
849 self._window = gtk_toolbox.find_parent_window(self._recentview)
850 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
852 self._updateSink = gtk_toolbox.threaded_stage(
854 self._idly_populate_recentview,
855 gtk_toolbox.null_sink(),
860 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
861 self._recentview.set_model(self._recentmodel)
863 self._recentview.append_column(self._dateColumn)
864 self._recentview.append_column(self._actionColumn)
865 self._recentview.append_column(self._numberColumn)
866 self._recentview.append_column(self._nameColumn)
867 self._recentviewselection = self._recentview.get_selection()
868 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
870 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
873 self._recentview.disconnect(self._onRecentviewRowActivatedId)
877 self._recentview.remove_column(self._dateColumn)
878 self._recentview.remove_column(self._actionColumn)
879 self._recentview.remove_column(self._nameColumn)
880 self._recentview.remove_column(self._numberColumn)
881 self._recentview.set_model(None)
883 def number_selected(self, action, number, message):
885 @note Actual dial function is patched in later
887 raise NotImplementedError("Horrible unknown error has occurred")
889 def update(self, force = False):
890 if not force and self._isPopulated:
892 self._updateSink.send(())
896 self._isPopulated = False
897 self._recentmodel.clear()
901 return "Recent Calls"
903 def load_settings(self, config, section):
906 def save_settings(self, config, section):
908 @note Thread Agnostic
912 def _idly_populate_recentview(self):
913 self._recentmodel.clear()
914 self._isPopulated = True
917 recentItems = self._backend.get_recent()
918 except StandardError, e:
919 self._errorDisplay.push_exception_with_lock()
920 self._isPopulated = False
923 for personName, phoneNumber, date, action in recentItems:
925 personName = "Unknown"
926 date = abbrev_relative_date(date)
927 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
928 prettyNumber = make_pretty(prettyNumber)
929 item = (prettyNumber, date, action.capitalize(), personName)
930 with gtk_toolbox.gtk_lock():
931 self._recentmodel.append(item)
935 def _on_recentview_row_activated(self, treeview, path, view_column):
936 model, itr = self._recentviewselection.get_selected()
940 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
941 number = make_ugly(number)
942 contactPhoneNumbers = [("Phone", number)]
943 description = self._recentmodel.get_value(itr, self.FROM_IDX)
945 action, phoneNumber, message = self._phoneTypeSelector.run(
947 message = description,
948 parent = self._window,
950 if action == PhoneTypeSelector.ACTION_CANCEL:
952 assert phoneNumber, "A lack of phone number exists"
954 self.number_selected(action, phoneNumber, message)
955 self._recentviewselection.unselect_all()
958 class MessagesView(object):
965 def __init__(self, widgetTree, backend, errorDisplay):
966 self._errorDisplay = errorDisplay
967 self._backend = backend
969 self._isPopulated = False
970 self._messagemodel = gtk.ListStore(
971 gobject.TYPE_STRING, # number
972 gobject.TYPE_STRING, # date
973 gobject.TYPE_STRING, # header
974 gobject.TYPE_STRING, # message
976 self._messageview = widgetTree.get_widget("messages_view")
977 self._messageviewselection = None
978 self._onMessageviewRowActivatedId = 0
980 self._messageRenderer = gtk.CellRendererText()
981 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
982 self._messageRenderer.set_property("wrap-width", 500)
983 self._messageColumn = gtk.TreeViewColumn("Messages")
984 self._messageColumn.pack_start(self._messageRenderer, expand=True)
985 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
986 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
988 self._window = gtk_toolbox.find_parent_window(self._messageview)
989 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
991 self._updateSink = gtk_toolbox.threaded_stage(
993 self._idly_populate_messageview,
994 gtk_toolbox.null_sink(),
999 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1000 self._messageview.set_model(self._messagemodel)
1002 self._messageview.append_column(self._messageColumn)
1003 self._messageviewselection = self._messageview.get_selection()
1004 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1006 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1009 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1013 self._messageview.remove_column(self._messageColumn)
1014 self._messageview.set_model(None)
1016 def number_selected(self, action, number, message):
1018 @note Actual dial function is patched in later
1020 raise NotImplementedError("Horrible unknown error has occurred")
1022 def update(self, force = False):
1023 if not force and self._isPopulated:
1025 self._updateSink.send(())
1029 self._isPopulated = False
1030 self._messagemodel.clear()
1036 def load_settings(self, config, section):
1039 def save_settings(self, config, section):
1041 @note Thread Agnostic
1045 def _idly_populate_messageview(self):
1046 self._messagemodel.clear()
1047 self._isPopulated = True
1050 messageItems = self._backend.get_messages()
1051 except StandardError, e:
1052 self._errorDisplay.push_exception_with_lock()
1053 self._isPopulated = False
1056 for header, number, relativeDate, message in messageItems:
1057 prettyNumber = number[2:] if number.startswith("+1") else number
1058 prettyNumber = make_pretty(prettyNumber)
1059 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1060 number = make_ugly(number)
1061 row = (number, relativeDate, header, message)
1062 with gtk_toolbox.gtk_lock():
1063 self._messagemodel.append(row)
1067 def _on_messageview_row_activated(self, treeview, path, view_column):
1068 model, itr = self._messageviewselection.get_selected()
1072 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1073 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1075 action, phoneNumber, message = self._phoneTypeSelector.run(
1076 contactPhoneNumbers,
1077 message = description,
1078 parent = self._window,
1080 if action == PhoneTypeSelector.ACTION_CANCEL:
1082 assert phoneNumber, "A lock of phone number exists"
1084 self.number_selected(action, phoneNumber, message)
1085 self._messageviewselection.unselect_all()
1088 class ContactsView(object):
1090 def __init__(self, widgetTree, backend, errorDisplay):
1091 self._errorDisplay = errorDisplay
1092 self._backend = backend
1094 self._addressBook = None
1095 self._selectedComboIndex = 0
1096 self._addressBookFactories = [null_backend.NullAddressBook()]
1098 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1099 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1101 self._isPopulated = False
1102 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1103 self._contactsviewselection = None
1104 self._contactsview = widgetTree.get_widget("contactsview")
1106 self._contactColumn = gtk.TreeViewColumn("Contact")
1107 displayContactSource = False
1108 if displayContactSource:
1109 textrenderer = gtk.CellRendererText()
1110 self._contactColumn.pack_start(textrenderer, expand=False)
1111 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1112 textrenderer = gtk.CellRendererText()
1113 hildonize.set_cell_thumb_selectable(textrenderer)
1114 self._contactColumn.pack_start(textrenderer, expand=True)
1115 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1116 textrenderer = gtk.CellRendererText()
1117 self._contactColumn.pack_start(textrenderer, expand=True)
1118 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1119 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1120 self._contactColumn.set_sort_column_id(1)
1121 self._contactColumn.set_visible(True)
1123 self._onContactsviewRowActivatedId = 0
1124 self._onAddressbookComboChangedId = 0
1125 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1126 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1128 self._updateSink = gtk_toolbox.threaded_stage(
1130 self._idly_populate_contactsview,
1131 gtk_toolbox.null_sink(),
1136 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1138 self._contactsview.set_model(self._contactsmodel)
1139 self._contactsview.append_column(self._contactColumn)
1140 self._contactsviewselection = self._contactsview.get_selection()
1141 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1143 self._booksList.clear()
1144 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1145 if factoryName and bookName:
1146 entryName = "%s: %s" % (factoryName, bookName)
1148 entryName = factoryName
1150 entryName = bookName
1152 entryName = "Bad name (%d)" % factoryId
1153 row = (str(factoryId), bookId, entryName)
1154 self._booksList.append(row)
1156 self._booksSelectionBox.set_model(self._booksList)
1157 cell = gtk.CellRendererText()
1158 self._booksSelectionBox.pack_start(cell, True)
1159 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1161 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1162 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1164 if len(self._booksList) <= self._selectedComboIndex:
1165 self._selectedComboIndex = 0
1166 self._booksSelectionBox.set_active(self._selectedComboIndex)
1169 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1170 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1174 self._booksSelectionBox.clear()
1175 self._booksSelectionBox.set_model(None)
1176 self._contactsview.set_model(None)
1177 self._contactsview.remove_column(self._contactColumn)
1179 def number_selected(self, action, number, message):
1181 @note Actual dial function is patched in later
1183 raise NotImplementedError("Horrible unknown error has occurred")
1185 def get_addressbooks(self):
1187 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1189 for i, factory in enumerate(self._addressBookFactories):
1190 for bookFactory, bookId, bookName in factory.get_addressbooks():
1191 yield (str(i), bookId), (factory.factory_name(), bookName)
1193 def open_addressbook(self, bookFactoryId, bookId):
1194 bookFactoryIndex = int(bookFactoryId)
1195 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1197 forceUpdate = True if addressBook is not self._addressBook else False
1199 self._addressBook = addressBook
1200 self.update(force=forceUpdate)
1202 def update(self, force = False):
1203 if not force and self._isPopulated:
1205 self._updateSink.send(())
1209 self._isPopulated = False
1210 self._contactsmodel.clear()
1211 for factory in self._addressBookFactories:
1212 factory.clear_caches()
1213 self._addressBook.clear_caches()
1215 def append(self, book):
1216 self._addressBookFactories.append(book)
1218 def extend(self, books):
1219 self._addressBookFactories.extend(books)
1225 def load_settings(self, config, sectionName):
1227 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1228 except ConfigParser.NoOptionError:
1229 self._selectedComboIndex = 0
1231 def save_settings(self, config, sectionName):
1232 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1234 def _idly_populate_contactsview(self):
1236 while addressBook is not self._addressBook:
1237 addressBook = self._addressBook
1238 with gtk_toolbox.gtk_lock():
1239 self._contactsview.set_model(None)
1243 contacts = addressBook.get_contacts()
1244 except StandardError, e:
1246 self._isPopulated = False
1247 self._errorDisplay.push_exception_with_lock()
1248 for contactId, contactName in contacts:
1249 contactType = (addressBook.contact_source_short_name(contactId), )
1250 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1252 with gtk_toolbox.gtk_lock():
1253 self._contactsview.set_model(self._contactsmodel)
1255 self._isPopulated = True
1258 def _on_addressbook_combo_changed(self, *args, **kwds):
1259 itr = self._booksSelectionBox.get_active_iter()
1262 self._selectedComboIndex = self._booksSelectionBox.get_active()
1263 selectedFactoryId = self._booksList.get_value(itr, 0)
1264 selectedBookId = self._booksList.get_value(itr, 1)
1265 self.open_addressbook(selectedFactoryId, selectedBookId)
1267 def _on_contactsview_row_activated(self, treeview, path, view_column):
1268 model, itr = self._contactsviewselection.get_selected()
1272 contactId = self._contactsmodel.get_value(itr, 3)
1273 contactName = self._contactsmodel.get_value(itr, 1)
1275 contactDetails = self._addressBook.get_contact_details(contactId)
1276 except StandardError, e:
1278 self._errorDisplay.push_exception()
1279 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1281 if len(contactPhoneNumbers) == 0:
1284 action, phoneNumber, message = self._phoneTypeSelector.run(
1285 contactPhoneNumbers,
1286 message = contactName,
1287 parent = self._window,
1289 if action == PhoneTypeSelector.ACTION_CANCEL:
1291 assert phoneNumber, "A lack of phone number exists"
1293 self.number_selected(action, phoneNumber, message)
1294 self._contactsviewselection.unselect_all()