4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Collapse voicemails
24 from __future__ import with_statement
37 from backends import gv_backend
38 from backends import null_backend
41 _moduleLogger = logging.getLogger("gv_views")
44 def make_ugly(prettynumber):
46 function to take a phone number and strip out all non-numeric
49 >>> make_ugly("+012-(345)-678-90")
52 return normalize_number(prettynumber)
55 def normalize_number(prettynumber):
57 function to take a phone number and strip out all non-numeric
60 >>> normalize_number("+012-(345)-678-90")
62 >>> normalize_number("1-(345)-678-9000")
64 >>> normalize_number("+1-(345)-678-9000")
67 uglynumber = re.sub('[^0-9+]', '', prettynumber)
72 def _make_pretty_with_areacodde(phonenumber):
73 prettynumber = "(%s)" % (phonenumber[0:3], )
74 if 3 < len(phonenumber):
75 prettynumber += " %s" % (phonenumber[3:6], )
76 if 6 < len(phonenumber):
77 prettynumber += "-%s" % (phonenumber[6:], )
81 def _make_pretty_local(phonenumber):
82 prettynumber = "%s" % (phonenumber[0:3], )
83 if 3 < len(phonenumber):
84 prettynumber += "-%s" % (phonenumber[3:], )
88 def _make_pretty_international(phonenumber):
89 prettynumber = phonenumber
90 if phonenumber.startswith("0"):
91 prettynumber = "+%s " % (phonenumber[0:3], )
92 if 3 < len(phonenumber):
93 prettynumber += _make_pretty_with_areacodde(phonenumber[3:])
94 if phonenumber.startswith("1"):
96 prettynumber += _make_pretty_with_areacodde(phonenumber[1:])
100 def make_pretty(phonenumber):
102 Function to take a phone number and return the pretty version
104 if phonenumber begins with 0:
106 if phonenumber begins with 1: ( for gizmo callback numbers )
108 if phonenumber is 13 digits:
110 if phonenumber is 10 digits:
112 >>> make_pretty("12")
114 >>> make_pretty("1234567")
116 >>> make_pretty("2345678901")
118 >>> make_pretty("12345678901")
120 >>> make_pretty("01234567890")
122 >>> make_pretty("+01234567890")
124 >>> make_pretty("+12")
126 >>> make_pretty("+123")
128 >>> make_pretty("+1234")
131 if phonenumber is None or phonenumber is "":
134 phonenumber = normalize_number(phonenumber)
136 if phonenumber[0] == "+":
137 prettynumber = _make_pretty_international(phonenumber[1:])
138 if not prettynumber.startswith("+"):
139 prettynumber = "+"+prettynumber
140 elif 8 < len(phonenumber) and phonenumber[0] in ("0", "1"):
141 prettynumber = _make_pretty_international(phonenumber)
142 elif 7 < len(phonenumber):
143 prettynumber = _make_pretty_with_areacodde(phonenumber)
144 elif 3 < len(phonenumber):
145 prettynumber = _make_pretty_local(phonenumber)
147 prettynumber = phonenumber
148 return prettynumber.strip()
151 def abbrev_relative_date(date):
153 >>> abbrev_relative_date("42 hours ago")
155 >>> abbrev_relative_date("2 days ago")
157 >>> abbrev_relative_date("4 weeks ago")
160 parts = date.split(" ")
161 return "%s %s" % (parts[0], parts[1][0])
164 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
167 numLines = len(messageLines)
168 for line in messageLines[0:min(maxLines, numLines)]:
169 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
170 allowedLines = maxLines - lines
171 acceptedLines = min(allowedLines, linesPerLine)
172 acceptedChars = acceptedLines * maxCharsPerLine
174 if acceptedChars < (len(line) + 3):
177 acceptedChars = len(line) # eh, might as well complete the line
179 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
182 lines += acceptedLines
183 if maxLines <= lines:
187 def collapse_message(message, maxCharsPerLine, maxLines):
189 >>> collapse_message("Hello", 60, 2)
191 >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
192 'Hello world how are you doing today? 01234567890123456789012...'
193 >>> collapse_message('''Hello world how are you doing today?
194 ... 01234567890123456789
195 ... 01234567890123456789
196 ... 01234567890123456789
197 ... 01234567890123456789''', 60, 2)
198 'Hello world how are you doing today?\n01234567890123456789'
199 >>> collapse_message('''
200 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
201 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
202 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
203 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
204 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
205 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
206 '\nHello world how are you doing today? 01234567890123456789012...'
208 messageLines = message.split("\n")
209 return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
212 def _get_contact_numbers(backend, contactId, number):
214 contactPhoneNumbers = list(backend.get_contact_details(contactId))
215 uglyContactNumbers = (
216 make_ugly(contactNumber)
217 for (numberDescription, contactNumber) in contactPhoneNumbers
221 number == contactNumber or
222 number[1:] == contactNumber and number.startswith("1") or
223 number[2:] == contactNumber and number.startswith("+1") or
224 number == contactNumber[1:] and contactNumber.startswith("1") or
225 number == contactNumber[2:] and contactNumber.startswith("+1")
227 for contactNumber in uglyContactNumbers
230 defaultIndex = defaultMatches.index(True)
232 contactPhoneNumbers.append(("Other", number))
233 defaultIndex = len(contactPhoneNumbers)-1
235 "Could not find contact %r's number %s among %r" % (
236 contactId, number, contactPhoneNumbers
240 contactPhoneNumbers = [("Phone", number)]
243 return contactPhoneNumbers, defaultIndex
246 class SmsEntryWindow(object):
250 def __init__(self, widgetTree, parent):
251 self._clipboard = gtk.clipboard_get()
252 self._widgetTree = widgetTree
253 self._window = self._widgetTree.get_widget("smsWindow")
254 self._window.connect("delete-event", self._on_delete)
255 self._window.connect("key-press-event", self._on_key_press)
256 self._window.connect("window-state-event", self._on_window_state_change)
257 self._isFullScreen = False
258 self._parent = parent
260 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
261 self._smsButton.connect("clicked", self._on_send)
262 self._dialButton = self._widgetTree.get_widget("dialButton")
263 self._dialButton.connect("clicked", self._on_dial)
265 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
267 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
268 self._messagesView = self._widgetTree.get_widget("smsMessages")
270 textrenderer = gtk.CellRendererText()
271 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
272 textrenderer.set_property("wrap-width", 450)
273 messageColumn = gtk.TreeViewColumn("")
274 messageColumn.pack_start(textrenderer, expand=True)
275 messageColumn.add_attribute(textrenderer, "markup", 0)
276 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
277 self._messagesView.append_column(messageColumn)
278 self._messagesView.set_headers_visible(False)
279 self._messagesView.set_model(self._messagemodel)
280 self._messagesView.set_fixed_height_mode(False)
282 self._conversationView = self._messagesView.get_parent()
283 self._conversationViewPort = self._conversationView.get_parent()
284 self._scrollWindow = self._conversationViewPort.get_parent()
286 self._targetList = self._widgetTree.get_widget("smsTargetList")
287 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
288 self._phoneButton.connect("clicked", self._on_phone)
289 self._smsEntry = self._widgetTree.get_widget("smsEntry")
290 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
291 self._smsEntrySize = None
295 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
296 contactNumbers = list(self._to_contact_numbers(contactDetails))
297 assert contactNumbers
298 contactIndex = defaultIndex if defaultIndex != -1 else 0
299 contact = contactNumbers, contactIndex, messages
300 self._contacts.append(contact)
302 nameLabel = gtk.Label(name)
303 selector = gtk.Button(contactNumbers[0][1])
304 if len(contactNumbers) == 1:
305 selector.set_sensitive(False)
306 removeContact = gtk.Button(stock="gtk-delete")
308 row.pack_start(nameLabel, True, True)
309 row.pack_start(selector, True, True)
310 row.pack_start(removeContact, False, False)
312 self._targetList.pack_start(row)
313 selector.connect("clicked", self._on_choose_phone_n, row)
314 removeContact.connect("clicked", self._on_remove_phone_n, row)
315 self._update_button_state()
316 self._update_context()
318 parentSize = self._parent.get_size()
319 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
321 self._window.present()
323 self._smsEntry.grab_focus()
324 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
326 adjustment = self._scrollWindow.get_vadjustment()
327 adjustment.value = dx
330 del self._contacts[:]
332 for row in list(self._targetList.get_children()):
333 self._targetList.remove(row)
334 self._smsEntry.get_buffer().set_text("")
335 self._update_letter_count()
336 self._update_context()
338 def fullscreen(self):
339 self._window.fullscreen()
341 def unfullscreen(self):
342 self._window.unfullscreen()
344 def _remove_contact(self, contactIndex):
345 del self._contacts[contactIndex]
347 row = list(self._targetList.get_children())[contactIndex]
348 self._targetList.remove(row)
349 self._update_button_state()
350 self._update_context()
352 def _update_letter_count(self):
353 if self._smsEntrySize is None:
354 self._smsEntrySize = self._smsEntry.size_request()
356 self._smsEntry.set_size_request(*self._smsEntrySize)
357 entryLength = self._smsEntry.get_buffer().get_char_count()
359 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
361 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
363 self._letterCountLabel.set_text("%s" % (numCharInText, ))
365 self._update_button_state()
367 def _update_context(self):
368 self._messagemodel.clear()
369 if len(self._contacts) == 0:
370 self._messagesView.hide()
371 self._targetList.hide()
372 self._phoneButton.hide()
373 self._phoneButton.set_label("Error: You shouldn't see this")
374 elif len(self._contacts) == 1:
375 contactNumbers, index, messages = self._contacts[0]
377 self._messagesView.show()
378 for message in messages:
380 self._messagemodel.append(row)
381 messagesSelection = self._messagesView.get_selection()
382 messagesSelection.select_path((len(messages)-1, ))
384 self._messagesView.hide()
385 self._targetList.hide()
386 self._phoneButton.show()
387 self._phoneButton.set_label(contactNumbers[index][1])
388 if 1 < len(contactNumbers):
389 self._phoneButton.set_sensitive(True)
391 self._phoneButton.set_sensitive(False)
393 self._messagesView.hide()
394 self._targetList.show()
395 self._phoneButton.hide()
396 self._phoneButton.set_label("Error: You shouldn't see this")
398 def _update_button_state(self):
399 if len(self._contacts) == 0:
400 self._dialButton.set_sensitive(False)
401 self._smsButton.set_sensitive(False)
402 elif len(self._contacts) == 1:
403 entryLength = self._smsEntry.get_buffer().get_char_count()
405 self._dialButton.set_sensitive(True)
406 self._smsButton.set_sensitive(False)
408 self._dialButton.set_sensitive(False)
409 self._smsButton.set_sensitive(True)
411 self._dialButton.set_sensitive(False)
412 self._smsButton.set_sensitive(True)
414 def _to_contact_numbers(self, contactDetails):
415 for phoneType, phoneNumber in contactDetails:
416 display = " - ".join((make_pretty(phoneNumber), phoneType))
417 yield (phoneNumber, display)
423 def _request_number(self, contactIndex):
424 contactNumbers, index, messages = self._contacts[contactIndex]
425 assert 0 <= index, "%r" % index
427 index = hildonize.touch_selector(
430 (description for (number, description) in contactNumbers),
433 self._contacts[contactIndex] = contactNumbers, index, messages
435 def send_sms(self, numbers, message):
436 raise NotImplementedError()
438 def dial(self, number):
439 raise NotImplementedError()
441 def _on_phone(self, *args):
443 assert len(self._contacts) == 1
444 self._request_number(0)
446 contactNumbers, numberIndex, messages = self._contacts[0]
447 self._phoneButton.set_label(contactNumbers[numberIndex][1])
448 row = list(self._targetList.get_children())[0]
449 phoneButton = list(row.get_children())[1]
450 phoneButton.set_label(contactNumbers[numberIndex][1])
452 _moduleLogger.exception("%s" % str(e))
454 def _on_choose_phone_n(self, button, row):
456 assert 1 < len(self._contacts)
457 targetList = list(self._targetList.get_children())
458 index = targetList.index(row)
459 self._request_number(index)
461 contactNumbers, numberIndex, messages = self._contacts[0]
462 phoneButton = list(row.get_children())[1]
463 phoneButton.set_label(contactNumbers[numberIndex][1])
465 _moduleLogger.exception("%s" % str(e))
467 def _on_remove_phone_n(self, button, row):
469 assert 1 < len(self._contacts)
470 targetList = list(self._targetList.get_children())
471 index = targetList.index(row)
473 del self._contacts[index]
474 self._targetList.remove(row)
475 self._update_context()
476 self._update_button_state()
478 _moduleLogger.exception("%s" % str(e))
480 def _on_entry_changed(self, *args):
481 self._update_letter_count()
483 def _on_send(self, *args):
484 assert 0 < len(self._contacts), "%r" % self._contacts
486 make_ugly(contact[0][contact[1]][0])
487 for contact in self._contacts
490 entryBuffer = self._smsEntry.get_buffer()
491 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
492 enteredMessage = enteredMessage.strip()
493 assert enteredMessage
494 self.send_sms(phoneNumbers, enteredMessage)
497 def _on_dial(self, *args):
498 assert len(self._contacts) == 1, "%r" % self._contacts
499 contact = self._contacts[0]
500 contactNumber = contact[0][contact[1]][0]
501 phoneNumber = make_ugly(contactNumber)
502 self.dial(phoneNumber)
505 def _on_delete(self, *args):
506 self._window.emit_stop_by_name("delete-event")
510 def _on_window_state_change(self, widget, event, *args):
512 @note Hildon specific
515 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
516 self._isFullScreen = True
518 self._isFullScreen = False
520 self._errorDisplay.push_exception()
522 def _on_key_press(self, widget, event):
523 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
526 event.keyval == gtk.keysyms.F6 or
527 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
529 if self._isFullScreen:
530 self._window.unfullscreen()
532 self._window.fullscreen()
533 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
536 for messagePart in self._messagemodel
538 self._clipboard.set_text(str(message))
540 event.keyval == gtk.keysyms.w and
541 event.get_state() & gtk.gdk.CONTROL_MASK
545 event.keyval == gtk.keysyms.q and
546 event.get_state() & gtk.gdk.CONTROL_MASK
548 self._parent.destroy()
550 _moduleLogger.exception(str(e))
553 class Dialpad(object):
555 def __init__(self, widgetTree, errorDisplay):
556 self._clipboard = gtk.clipboard_get()
557 self._errorDisplay = errorDisplay
559 self._numberdisplay = widgetTree.get_widget("numberdisplay")
560 self._okButton = widgetTree.get_widget("dialpadOk")
561 self._backButton = widgetTree.get_widget("back")
562 self._plusButton = widgetTree.get_widget("plus")
563 self._phonenumber = ""
564 self._prettynumber = ""
567 "on_digit_clicked": self._on_digit_clicked,
569 widgetTree.signal_autoconnect(callbackMapping)
570 self._okButton.connect("clicked", self._on_ok_clicked)
571 self._plusButton.connect("clicked", self._on_plus)
573 self._originalLabel = self._backButton.get_label()
574 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
575 self._backTapHandler.on_tap = self._on_backspace
576 self._backTapHandler.on_hold = self._on_clearall
577 self._backTapHandler.on_holding = self._set_clear_button
578 self._backTapHandler.on_cancel = self._reset_back_button
580 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
581 self._keyPressEventId = 0
584 self._okButton.grab_focus()
585 self._backTapHandler.enable()
586 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
589 self._window.disconnect(self._keyPressEventId)
590 self._keyPressEventId = 0
591 self._reset_back_button()
592 self._backTapHandler.disable()
594 def add_contact(self, *args, **kwds):
596 @note Actual dial function is patched in later
598 raise NotImplementedError("Horrible unknown error has occurred")
600 def get_number(self):
601 return self._phonenumber
603 def set_number(self, number):
605 Set the number to dial
608 self._phonenumber = make_ugly(number)
609 self._prettynumber = make_pretty(self._phonenumber)
610 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
612 self._errorDisplay.push_exception()
621 def load_settings(self, config, section):
624 def save_settings(self, config, section):
626 @note Thread Agnostic
630 def _on_key_press(self, widget, event):
632 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
633 contents = self._clipboard.wait_for_text()
634 if contents is not None:
635 self.set_number(contents)
637 self._errorDisplay.push_exception()
639 def _on_ok_clicked(self, widget):
641 phoneNumber = self.get_number()
644 [("Dialer", phoneNumber)], ()
648 self._errorDisplay.push_exception()
650 def _on_digit_clicked(self, widget):
652 self.set_number(self._phonenumber + widget.get_name()[-1])
654 self._errorDisplay.push_exception()
656 def _on_plus(self, *args):
658 self.set_number(self._phonenumber + "+")
660 self._errorDisplay.push_exception()
662 def _on_backspace(self, taps):
664 self.set_number(self._phonenumber[:-taps])
665 self._reset_back_button()
667 self._errorDisplay.push_exception()
669 def _on_clearall(self, taps):
672 self._reset_back_button()
674 self._errorDisplay.push_exception()
677 def _set_clear_button(self):
679 self._backButton.set_label("gtk-clear")
681 self._errorDisplay.push_exception()
683 def _reset_back_button(self):
685 self._backButton.set_label(self._originalLabel)
687 self._errorDisplay.push_exception()
690 class AccountInfo(object):
692 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
693 self._errorDisplay = errorDisplay
694 self._backend = backend
695 self._isPopulated = False
696 self._alarmHandler = alarmHandler
697 self._notifyOnMissed = False
698 self._notifyOnVoicemail = False
699 self._notifyOnSms = False
701 self._callbackList = []
702 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
703 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
704 self._onCallbackSelectChangedId = 0
706 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
707 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
708 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
709 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
710 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
711 self._onNotifyToggled = 0
712 self._onMinutesChanged = 0
713 self._onMissedToggled = 0
714 self._onVoicemailToggled = 0
715 self._onSmsToggled = 0
716 self._applyAlarmTimeoutId = None
718 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
719 self._callbackNumber = ""
722 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
724 self._accountViewNumberDisplay.set_use_markup(True)
725 self.set_account_number("")
727 del self._callbackList[:]
728 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
729 self._set_callback_label("")
731 if self._alarmHandler is not None:
732 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
733 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
734 self._missedCheckbox.set_active(self._notifyOnMissed)
735 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
736 self._smsCheckbox.set_active(self._notifyOnSms)
738 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
739 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
740 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
741 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
742 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
744 self._notifyCheckbox.set_sensitive(False)
745 self._minutesEntryButton.set_sensitive(False)
746 self._missedCheckbox.set_sensitive(False)
747 self._voicemailCheckbox.set_sensitive(False)
748 self._smsCheckbox.set_sensitive(False)
750 self.update(force=True)
753 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
754 self._onCallbackSelectChangedId = 0
755 self._set_callback_label("")
757 if self._alarmHandler is not None:
758 self._notifyCheckbox.disconnect(self._onNotifyToggled)
759 self._minutesEntryButton.disconnect(self._onMinutesChanged)
760 self._missedCheckbox.disconnect(self._onNotifyToggled)
761 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
762 self._smsCheckbox.disconnect(self._onNotifyToggled)
763 self._onNotifyToggled = 0
764 self._onMinutesChanged = 0
765 self._onMissedToggled = 0
766 self._onVoicemailToggled = 0
767 self._onSmsToggled = 0
769 self._notifyCheckbox.set_sensitive(True)
770 self._minutesEntryButton.set_sensitive(True)
771 self._missedCheckbox.set_sensitive(True)
772 self._voicemailCheckbox.set_sensitive(True)
773 self._smsCheckbox.set_sensitive(True)
776 del self._callbackList[:]
778 def set_account_number(self, number):
780 Displays current account number
782 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
784 def update(self, force = False):
785 if not force and self._isPopulated:
787 self._populate_callback_combo()
788 self.set_account_number(self._backend.get_account_number())
792 self._set_callback_label("")
793 self.set_account_number("")
794 self._isPopulated = False
796 def save_everything(self):
797 raise NotImplementedError
801 return "Account Info"
803 def load_settings(self, config, section):
804 self._callbackNumber = make_ugly(config.get(section, "callback"))
805 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
806 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
807 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
809 def save_settings(self, config, section):
811 @note Thread Agnostic
813 config.set(section, "callback", self._callbackNumber)
814 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
815 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
816 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
818 def _populate_callback_combo(self):
819 self._isPopulated = True
820 del self._callbackList[:]
822 callbackNumbers = self._backend.get_callback_numbers()
824 self._errorDisplay.push_exception()
825 self._isPopulated = False
828 if len(callbackNumbers) == 0:
829 callbackNumbers = {"": "No callback numbers available"}
831 for number, description in callbackNumbers.iteritems():
832 self._callbackList.append((make_pretty(number), description))
834 self._set_callback_number(self._callbackNumber)
836 def _set_callback_number(self, number):
838 if not self._backend.is_valid_syntax(number) and 0 < len(number):
839 self._errorDisplay.push_message("%s is not a valid callback number" % number)
840 elif number == self._backend.get_callback_number() and 0 < len(number):
841 _moduleLogger.warning(
842 "Callback number already is %s" % (
843 self._backend.get_callback_number(),
846 self._set_callback_label(number)
848 if number.startswith("1747"): number = "+" + number
849 self._backend.set_callback_number(number)
850 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
851 make_pretty(number), make_pretty(self._backend.get_callback_number())
853 self._callbackNumber = make_ugly(number)
854 self._set_callback_label(number)
856 "Callback number set to %s" % (
857 self._backend.get_callback_number(),
861 self._errorDisplay.push_exception()
863 def _set_callback_label(self, uglyNumber):
864 prettyNumber = make_pretty(uglyNumber)
865 if len(prettyNumber) == 0:
866 prettyNumber = "No Callback Number"
867 self._callbackSelectButton.set_label(prettyNumber)
869 def _update_alarm_settings(self, recurrence):
871 isEnabled = self._notifyCheckbox.get_active()
872 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
873 self._alarmHandler.apply_settings(isEnabled, recurrence)
875 self.save_everything()
876 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
877 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
879 def _on_callbackentry_clicked(self, *args):
881 actualSelection = make_pretty(self._callbackNumber)
884 (number, "%s (%s)" % (number, description))
885 for (number, description) in self._callbackList
887 defaultSelection = userOptions.get(actualSelection, actualSelection)
889 userSelection = hildonize.touch_selector_entry(
892 list(userOptions.itervalues()),
895 reversedUserOptions = dict(
896 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
898 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
900 number = make_ugly(selectedNumber)
901 self._set_callback_number(number)
902 except RuntimeError, e:
903 _moduleLogger.exception("%s" % str(e))
905 self._errorDisplay.push_exception()
907 def _on_notify_toggled(self, *args):
909 if self._applyAlarmTimeoutId is not None:
910 gobject.source_remove(self._applyAlarmTimeoutId)
911 self._applyAlarmTimeoutId = None
912 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
914 self._errorDisplay.push_exception()
916 def _on_minutes_clicked(self, *args):
917 recurrenceChoices = [
933 actualSelection = self._alarmHandler.recurrence
935 closestSelectionIndex = 0
936 for i, possible in enumerate(recurrenceChoices):
937 if possible[0] <= actualSelection:
938 closestSelectionIndex = i
939 recurrenceIndex = hildonize.touch_selector(
942 (("%s" % m[1]) for m in recurrenceChoices),
943 closestSelectionIndex,
945 recurrence = recurrenceChoices[recurrenceIndex][0]
947 self._update_alarm_settings(recurrence)
948 except RuntimeError, e:
949 _moduleLogger.exception("%s" % str(e))
951 self._errorDisplay.push_exception()
953 def _on_apply_timeout(self, *args):
955 self._applyAlarmTimeoutId = None
957 self._update_alarm_settings(self._alarmHandler.recurrence)
959 self._errorDisplay.push_exception()
962 def _on_missed_toggled(self, *args):
964 self._notifyOnMissed = self._missedCheckbox.get_active()
965 self.save_everything()
967 self._errorDisplay.push_exception()
969 def _on_voicemail_toggled(self, *args):
971 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
972 self.save_everything()
974 self._errorDisplay.push_exception()
976 def _on_sms_toggled(self, *args):
978 self._notifyOnSms = self._smsCheckbox.get_active()
979 self.save_everything()
981 self._errorDisplay.push_exception()
984 class CallHistoryView(object):
992 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
994 def __init__(self, widgetTree, backend, errorDisplay):
995 self._errorDisplay = errorDisplay
996 self._backend = backend
998 self._isPopulated = False
999 self._historymodel = gtk.ListStore(
1000 gobject.TYPE_STRING, # number
1001 gobject.TYPE_STRING, # date
1002 gobject.TYPE_STRING, # action
1003 gobject.TYPE_STRING, # from
1004 gobject.TYPE_STRING, # from id
1006 self._historymodelfiltered = self._historymodel.filter_new()
1007 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1008 self._historyview = widgetTree.get_widget("historyview")
1009 self._historyviewselection = None
1010 self._onRecentviewRowActivatedId = 0
1012 textrenderer = gtk.CellRendererText()
1013 textrenderer.set_property("yalign", 0)
1014 self._dateColumn = gtk.TreeViewColumn("Date")
1015 self._dateColumn.pack_start(textrenderer, expand=True)
1016 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1018 textrenderer = gtk.CellRendererText()
1019 textrenderer.set_property("yalign", 0)
1020 self._actionColumn = gtk.TreeViewColumn("Action")
1021 self._actionColumn.pack_start(textrenderer, expand=True)
1022 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1024 textrenderer = gtk.CellRendererText()
1025 textrenderer.set_property("yalign", 0)
1026 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1027 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1028 self._numberColumn = gtk.TreeViewColumn("Number")
1029 self._numberColumn.pack_start(textrenderer, expand=True)
1030 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1032 textrenderer = gtk.CellRendererText()
1033 textrenderer.set_property("yalign", 0)
1034 hildonize.set_cell_thumb_selectable(textrenderer)
1035 self._nameColumn = gtk.TreeViewColumn("From")
1036 self._nameColumn.pack_start(textrenderer, expand=True)
1037 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1038 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1040 self._window = gtk_toolbox.find_parent_window(self._historyview)
1042 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1043 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1044 self._selectedFilter = "All"
1046 self._updateSink = gtk_toolbox.threaded_stage(
1048 self._idly_populate_historyview,
1049 gtk_toolbox.null_sink(),
1054 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1055 self._historyFilterSelector.set_label(self._selectedFilter)
1057 self._historyview.set_model(self._historymodelfiltered)
1058 self._historyview.set_fixed_height_mode(False)
1060 self._historyview.append_column(self._dateColumn)
1061 self._historyview.append_column(self._actionColumn)
1062 self._historyview.append_column(self._numberColumn)
1063 self._historyview.append_column(self._nameColumn)
1064 self._historyviewselection = self._historyview.get_selection()
1065 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1067 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1070 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1074 self._historyview.remove_column(self._dateColumn)
1075 self._historyview.remove_column(self._actionColumn)
1076 self._historyview.remove_column(self._nameColumn)
1077 self._historyview.remove_column(self._numberColumn)
1078 self._historyview.set_model(None)
1080 def add_contact(self, *args, **kwds):
1082 @note Actual dial function is patched in later
1084 raise NotImplementedError("Horrible unknown error has occurred")
1086 def update(self, force = False):
1087 if not force and self._isPopulated:
1089 self._updateSink.send(())
1093 self._isPopulated = False
1094 self._historymodel.clear()
1098 return "Recent Calls"
1100 def load_settings(self, config, sectionName):
1102 self._selectedFilter = config.get(sectionName, "filter")
1103 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1104 self._messageType = self.HISTORY_ITEM_TYPES[0]
1105 except ConfigParser.NoOptionError:
1108 def save_settings(self, config, sectionName):
1110 @note Thread Agnostic
1112 config.set(sectionName, "filter", self._selectedFilter)
1114 def _is_history_visible(self, model, iter):
1116 action = model.get_value(iter, self.ACTION_IDX)
1118 return False # this seems weird but oh well
1120 if self._selectedFilter in [action, "All"]:
1124 except Exception, e:
1125 self._errorDisplay.push_exception()
1127 def _idly_populate_historyview(self):
1128 with gtk_toolbox.gtk_lock():
1129 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1131 self._historymodel.clear()
1132 self._isPopulated = True
1135 historyItems = self._backend.get_recent()
1136 except Exception, e:
1137 self._errorDisplay.push_exception_with_lock()
1138 self._isPopulated = False
1142 gv_backend.decorate_recent(data)
1143 for data in gv_backend.sort_messages(historyItems)
1146 for contactId, personName, phoneNumber, date, action in historyItems:
1148 personName = "Unknown"
1149 date = abbrev_relative_date(date)
1150 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1151 prettyNumber = make_pretty(prettyNumber)
1152 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1153 with gtk_toolbox.gtk_lock():
1154 self._historymodel.append(item)
1155 except Exception, e:
1156 self._errorDisplay.push_exception_with_lock()
1158 with gtk_toolbox.gtk_lock():
1159 hildonize.show_busy_banner_end(banner)
1163 def _on_history_filter_clicked(self, *args, **kwds):
1165 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1168 newSelectedComboIndex = hildonize.touch_selector(
1171 self.HISTORY_ITEM_TYPES,
1174 except RuntimeError:
1177 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1178 self._selectedFilter = option
1179 self._historyFilterSelector.set_label(self._selectedFilter)
1180 self._historymodelfiltered.refilter()
1181 except Exception, e:
1182 self._errorDisplay.push_exception()
1184 def _history_summary(self, expectedNumber):
1185 for number, action, date, whoFrom, whoFromId in self._historymodel:
1186 if expectedNumber is not None and expectedNumber == number:
1187 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1189 def _on_historyview_row_activated(self, treeview, path, view_column):
1191 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1192 itr = self._historymodel.get_iter(childPath)
1196 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1197 number = make_ugly(prettyNumber)
1198 description = list(self._history_summary(prettyNumber))
1199 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1200 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1201 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1205 contactPhoneNumbers,
1206 messages = description,
1207 defaultIndex = defaultIndex,
1209 self._historyviewselection.unselect_all()
1210 except Exception, e:
1211 self._errorDisplay.push_exception()
1214 class MessagesView(object):
1222 MESSAGE_DATA_IDX = 6
1224 NO_MESSAGES = "None"
1225 VOICEMAIL_MESSAGES = "Voicemail"
1226 TEXT_MESSAGES = "Texts"
1227 ALL_TYPES = "All Messages"
1228 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1230 UNREAD_STATUS = "Unread"
1231 UNARCHIVED_STATUS = "Inbox"
1233 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1235 def __init__(self, widgetTree, backend, errorDisplay):
1236 self._errorDisplay = errorDisplay
1237 self._backend = backend
1239 self._isPopulated = False
1240 self._messagemodel = gtk.ListStore(
1241 gobject.TYPE_STRING, # number
1242 gobject.TYPE_STRING, # date
1243 gobject.TYPE_STRING, # header
1244 gobject.TYPE_STRING, # message
1246 gobject.TYPE_STRING, # from id
1247 object, # message data
1249 self._messagemodelfiltered = self._messagemodel.filter_new()
1250 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1251 self._messageview = widgetTree.get_widget("messages_view")
1252 self._messageviewselection = None
1253 self._onMessageviewRowActivatedId = 0
1255 self._messageRenderer = gtk.CellRendererText()
1256 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1257 self._messageRenderer.set_property("wrap-width", 500)
1258 self._messageColumn = gtk.TreeViewColumn("Messages")
1259 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1260 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1261 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1263 self._window = gtk_toolbox.find_parent_window(self._messageview)
1265 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1266 self._onMessageTypeClickedId = 0
1267 self._messageType = self.ALL_TYPES
1268 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1269 self._onMessageStatusClickedId = 0
1270 self._messageStatus = self.ALL_STATUS
1272 self._updateSink = gtk_toolbox.threaded_stage(
1274 self._idly_populate_messageview,
1275 gtk_toolbox.null_sink(),
1280 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1281 self._messageview.set_model(self._messagemodelfiltered)
1282 self._messageview.set_headers_visible(False)
1283 self._messageview.set_fixed_height_mode(False)
1285 self._messageview.append_column(self._messageColumn)
1286 self._messageviewselection = self._messageview.get_selection()
1287 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1289 self._messageTypeButton.set_label(self._messageType)
1290 self._messageStatusButton.set_label(self._messageStatus)
1292 self._onMessageviewRowActivatedId = self._messageview.connect(
1293 "row-activated", self._on_messageview_row_activated
1295 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1296 "clicked", self._on_message_type_clicked
1298 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1299 "clicked", self._on_message_status_clicked
1303 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1304 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1305 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1309 self._messageview.remove_column(self._messageColumn)
1310 self._messageview.set_model(None)
1312 def add_contact(self, *args, **kwds):
1314 @note Actual dial function is patched in later
1316 raise NotImplementedError("Horrible unknown error has occurred")
1318 def update(self, force = False):
1319 if not force and self._isPopulated:
1321 self._updateSink.send(())
1325 self._isPopulated = False
1326 self._messagemodel.clear()
1332 def load_settings(self, config, sectionName):
1334 self._messageType = config.get(sectionName, "type")
1335 if self._messageType not in self.MESSAGE_TYPES:
1336 self._messageType = self.ALL_TYPES
1337 self._messageStatus = config.get(sectionName, "status")
1338 if self._messageStatus not in self.MESSAGE_STATUSES:
1339 self._messageStatus = self.ALL_STATUS
1340 except ConfigParser.NoOptionError:
1343 def save_settings(self, config, sectionName):
1345 @note Thread Agnostic
1347 config.set(sectionName, "status", self._messageStatus)
1348 config.set(sectionName, "type", self._messageType)
1350 def _is_message_visible(self, model, iter):
1352 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1354 return False # this seems weird but oh well
1355 return self._filter_messages(message, self._messageType, self._messageStatus)
1356 except Exception, e:
1357 self._errorDisplay.push_exception()
1360 def _filter_messages(cls, message, type, status):
1361 if type == cls.ALL_TYPES:
1364 messageType = message["type"]
1365 isType = messageType == type
1367 if status == cls.ALL_STATUS:
1370 isUnarchived = not message["isArchived"]
1371 isUnread = not message["isRead"]
1372 if status == cls.UNREAD_STATUS:
1373 isStatus = isUnarchived and isUnread
1374 elif status == cls.UNARCHIVED_STATUS:
1375 isStatus = isUnarchived
1377 assert "Status %s is bad for %r" % (status, message)
1379 return isType and isStatus
1381 _MIN_MESSAGES_SHOWN = 4
1383 def _idly_populate_messageview(self):
1384 with gtk_toolbox.gtk_lock():
1385 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1387 self._messagemodel.clear()
1388 self._isPopulated = True
1390 if self._messageType == self.NO_MESSAGES:
1394 messageItems = self._backend.get_messages()
1395 except Exception, e:
1396 self._errorDisplay.push_exception_with_lock()
1397 self._isPopulated = False
1401 (gv_backend.decorate_message(message), message)
1402 for message in gv_backend.sort_messages(messageItems)
1405 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1406 prettyNumber = number[2:] if number.startswith("+1") else number
1407 prettyNumber = make_pretty(prettyNumber)
1409 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1410 expandedMessages = [firstMessage]
1411 expandedMessages.extend(messages)
1412 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1413 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1414 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1415 collapsedMessages = [firstMessage, secondMessage]
1416 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1418 collapsedMessages = expandedMessages
1419 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1421 number = make_ugly(number)
1423 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1424 with gtk_toolbox.gtk_lock():
1425 self._messagemodel.append(row)
1426 except Exception, e:
1427 self._errorDisplay.push_exception_with_lock()
1429 with gtk_toolbox.gtk_lock():
1430 hildonize.show_busy_banner_end(banner)
1431 self._messagemodelfiltered.refilter()
1435 def _on_messageview_row_activated(self, treeview, path, view_column):
1437 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1438 itr = self._messagemodel.get_iter(childPath)
1442 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1443 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1445 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1446 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1447 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1451 contactPhoneNumbers,
1452 messages = description,
1453 defaultIndex = defaultIndex,
1455 self._messageviewselection.unselect_all()
1456 except Exception, e:
1457 self._errorDisplay.push_exception()
1459 def _on_message_type_clicked(self, *args, **kwds):
1461 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1464 newSelectedIndex = hildonize.touch_selector(
1470 except RuntimeError:
1473 if selectedIndex != newSelectedIndex:
1474 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1475 self._messageTypeButton.set_label(self._messageType)
1476 self._messagemodelfiltered.refilter()
1477 except Exception, e:
1478 self._errorDisplay.push_exception()
1480 def _on_message_status_clicked(self, *args, **kwds):
1482 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1485 newSelectedIndex = hildonize.touch_selector(
1488 self.MESSAGE_STATUSES,
1491 except RuntimeError:
1494 if selectedIndex != newSelectedIndex:
1495 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1496 self._messageStatusButton.set_label(self._messageStatus)
1497 self._messagemodelfiltered.refilter()
1498 except Exception, e:
1499 self._errorDisplay.push_exception()
1502 class ContactsView(object):
1504 CONTACT_TYPE_IDX = 0
1505 CONTACT_NAME_IDX = 1
1508 def __init__(self, widgetTree, backend, errorDisplay):
1509 self._errorDisplay = errorDisplay
1510 self._backend = backend
1512 self._addressBook = None
1513 self._selectedComboIndex = 0
1514 self._addressBookFactories = [null_backend.NullAddressBook()]
1516 self._booksList = []
1517 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1519 self._isPopulated = False
1520 self._contactsmodel = gtk.ListStore(
1521 gobject.TYPE_STRING, # Contact Type
1522 gobject.TYPE_STRING, # Contact Name
1523 gobject.TYPE_STRING, # Contact ID
1525 self._contactsviewselection = None
1526 self._contactsview = widgetTree.get_widget("contactsview")
1528 self._contactColumn = gtk.TreeViewColumn("Contact")
1529 displayContactSource = False
1530 if displayContactSource:
1531 textrenderer = gtk.CellRendererText()
1532 self._contactColumn.pack_start(textrenderer, expand=False)
1533 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1534 textrenderer = gtk.CellRendererText()
1535 hildonize.set_cell_thumb_selectable(textrenderer)
1536 self._contactColumn.pack_start(textrenderer, expand=True)
1537 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1538 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1539 self._contactColumn.set_sort_column_id(1)
1540 self._contactColumn.set_visible(True)
1542 self._onContactsviewRowActivatedId = 0
1543 self._onAddressbookButtonChangedId = 0
1544 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1546 self._updateSink = gtk_toolbox.threaded_stage(
1548 self._idly_populate_contactsview,
1549 gtk_toolbox.null_sink(),
1554 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1556 self._contactsview.set_model(self._contactsmodel)
1557 self._contactsview.set_fixed_height_mode(False)
1558 self._contactsview.append_column(self._contactColumn)
1559 self._contactsviewselection = self._contactsview.get_selection()
1560 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1562 del self._booksList[:]
1563 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1564 if factoryName and bookName:
1565 entryName = "%s: %s" % (factoryName, bookName)
1567 entryName = factoryName
1569 entryName = bookName
1571 entryName = "Bad name (%d)" % factoryId
1572 row = (str(factoryId), bookId, entryName)
1573 self._booksList.append(row)
1575 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1576 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1578 if len(self._booksList) <= self._selectedComboIndex:
1579 self._selectedComboIndex = 0
1580 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1582 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1583 selectedBookId = self._booksList[self._selectedComboIndex][1]
1584 self.open_addressbook(selectedFactoryId, selectedBookId)
1587 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1588 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1592 self._bookSelectionButton.set_label("")
1593 self._contactsview.set_model(None)
1594 self._contactsview.remove_column(self._contactColumn)
1596 def add_contact(self, *args, **kwds):
1598 @note Actual dial function is patched in later
1600 raise NotImplementedError("Horrible unknown error has occurred")
1602 def get_addressbooks(self):
1604 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1606 for i, factory in enumerate(self._addressBookFactories):
1607 for bookFactory, bookId, bookName in factory.get_addressbooks():
1608 yield (str(i), bookId), (factory.factory_name(), bookName)
1610 def open_addressbook(self, bookFactoryId, bookId):
1611 bookFactoryIndex = int(bookFactoryId)
1612 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1613 self._addressBook = addressBook
1615 def update(self, force = False):
1616 if not force and self._isPopulated:
1618 self._updateSink.send(())
1622 self._isPopulated = False
1623 self._contactsmodel.clear()
1624 for factory in self._addressBookFactories:
1625 factory.clear_caches()
1626 self._addressBook.clear_caches()
1628 def append(self, book):
1629 self._addressBookFactories.append(book)
1631 def extend(self, books):
1632 self._addressBookFactories.extend(books)
1638 def load_settings(self, config, sectionName):
1640 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1641 except ConfigParser.NoOptionError:
1642 self._selectedComboIndex = 0
1644 def save_settings(self, config, sectionName):
1645 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1647 def _idly_populate_contactsview(self):
1648 with gtk_toolbox.gtk_lock():
1649 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1652 while addressBook is not self._addressBook:
1653 addressBook = self._addressBook
1654 with gtk_toolbox.gtk_lock():
1655 self._contactsview.set_model(None)
1659 contacts = addressBook.get_contacts()
1660 except Exception, e:
1662 self._isPopulated = False
1663 self._errorDisplay.push_exception_with_lock()
1664 for contactId, contactName in contacts:
1665 contactType = addressBook.contact_source_short_name(contactId)
1666 row = contactType, contactName, contactId
1667 self._contactsmodel.append(row)
1669 with gtk_toolbox.gtk_lock():
1670 self._contactsview.set_model(self._contactsmodel)
1672 self._isPopulated = True
1673 except Exception, e:
1674 self._errorDisplay.push_exception_with_lock()
1676 with gtk_toolbox.gtk_lock():
1677 hildonize.show_busy_banner_end(banner)
1680 def _on_addressbook_button_changed(self, *args, **kwds):
1683 newSelectedComboIndex = hildonize.touch_selector(
1686 (("%s" % m[2]) for m in self._booksList),
1687 self._selectedComboIndex,
1689 except RuntimeError:
1692 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1693 selectedBookId = self._booksList[newSelectedComboIndex][1]
1695 oldAddressbook = self._addressBook
1696 self.open_addressbook(selectedFactoryId, selectedBookId)
1697 forceUpdate = True if oldAddressbook is not self._addressBook else False
1698 self.update(force=forceUpdate)
1700 self._selectedComboIndex = newSelectedComboIndex
1701 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1702 except Exception, e:
1703 self._errorDisplay.push_exception()
1705 def _on_contactsview_row_activated(self, treeview, path, view_column):
1707 itr = self._contactsmodel.get_iter(path)
1711 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1712 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1714 contactDetails = self._addressBook.get_contact_details(contactId)
1715 except Exception, e:
1717 self._errorDisplay.push_exception()
1718 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1720 if len(contactPhoneNumbers) == 0:
1725 contactPhoneNumbers,
1726 messages = (contactName, ),
1728 self._contactsviewselection.unselect_all()
1729 except Exception, e:
1730 self._errorDisplay.push_exception()