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(),
744 self._backend.set_callback_number(number)
745 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
746 make_pretty(number), make_pretty(self._backend.get_callback_number())
749 "Callback number set to %s" % (
750 self._backend.get_callback_number(),
754 except StandardError, e:
755 self._errorDisplay.push_exception()
757 def _update_alarm_settings(self, recurrence):
759 isEnabled = self._notifyCheckbox.get_active()
760 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
761 self._alarmHandler.apply_settings(isEnabled, recurrence)
763 self.save_everything()
764 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
765 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
767 def _on_callbackentry_changed(self, *args):
768 text = self.get_selected_callback_number()
769 number = make_ugly(text)
770 self._set_callback_number(number)
772 def _on_notify_toggled(self, *args):
773 if self._applyAlarmTimeoutId is not None:
774 gobject.source_remove(self._applyAlarmTimeoutId)
775 self._applyAlarmTimeoutId = None
776 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
778 def _on_minutes_changed(self, *args):
779 recurrence = hildonize.request_number(
780 self._window, "Minutes", (1, 50), self._alarmHandler.recurrence
782 self._update_alarm_settings(recurrence)
784 def _on_apply_timeout(self, *args):
785 self._applyAlarmTimeoutId = None
787 self._update_alarm_settings(self._alarmHandler.recurrence)
790 def _on_missed_toggled(self, *args):
791 self._notifyOnMissed = self._missedCheckbox.get_active()
792 self.save_everything()
794 def _on_voicemail_toggled(self, *args):
795 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
796 self.save_everything()
798 def _on_sms_toggled(self, *args):
799 self._notifyOnSms = self._smsCheckbox.get_active()
800 self.save_everything()
803 class RecentCallsView(object):
810 def __init__(self, widgetTree, backend, errorDisplay):
811 self._errorDisplay = errorDisplay
812 self._backend = backend
814 self._isPopulated = False
815 self._recentmodel = gtk.ListStore(
816 gobject.TYPE_STRING, # number
817 gobject.TYPE_STRING, # date
818 gobject.TYPE_STRING, # action
819 gobject.TYPE_STRING, # from
821 self._recentview = widgetTree.get_widget("recentview")
822 self._recentviewselection = None
823 self._onRecentviewRowActivatedId = 0
825 textrenderer = gtk.CellRendererText()
826 textrenderer.set_property("yalign", 0)
827 self._dateColumn = gtk.TreeViewColumn("Date")
828 self._dateColumn.pack_start(textrenderer, expand=True)
829 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
831 textrenderer = gtk.CellRendererText()
832 textrenderer.set_property("yalign", 0)
833 self._actionColumn = gtk.TreeViewColumn("Action")
834 self._actionColumn.pack_start(textrenderer, expand=True)
835 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
837 textrenderer = gtk.CellRendererText()
838 textrenderer.set_property("yalign", 0)
839 hildonize.set_cell_thumb_selectable(textrenderer)
840 self._nameColumn = gtk.TreeViewColumn("From")
841 self._nameColumn.pack_start(textrenderer, expand=True)
842 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
843 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
845 textrenderer = gtk.CellRendererText()
846 textrenderer.set_property("yalign", 0)
847 hildonize.set_cell_thumb_selectable(textrenderer)
848 self._numberColumn = gtk.TreeViewColumn("Number")
849 self._numberColumn.pack_start(textrenderer, expand=True)
850 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
852 self._window = gtk_toolbox.find_parent_window(self._recentview)
853 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
855 self._updateSink = gtk_toolbox.threaded_stage(
857 self._idly_populate_recentview,
858 gtk_toolbox.null_sink(),
863 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
864 self._recentview.set_model(self._recentmodel)
866 self._recentview.append_column(self._dateColumn)
867 self._recentview.append_column(self._actionColumn)
868 self._recentview.append_column(self._numberColumn)
869 self._recentview.append_column(self._nameColumn)
870 self._recentviewselection = self._recentview.get_selection()
871 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
873 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
876 self._recentview.disconnect(self._onRecentviewRowActivatedId)
880 self._recentview.remove_column(self._dateColumn)
881 self._recentview.remove_column(self._actionColumn)
882 self._recentview.remove_column(self._nameColumn)
883 self._recentview.remove_column(self._numberColumn)
884 self._recentview.set_model(None)
886 def number_selected(self, action, number, message):
888 @note Actual dial function is patched in later
890 raise NotImplementedError("Horrible unknown error has occurred")
892 def update(self, force = False):
893 if not force and self._isPopulated:
895 self._updateSink.send(())
899 self._isPopulated = False
900 self._recentmodel.clear()
904 return "Recent Calls"
906 def load_settings(self, config, section):
909 def save_settings(self, config, section):
911 @note Thread Agnostic
915 def _idly_populate_recentview(self):
916 self._recentmodel.clear()
917 self._isPopulated = True
920 recentItems = self._backend.get_recent()
921 except StandardError, e:
922 self._errorDisplay.push_exception_with_lock()
923 self._isPopulated = False
926 for personName, phoneNumber, date, action in recentItems:
928 personName = "Unknown"
929 date = abbrev_relative_date(date)
930 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
931 prettyNumber = make_pretty(prettyNumber)
932 item = (prettyNumber, date, action.capitalize(), personName)
933 with gtk_toolbox.gtk_lock():
934 self._recentmodel.append(item)
938 def _on_recentview_row_activated(self, treeview, path, view_column):
939 model, itr = self._recentviewselection.get_selected()
943 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
944 number = make_ugly(number)
945 contactPhoneNumbers = [("Phone", number)]
946 description = self._recentmodel.get_value(itr, self.FROM_IDX)
948 action, phoneNumber, message = self._phoneTypeSelector.run(
950 message = description,
951 parent = self._window,
953 if action == PhoneTypeSelector.ACTION_CANCEL:
955 assert phoneNumber, "A lack of phone number exists"
957 self.number_selected(action, phoneNumber, message)
958 self._recentviewselection.unselect_all()
961 class MessagesView(object):
968 def __init__(self, widgetTree, backend, errorDisplay):
969 self._errorDisplay = errorDisplay
970 self._backend = backend
972 self._isPopulated = False
973 self._messagemodel = gtk.ListStore(
974 gobject.TYPE_STRING, # number
975 gobject.TYPE_STRING, # date
976 gobject.TYPE_STRING, # header
977 gobject.TYPE_STRING, # message
979 self._messageview = widgetTree.get_widget("messages_view")
980 self._messageviewselection = None
981 self._onMessageviewRowActivatedId = 0
983 self._messageRenderer = gtk.CellRendererText()
984 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
985 self._messageRenderer.set_property("wrap-width", 500)
986 self._messageColumn = gtk.TreeViewColumn("Messages")
987 self._messageColumn.pack_start(self._messageRenderer, expand=True)
988 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
989 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
991 self._window = gtk_toolbox.find_parent_window(self._messageview)
992 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
994 self._updateSink = gtk_toolbox.threaded_stage(
996 self._idly_populate_messageview,
997 gtk_toolbox.null_sink(),
1002 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1003 self._messageview.set_model(self._messagemodel)
1005 self._messageview.append_column(self._messageColumn)
1006 self._messageviewselection = self._messageview.get_selection()
1007 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1009 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1012 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1016 self._messageview.remove_column(self._messageColumn)
1017 self._messageview.set_model(None)
1019 def number_selected(self, action, number, message):
1021 @note Actual dial function is patched in later
1023 raise NotImplementedError("Horrible unknown error has occurred")
1025 def update(self, force = False):
1026 if not force and self._isPopulated:
1028 self._updateSink.send(())
1032 self._isPopulated = False
1033 self._messagemodel.clear()
1039 def load_settings(self, config, section):
1042 def save_settings(self, config, section):
1044 @note Thread Agnostic
1048 def _idly_populate_messageview(self):
1049 self._messagemodel.clear()
1050 self._isPopulated = True
1053 messageItems = self._backend.get_messages()
1054 except StandardError, e:
1055 self._errorDisplay.push_exception_with_lock()
1056 self._isPopulated = False
1059 for header, number, relativeDate, message in messageItems:
1060 prettyNumber = number[2:] if number.startswith("+1") else number
1061 prettyNumber = make_pretty(prettyNumber)
1062 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1063 number = make_ugly(number)
1064 row = (number, relativeDate, header, message)
1065 with gtk_toolbox.gtk_lock():
1066 self._messagemodel.append(row)
1070 def _on_messageview_row_activated(self, treeview, path, view_column):
1071 model, itr = self._messageviewselection.get_selected()
1075 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1076 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1078 action, phoneNumber, message = self._phoneTypeSelector.run(
1079 contactPhoneNumbers,
1080 message = description,
1081 parent = self._window,
1083 if action == PhoneTypeSelector.ACTION_CANCEL:
1085 assert phoneNumber, "A lock of phone number exists"
1087 self.number_selected(action, phoneNumber, message)
1088 self._messageviewselection.unselect_all()
1091 class ContactsView(object):
1093 def __init__(self, widgetTree, backend, errorDisplay):
1094 self._errorDisplay = errorDisplay
1095 self._backend = backend
1097 self._addressBook = None
1098 self._selectedComboIndex = 0
1099 self._addressBookFactories = [null_backend.NullAddressBook()]
1101 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1102 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1104 self._isPopulated = False
1105 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1106 self._contactsviewselection = None
1107 self._contactsview = widgetTree.get_widget("contactsview")
1109 self._contactColumn = gtk.TreeViewColumn("Contact")
1110 displayContactSource = False
1111 if displayContactSource:
1112 textrenderer = gtk.CellRendererText()
1113 self._contactColumn.pack_start(textrenderer, expand=False)
1114 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1115 textrenderer = gtk.CellRendererText()
1116 hildonize.set_cell_thumb_selectable(textrenderer)
1117 self._contactColumn.pack_start(textrenderer, expand=True)
1118 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1119 textrenderer = gtk.CellRendererText()
1120 self._contactColumn.pack_start(textrenderer, expand=True)
1121 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1122 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1123 self._contactColumn.set_sort_column_id(1)
1124 self._contactColumn.set_visible(True)
1126 self._onContactsviewRowActivatedId = 0
1127 self._onAddressbookComboChangedId = 0
1128 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1129 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1131 self._updateSink = gtk_toolbox.threaded_stage(
1133 self._idly_populate_contactsview,
1134 gtk_toolbox.null_sink(),
1139 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1141 self._contactsview.set_model(self._contactsmodel)
1142 self._contactsview.append_column(self._contactColumn)
1143 self._contactsviewselection = self._contactsview.get_selection()
1144 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1146 self._booksList.clear()
1147 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1148 if factoryName and bookName:
1149 entryName = "%s: %s" % (factoryName, bookName)
1151 entryName = factoryName
1153 entryName = bookName
1155 entryName = "Bad name (%d)" % factoryId
1156 row = (str(factoryId), bookId, entryName)
1157 self._booksList.append(row)
1159 self._booksSelectionBox.set_model(self._booksList)
1160 cell = gtk.CellRendererText()
1161 self._booksSelectionBox.pack_start(cell, True)
1162 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1164 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1165 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1167 if len(self._booksList) <= self._selectedComboIndex:
1168 self._selectedComboIndex = 0
1169 self._booksSelectionBox.set_active(self._selectedComboIndex)
1172 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1173 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1177 self._booksSelectionBox.clear()
1178 self._booksSelectionBox.set_model(None)
1179 self._contactsview.set_model(None)
1180 self._contactsview.remove_column(self._contactColumn)
1182 def number_selected(self, action, number, message):
1184 @note Actual dial function is patched in later
1186 raise NotImplementedError("Horrible unknown error has occurred")
1188 def get_addressbooks(self):
1190 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1192 for i, factory in enumerate(self._addressBookFactories):
1193 for bookFactory, bookId, bookName in factory.get_addressbooks():
1194 yield (str(i), bookId), (factory.factory_name(), bookName)
1196 def open_addressbook(self, bookFactoryId, bookId):
1197 bookFactoryIndex = int(bookFactoryId)
1198 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1200 forceUpdate = True if addressBook is not self._addressBook else False
1202 self._addressBook = addressBook
1203 self.update(force=forceUpdate)
1205 def update(self, force = False):
1206 if not force and self._isPopulated:
1208 self._updateSink.send(())
1212 self._isPopulated = False
1213 self._contactsmodel.clear()
1214 for factory in self._addressBookFactories:
1215 factory.clear_caches()
1216 self._addressBook.clear_caches()
1218 def append(self, book):
1219 self._addressBookFactories.append(book)
1221 def extend(self, books):
1222 self._addressBookFactories.extend(books)
1228 def load_settings(self, config, sectionName):
1230 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1231 except ConfigParser.NoOptionError:
1232 self._selectedComboIndex = 0
1234 def save_settings(self, config, sectionName):
1235 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1237 def _idly_populate_contactsview(self):
1239 while addressBook is not self._addressBook:
1240 addressBook = self._addressBook
1241 with gtk_toolbox.gtk_lock():
1242 self._contactsview.set_model(None)
1246 contacts = addressBook.get_contacts()
1247 except StandardError, e:
1249 self._isPopulated = False
1250 self._errorDisplay.push_exception_with_lock()
1251 for contactId, contactName in contacts:
1252 contactType = (addressBook.contact_source_short_name(contactId), )
1253 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1255 with gtk_toolbox.gtk_lock():
1256 self._contactsview.set_model(self._contactsmodel)
1258 self._isPopulated = True
1261 def _on_addressbook_combo_changed(self, *args, **kwds):
1262 itr = self._booksSelectionBox.get_active_iter()
1265 self._selectedComboIndex = self._booksSelectionBox.get_active()
1266 selectedFactoryId = self._booksList.get_value(itr, 0)
1267 selectedBookId = self._booksList.get_value(itr, 1)
1268 self.open_addressbook(selectedFactoryId, selectedBookId)
1270 def _on_contactsview_row_activated(self, treeview, path, view_column):
1271 model, itr = self._contactsviewselection.get_selected()
1275 contactId = self._contactsmodel.get_value(itr, 3)
1276 contactName = self._contactsmodel.get_value(itr, 1)
1278 contactDetails = self._addressBook.get_contact_details(contactId)
1279 except StandardError, e:
1281 self._errorDisplay.push_exception()
1282 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1284 if len(contactPhoneNumbers) == 0:
1287 action, phoneNumber, message = self._phoneTypeSelector.run(
1288 contactPhoneNumbers,
1289 message = contactName,
1290 parent = self._window,
1292 if action == PhoneTypeSelector.ACTION_CANCEL:
1294 assert phoneNumber, "A lack of phone number exists"
1296 self.number_selected(action, phoneNumber, message)
1297 self._contactsviewselection.unselect_all()