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, app):
251 self._clipboard = gtk.clipboard_get()
252 self._widgetTree = widgetTree
253 self._parent = parent
255 self._isFullScreen = False
257 self._window = self._widgetTree.get_widget("smsWindow")
258 self._window = hildonize.hildonize_window(self._app, self._window)
259 self._window.connect("delete-event", self._on_delete)
260 self._window.connect("key-press-event", self._on_key_press)
261 self._window.connect("window-state-event", self._on_window_state_change)
263 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
264 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
265 errorClose = self._widgetTree.get_widget("smsErrorClose")
266 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
268 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
269 self._smsButton.connect("clicked", self._on_send)
270 self._dialButton = self._widgetTree.get_widget("dialButton")
271 self._dialButton.connect("clicked", self._on_dial)
273 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
275 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
276 self._messagesView = self._widgetTree.get_widget("smsMessages")
278 textrenderer = gtk.CellRendererText()
279 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
280 textrenderer.set_property("wrap-width", 450)
281 messageColumn = gtk.TreeViewColumn("")
282 messageColumn.pack_start(textrenderer, expand=True)
283 messageColumn.add_attribute(textrenderer, "markup", 0)
284 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
285 self._messagesView.append_column(messageColumn)
286 self._messagesView.set_headers_visible(False)
287 self._messagesView.set_model(self._messagemodel)
288 self._messagesView.set_fixed_height_mode(False)
290 self._conversationView = self._messagesView.get_parent()
291 self._conversationViewPort = self._conversationView.get_parent()
292 self._scrollWindow = self._conversationViewPort.get_parent()
294 self._targetList = self._widgetTree.get_widget("smsTargetList")
295 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
296 self._phoneButton.connect("clicked", self._on_phone)
297 self._smsEntry = self._widgetTree.get_widget("smsEntry")
298 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
299 self._smsEntrySize = None
303 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
304 contactNumbers = list(self._to_contact_numbers(contactDetails))
305 assert contactNumbers
306 contactIndex = defaultIndex if defaultIndex != -1 else 0
307 contact = contactNumbers, contactIndex, messages
308 self._contacts.append(contact)
310 nameLabel = gtk.Label(name)
311 selector = gtk.Button(contactNumbers[0][1])
312 if len(contactNumbers) == 1:
313 selector.set_sensitive(False)
314 removeContact = gtk.Button(stock="gtk-delete")
316 row.pack_start(nameLabel, True, True)
317 row.pack_start(selector, True, True)
318 row.pack_start(removeContact, False, False)
320 self._targetList.pack_start(row)
321 selector.connect("clicked", self._on_choose_phone_n, row)
322 removeContact.connect("clicked", self._on_remove_phone_n, row)
323 self._update_button_state()
324 self._update_context()
326 parentSize = self._parent.get_size()
327 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
329 self._window.present()
331 self._smsEntry.grab_focus()
332 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
334 adjustment = self._scrollWindow.get_vadjustment()
335 adjustment.value = dx
338 del self._contacts[:]
340 for row in list(self._targetList.get_children()):
341 self._targetList.remove(row)
342 self._smsEntry.get_buffer().set_text("")
343 self._update_letter_count()
344 self._update_context()
346 def fullscreen(self):
347 self._window.fullscreen()
349 def unfullscreen(self):
350 self._window.unfullscreen()
352 def _remove_contact(self, contactIndex):
353 del self._contacts[contactIndex]
355 row = list(self._targetList.get_children())[contactIndex]
356 self._targetList.remove(row)
357 self._update_button_state()
358 self._update_context()
360 def _update_letter_count(self):
361 if self._smsEntrySize is None:
362 self._smsEntrySize = self._smsEntry.size_request()
364 self._smsEntry.set_size_request(*self._smsEntrySize)
365 entryLength = self._smsEntry.get_buffer().get_char_count()
367 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
369 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
371 self._letterCountLabel.set_text("%s" % (numCharInText, ))
373 self._update_button_state()
375 def _update_context(self):
376 self._messagemodel.clear()
377 if len(self._contacts) == 0:
378 self._messagesView.hide()
379 self._targetList.hide()
380 self._phoneButton.hide()
381 self._phoneButton.set_label("Error: You shouldn't see this")
382 elif len(self._contacts) == 1:
383 contactNumbers, index, messages = self._contacts[0]
385 self._messagesView.show()
386 for message in messages:
388 self._messagemodel.append(row)
389 messagesSelection = self._messagesView.get_selection()
390 messagesSelection.select_path((len(messages)-1, ))
392 self._messagesView.hide()
393 self._targetList.hide()
394 self._phoneButton.show()
395 self._phoneButton.set_label(contactNumbers[index][1])
396 if 1 < len(contactNumbers):
397 self._phoneButton.set_sensitive(True)
399 self._phoneButton.set_sensitive(False)
401 self._messagesView.hide()
402 self._targetList.show()
403 self._phoneButton.hide()
404 self._phoneButton.set_label("Error: You shouldn't see this")
406 def _update_button_state(self):
407 if len(self._contacts) == 0:
408 self._dialButton.set_sensitive(False)
409 self._smsButton.set_sensitive(False)
410 elif len(self._contacts) == 1:
411 entryLength = self._smsEntry.get_buffer().get_char_count()
413 self._dialButton.set_sensitive(True)
414 self._smsButton.set_sensitive(False)
416 self._dialButton.set_sensitive(False)
417 self._smsButton.set_sensitive(True)
419 self._dialButton.set_sensitive(False)
420 self._smsButton.set_sensitive(True)
422 def _to_contact_numbers(self, contactDetails):
423 for phoneType, phoneNumber in contactDetails:
424 display = " - ".join((make_pretty(phoneNumber), phoneType))
425 yield (phoneNumber, display)
427 def _pseudo_destroy(self):
431 def _request_number(self, contactIndex):
432 contactNumbers, index, messages = self._contacts[contactIndex]
433 assert 0 <= index, "%r" % index
435 index = hildonize.touch_selector(
438 (description for (number, description) in contactNumbers),
441 self._contacts[contactIndex] = contactNumbers, index, messages
443 def send_sms(self, numbers, message):
444 raise NotImplementedError()
446 def dial(self, number):
447 raise NotImplementedError()
449 def _on_phone(self, *args):
451 assert len(self._contacts) == 1
452 self._request_number(0)
454 contactNumbers, numberIndex, messages = self._contacts[0]
455 self._phoneButton.set_label(contactNumbers[numberIndex][1])
456 row = list(self._targetList.get_children())[0]
457 phoneButton = list(row.get_children())[1]
458 phoneButton.set_label(contactNumbers[numberIndex][1])
460 self._errorDisplay.push_exception()
462 def _on_choose_phone_n(self, button, row):
464 assert 1 < len(self._contacts)
465 targetList = list(self._targetList.get_children())
466 index = targetList.index(row)
467 self._request_number(index)
469 contactNumbers, numberIndex, messages = self._contacts[0]
470 phoneButton = list(row.get_children())[1]
471 phoneButton.set_label(contactNumbers[numberIndex][1])
473 self._errorDisplay.push_exception()
475 def _on_remove_phone_n(self, button, row):
477 assert 1 < len(self._contacts)
478 targetList = list(self._targetList.get_children())
479 index = targetList.index(row)
481 del self._contacts[index]
482 self._targetList.remove(row)
483 self._update_context()
484 self._update_button_state()
486 self._errorDisplay.push_exception()
488 def _on_entry_changed(self, *args):
490 self._update_letter_count()
492 self._errorDisplay.push_exception()
494 def _on_send(self, *args):
496 assert 0 < len(self._contacts), "%r" % self._contacts
498 make_ugly(contact[0][contact[1]][0])
499 for contact in self._contacts
502 entryBuffer = self._smsEntry.get_buffer()
503 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
504 enteredMessage = enteredMessage.strip()
505 assert enteredMessage
506 self.send_sms(phoneNumbers, enteredMessage)
507 self._pseudo_destroy()
509 self._errorDisplay.push_exception()
511 def _on_dial(self, *args):
513 assert len(self._contacts) == 1, "%r" % self._contacts
514 contact = self._contacts[0]
515 contactNumber = contact[0][contact[1]][0]
516 phoneNumber = make_ugly(contactNumber)
517 self.dial(phoneNumber)
518 self._pseudo_destroy()
520 self._errorDisplay.push_exception()
522 def _on_delete(self, *args):
524 self._window.emit_stop_by_name("delete-event")
525 if hildonize.IS_FREMANTLE_SUPPORTED:
528 self._pseudo_destroy()
530 self._errorDisplay.push_exception()
533 def _on_window_state_change(self, widget, event, *args):
535 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
536 self._isFullScreen = True
538 self._isFullScreen = False
540 self._errorDisplay.push_exception()
542 def _on_key_press(self, widget, event):
543 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
546 event.keyval == gtk.keysyms.F6 or
547 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
549 if self._isFullScreen:
550 self._window.unfullscreen()
552 self._window.fullscreen()
553 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
556 for messagePart in self._messagemodel
558 self._clipboard.set_text(str(message))
560 event.keyval == gtk.keysyms.h and
561 event.get_state() & gtk.gdk.CONTROL_MASK
565 event.keyval == gtk.keysyms.w and
566 event.get_state() & gtk.gdk.CONTROL_MASK
568 self._pseudo_destroy()
570 event.keyval == gtk.keysyms.q and
571 event.get_state() & gtk.gdk.CONTROL_MASK
573 self._parent.destroy()
575 self._errorDisplay.push_exception()
578 class Dialpad(object):
580 def __init__(self, widgetTree, errorDisplay):
581 self._clipboard = gtk.clipboard_get()
582 self._errorDisplay = errorDisplay
584 self._numberdisplay = widgetTree.get_widget("numberdisplay")
585 self._okButton = widgetTree.get_widget("dialpadOk")
586 self._backButton = widgetTree.get_widget("back")
587 self._plusButton = widgetTree.get_widget("plus")
588 self._phonenumber = ""
589 self._prettynumber = ""
592 "on_digit_clicked": self._on_digit_clicked,
594 widgetTree.signal_autoconnect(callbackMapping)
595 self._okButton.connect("clicked", self._on_ok_clicked)
596 self._plusButton.connect("clicked", self._on_plus)
598 self._originalLabel = self._backButton.get_label()
599 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
600 self._backTapHandler.on_tap = self._on_backspace
601 self._backTapHandler.on_hold = self._on_clearall
602 self._backTapHandler.on_holding = self._set_clear_button
603 self._backTapHandler.on_cancel = self._reset_back_button
605 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
606 self._keyPressEventId = 0
609 self._okButton.grab_focus()
610 self._backTapHandler.enable()
611 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
614 self._window.disconnect(self._keyPressEventId)
615 self._keyPressEventId = 0
616 self._reset_back_button()
617 self._backTapHandler.disable()
619 def add_contact(self, *args, **kwds):
621 @note Actual dial function is patched in later
623 raise NotImplementedError("Horrible unknown error has occurred")
625 def get_number(self):
626 return self._phonenumber
628 def set_number(self, number):
630 Set the number to dial
633 self._phonenumber = make_ugly(number)
634 self._prettynumber = make_pretty(self._phonenumber)
635 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
637 self._errorDisplay.push_exception()
646 def load_settings(self, config, section):
649 def save_settings(self, config, section):
651 @note Thread Agnostic
655 def _on_key_press(self, widget, event):
657 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
658 contents = self._clipboard.wait_for_text()
659 if contents is not None:
660 self.set_number(contents)
662 self._errorDisplay.push_exception()
664 def _on_ok_clicked(self, widget):
666 phoneNumber = self.get_number()
669 [("Dialer", phoneNumber)], ()
673 self._errorDisplay.push_exception()
675 def _on_digit_clicked(self, widget):
677 self.set_number(self._phonenumber + widget.get_name()[-1])
679 self._errorDisplay.push_exception()
681 def _on_plus(self, *args):
683 self.set_number(self._phonenumber + "+")
685 self._errorDisplay.push_exception()
687 def _on_backspace(self, taps):
689 self.set_number(self._phonenumber[:-taps])
690 self._reset_back_button()
692 self._errorDisplay.push_exception()
694 def _on_clearall(self, taps):
697 self._reset_back_button()
699 self._errorDisplay.push_exception()
702 def _set_clear_button(self):
704 self._backButton.set_label("gtk-clear")
706 self._errorDisplay.push_exception()
708 def _reset_back_button(self):
710 self._backButton.set_label(self._originalLabel)
712 self._errorDisplay.push_exception()
715 class AccountInfo(object):
717 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
718 self._errorDisplay = errorDisplay
719 self._backend = backend
720 self._isPopulated = False
721 self._alarmHandler = alarmHandler
722 self._notifyOnMissed = False
723 self._notifyOnVoicemail = False
724 self._notifyOnSms = False
726 self._callbackList = []
727 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
728 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
729 self._onCallbackSelectChangedId = 0
731 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
732 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
733 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
734 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
735 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
736 self._onNotifyToggled = 0
737 self._onMinutesChanged = 0
738 self._onMissedToggled = 0
739 self._onVoicemailToggled = 0
740 self._onSmsToggled = 0
741 self._applyAlarmTimeoutId = None
743 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
744 self._callbackNumber = ""
747 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
749 self._accountViewNumberDisplay.set_use_markup(True)
750 self.set_account_number("")
752 del self._callbackList[:]
753 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
754 self._set_callback_label("")
756 if self._alarmHandler is not None:
757 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
758 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
759 self._missedCheckbox.set_active(self._notifyOnMissed)
760 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
761 self._smsCheckbox.set_active(self._notifyOnSms)
763 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
764 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
765 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
766 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
767 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
769 self._notifyCheckbox.set_sensitive(False)
770 self._minutesEntryButton.set_sensitive(False)
771 self._missedCheckbox.set_sensitive(False)
772 self._voicemailCheckbox.set_sensitive(False)
773 self._smsCheckbox.set_sensitive(False)
775 self.update(force=True)
778 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
779 self._onCallbackSelectChangedId = 0
780 self._set_callback_label("")
782 if self._alarmHandler is not None:
783 self._notifyCheckbox.disconnect(self._onNotifyToggled)
784 self._minutesEntryButton.disconnect(self._onMinutesChanged)
785 self._missedCheckbox.disconnect(self._onNotifyToggled)
786 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
787 self._smsCheckbox.disconnect(self._onNotifyToggled)
788 self._onNotifyToggled = 0
789 self._onMinutesChanged = 0
790 self._onMissedToggled = 0
791 self._onVoicemailToggled = 0
792 self._onSmsToggled = 0
794 self._notifyCheckbox.set_sensitive(True)
795 self._minutesEntryButton.set_sensitive(True)
796 self._missedCheckbox.set_sensitive(True)
797 self._voicemailCheckbox.set_sensitive(True)
798 self._smsCheckbox.set_sensitive(True)
801 del self._callbackList[:]
803 def set_account_number(self, number):
805 Displays current account number
807 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
809 def update(self, force = False):
810 if not force and self._isPopulated:
812 self._populate_callback_combo()
813 self.set_account_number(self._backend.get_account_number())
817 self._set_callback_label("")
818 self.set_account_number("")
819 self._isPopulated = False
821 def save_everything(self):
822 raise NotImplementedError
826 return "Account Info"
828 def load_settings(self, config, section):
829 self._callbackNumber = make_ugly(config.get(section, "callback"))
830 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
831 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
832 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
834 def save_settings(self, config, section):
836 @note Thread Agnostic
838 config.set(section, "callback", self._callbackNumber)
839 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
840 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
841 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
843 def _populate_callback_combo(self):
844 self._isPopulated = True
845 del self._callbackList[:]
847 callbackNumbers = self._backend.get_callback_numbers()
849 self._errorDisplay.push_exception()
850 self._isPopulated = False
853 if len(callbackNumbers) == 0:
854 callbackNumbers = {"": "No callback numbers available"}
856 for number, description in callbackNumbers.iteritems():
857 self._callbackList.append((make_pretty(number), description))
859 self._set_callback_number(self._callbackNumber)
861 def _set_callback_number(self, number):
863 if not self._backend.is_valid_syntax(number) and 0 < len(number):
864 self._errorDisplay.push_message("%s is not a valid callback number" % number)
865 elif number == self._backend.get_callback_number() and 0 < len(number):
866 _moduleLogger.warning(
867 "Callback number already is %s" % (
868 self._backend.get_callback_number(),
871 self._set_callback_label(number)
873 if number.startswith("1747"): number = "+" + number
874 self._backend.set_callback_number(number)
875 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
876 make_pretty(number), make_pretty(self._backend.get_callback_number())
878 self._callbackNumber = make_ugly(number)
879 self._set_callback_label(number)
881 "Callback number set to %s" % (
882 self._backend.get_callback_number(),
886 self._errorDisplay.push_exception()
888 def _set_callback_label(self, uglyNumber):
889 prettyNumber = make_pretty(uglyNumber)
890 if len(prettyNumber) == 0:
891 prettyNumber = "No Callback Number"
892 self._callbackSelectButton.set_label(prettyNumber)
894 def _update_alarm_settings(self, recurrence):
896 isEnabled = self._notifyCheckbox.get_active()
897 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
898 self._alarmHandler.apply_settings(isEnabled, recurrence)
900 self.save_everything()
901 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
902 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
904 def _on_callbackentry_clicked(self, *args):
906 actualSelection = make_pretty(self._callbackNumber)
909 (number, "%s (%s)" % (number, description))
910 for (number, description) in self._callbackList
912 defaultSelection = userOptions.get(actualSelection, actualSelection)
914 userSelection = hildonize.touch_selector_entry(
917 list(userOptions.itervalues()),
920 reversedUserOptions = dict(
921 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
923 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
925 number = make_ugly(selectedNumber)
926 self._set_callback_number(number)
927 except RuntimeError, e:
928 _moduleLogger.exception("%s" % str(e))
930 self._errorDisplay.push_exception()
932 def _on_notify_toggled(self, *args):
934 if self._applyAlarmTimeoutId is not None:
935 gobject.source_remove(self._applyAlarmTimeoutId)
936 self._applyAlarmTimeoutId = None
937 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
939 self._errorDisplay.push_exception()
941 def _on_minutes_clicked(self, *args):
942 recurrenceChoices = [
958 actualSelection = self._alarmHandler.recurrence
960 closestSelectionIndex = 0
961 for i, possible in enumerate(recurrenceChoices):
962 if possible[0] <= actualSelection:
963 closestSelectionIndex = i
964 recurrenceIndex = hildonize.touch_selector(
967 (("%s" % m[1]) for m in recurrenceChoices),
968 closestSelectionIndex,
970 recurrence = recurrenceChoices[recurrenceIndex][0]
972 self._update_alarm_settings(recurrence)
973 except RuntimeError, e:
974 _moduleLogger.exception("%s" % str(e))
976 self._errorDisplay.push_exception()
978 def _on_apply_timeout(self, *args):
980 self._applyAlarmTimeoutId = None
982 self._update_alarm_settings(self._alarmHandler.recurrence)
984 self._errorDisplay.push_exception()
987 def _on_missed_toggled(self, *args):
989 self._notifyOnMissed = self._missedCheckbox.get_active()
990 self.save_everything()
992 self._errorDisplay.push_exception()
994 def _on_voicemail_toggled(self, *args):
996 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
997 self.save_everything()
999 self._errorDisplay.push_exception()
1001 def _on_sms_toggled(self, *args):
1003 self._notifyOnSms = self._smsCheckbox.get_active()
1004 self.save_everything()
1005 except Exception, e:
1006 self._errorDisplay.push_exception()
1009 class CallHistoryView(object):
1017 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1019 def __init__(self, widgetTree, backend, errorDisplay):
1020 self._errorDisplay = errorDisplay
1021 self._backend = backend
1023 self._isPopulated = False
1024 self._historymodel = gtk.ListStore(
1025 gobject.TYPE_STRING, # number
1026 gobject.TYPE_STRING, # date
1027 gobject.TYPE_STRING, # action
1028 gobject.TYPE_STRING, # from
1029 gobject.TYPE_STRING, # from id
1031 self._historymodelfiltered = self._historymodel.filter_new()
1032 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1033 self._historyview = widgetTree.get_widget("historyview")
1034 self._historyviewselection = None
1035 self._onRecentviewRowActivatedId = 0
1037 textrenderer = gtk.CellRendererText()
1038 textrenderer.set_property("yalign", 0)
1039 self._dateColumn = gtk.TreeViewColumn("Date")
1040 self._dateColumn.pack_start(textrenderer, expand=True)
1041 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1043 textrenderer = gtk.CellRendererText()
1044 textrenderer.set_property("yalign", 0)
1045 self._actionColumn = gtk.TreeViewColumn("Action")
1046 self._actionColumn.pack_start(textrenderer, expand=True)
1047 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1049 textrenderer = gtk.CellRendererText()
1050 textrenderer.set_property("yalign", 0)
1051 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1052 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1053 self._numberColumn = gtk.TreeViewColumn("Number")
1054 self._numberColumn.pack_start(textrenderer, expand=True)
1055 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1057 textrenderer = gtk.CellRendererText()
1058 textrenderer.set_property("yalign", 0)
1059 hildonize.set_cell_thumb_selectable(textrenderer)
1060 self._nameColumn = gtk.TreeViewColumn("From")
1061 self._nameColumn.pack_start(textrenderer, expand=True)
1062 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1063 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1065 self._window = gtk_toolbox.find_parent_window(self._historyview)
1067 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1068 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1069 self._selectedFilter = "All"
1071 self._updateSink = gtk_toolbox.threaded_stage(
1073 self._idly_populate_historyview,
1074 gtk_toolbox.null_sink(),
1079 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1080 self._historyFilterSelector.set_label(self._selectedFilter)
1082 self._historyview.set_model(self._historymodelfiltered)
1083 self._historyview.set_fixed_height_mode(False)
1085 self._historyview.append_column(self._dateColumn)
1086 self._historyview.append_column(self._actionColumn)
1087 self._historyview.append_column(self._numberColumn)
1088 self._historyview.append_column(self._nameColumn)
1089 self._historyviewselection = self._historyview.get_selection()
1090 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1092 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1095 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1099 self._historyview.remove_column(self._dateColumn)
1100 self._historyview.remove_column(self._actionColumn)
1101 self._historyview.remove_column(self._nameColumn)
1102 self._historyview.remove_column(self._numberColumn)
1103 self._historyview.set_model(None)
1105 def add_contact(self, *args, **kwds):
1107 @note Actual dial function is patched in later
1109 raise NotImplementedError("Horrible unknown error has occurred")
1111 def update(self, force = False):
1112 if not force and self._isPopulated:
1114 self._updateSink.send(())
1118 self._isPopulated = False
1119 self._historymodel.clear()
1123 return "Recent Calls"
1125 def load_settings(self, config, sectionName):
1127 self._selectedFilter = config.get(sectionName, "filter")
1128 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1129 self._messageType = self.HISTORY_ITEM_TYPES[0]
1130 except ConfigParser.NoOptionError:
1133 def save_settings(self, config, sectionName):
1135 @note Thread Agnostic
1137 config.set(sectionName, "filter", self._selectedFilter)
1139 def _is_history_visible(self, model, iter):
1141 action = model.get_value(iter, self.ACTION_IDX)
1143 return False # this seems weird but oh well
1145 if self._selectedFilter in [action, "All"]:
1149 except Exception, e:
1150 self._errorDisplay.push_exception()
1152 def _idly_populate_historyview(self):
1153 with gtk_toolbox.gtk_lock():
1154 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1156 self._historymodel.clear()
1157 self._isPopulated = True
1160 historyItems = self._backend.get_recent()
1161 except Exception, e:
1162 self._errorDisplay.push_exception_with_lock()
1163 self._isPopulated = False
1167 gv_backend.decorate_recent(data)
1168 for data in gv_backend.sort_messages(historyItems)
1171 for contactId, personName, phoneNumber, date, action in historyItems:
1173 personName = "Unknown"
1174 date = abbrev_relative_date(date)
1175 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1176 prettyNumber = make_pretty(prettyNumber)
1177 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1178 with gtk_toolbox.gtk_lock():
1179 self._historymodel.append(item)
1180 except Exception, e:
1181 self._errorDisplay.push_exception_with_lock()
1183 with gtk_toolbox.gtk_lock():
1184 hildonize.show_busy_banner_end(banner)
1188 def _on_history_filter_clicked(self, *args, **kwds):
1190 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1193 newSelectedComboIndex = hildonize.touch_selector(
1196 self.HISTORY_ITEM_TYPES,
1199 except RuntimeError:
1202 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1203 self._selectedFilter = option
1204 self._historyFilterSelector.set_label(self._selectedFilter)
1205 self._historymodelfiltered.refilter()
1206 except Exception, e:
1207 self._errorDisplay.push_exception()
1209 def _history_summary(self, expectedNumber):
1210 for number, action, date, whoFrom, whoFromId in self._historymodel:
1211 if expectedNumber is not None and expectedNumber == number:
1212 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1214 def _on_historyview_row_activated(self, treeview, path, view_column):
1216 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1217 itr = self._historymodel.get_iter(childPath)
1221 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1222 number = make_ugly(prettyNumber)
1223 description = list(self._history_summary(prettyNumber))
1224 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1225 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1226 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1230 contactPhoneNumbers,
1231 messages = description,
1232 defaultIndex = defaultIndex,
1234 self._historyviewselection.unselect_all()
1235 except Exception, e:
1236 self._errorDisplay.push_exception()
1239 class MessagesView(object):
1247 MESSAGE_DATA_IDX = 6
1249 NO_MESSAGES = "None"
1250 VOICEMAIL_MESSAGES = "Voicemail"
1251 TEXT_MESSAGES = "SMS"
1252 ALL_TYPES = "All Messages"
1253 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1255 UNREAD_STATUS = "Unread"
1256 UNARCHIVED_STATUS = "Inbox"
1258 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1260 def __init__(self, widgetTree, backend, errorDisplay):
1261 self._errorDisplay = errorDisplay
1262 self._backend = backend
1264 self._isPopulated = False
1265 self._messagemodel = gtk.ListStore(
1266 gobject.TYPE_STRING, # number
1267 gobject.TYPE_STRING, # date
1268 gobject.TYPE_STRING, # header
1269 gobject.TYPE_STRING, # message
1271 gobject.TYPE_STRING, # from id
1272 object, # message data
1274 self._messagemodelfiltered = self._messagemodel.filter_new()
1275 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1276 self._messageview = widgetTree.get_widget("messages_view")
1277 self._messageviewselection = None
1278 self._onMessageviewRowActivatedId = 0
1280 self._messageRenderer = gtk.CellRendererText()
1281 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1282 self._messageRenderer.set_property("wrap-width", 500)
1283 self._messageColumn = gtk.TreeViewColumn("Messages")
1284 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1285 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1286 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1288 self._window = gtk_toolbox.find_parent_window(self._messageview)
1290 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1291 self._onMessageTypeClickedId = 0
1292 self._messageType = self.ALL_TYPES
1293 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1294 self._onMessageStatusClickedId = 0
1295 self._messageStatus = self.ALL_STATUS
1297 self._updateSink = gtk_toolbox.threaded_stage(
1299 self._idly_populate_messageview,
1300 gtk_toolbox.null_sink(),
1305 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1306 self._messageview.set_model(self._messagemodelfiltered)
1307 self._messageview.set_headers_visible(False)
1308 self._messageview.set_fixed_height_mode(False)
1310 self._messageview.append_column(self._messageColumn)
1311 self._messageviewselection = self._messageview.get_selection()
1312 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1314 self._messageTypeButton.set_label(self._messageType)
1315 self._messageStatusButton.set_label(self._messageStatus)
1317 self._onMessageviewRowActivatedId = self._messageview.connect(
1318 "row-activated", self._on_messageview_row_activated
1320 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1321 "clicked", self._on_message_type_clicked
1323 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1324 "clicked", self._on_message_status_clicked
1328 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1329 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1330 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1334 self._messageview.remove_column(self._messageColumn)
1335 self._messageview.set_model(None)
1337 def add_contact(self, *args, **kwds):
1339 @note Actual dial function is patched in later
1341 raise NotImplementedError("Horrible unknown error has occurred")
1343 def update(self, force = False):
1344 if not force and self._isPopulated:
1346 self._updateSink.send(())
1350 self._isPopulated = False
1351 self._messagemodel.clear()
1357 def load_settings(self, config, sectionName):
1359 self._messageType = config.get(sectionName, "type")
1360 if self._messageType not in self.MESSAGE_TYPES:
1361 self._messageType = self.ALL_TYPES
1362 self._messageStatus = config.get(sectionName, "status")
1363 if self._messageStatus not in self.MESSAGE_STATUSES:
1364 self._messageStatus = self.ALL_STATUS
1365 except ConfigParser.NoOptionError:
1368 def save_settings(self, config, sectionName):
1370 @note Thread Agnostic
1372 config.set(sectionName, "status", self._messageStatus)
1373 config.set(sectionName, "type", self._messageType)
1375 def _is_message_visible(self, model, iter):
1377 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1379 return False # this seems weird but oh well
1380 return self._filter_messages(message, self._messageType, self._messageStatus)
1381 except Exception, e:
1382 self._errorDisplay.push_exception()
1385 def _filter_messages(cls, message, type, status):
1386 if type == cls.ALL_TYPES:
1389 messageType = message["type"]
1390 isType = messageType == type
1392 if status == cls.ALL_STATUS:
1395 isUnarchived = not message["isArchived"]
1396 isUnread = not message["isRead"]
1397 if status == cls.UNREAD_STATUS:
1398 isStatus = isUnarchived and isUnread
1399 elif status == cls.UNARCHIVED_STATUS:
1400 isStatus = isUnarchived
1402 assert "Status %s is bad for %r" % (status, message)
1404 return isType and isStatus
1406 _MIN_MESSAGES_SHOWN = 4
1408 def _idly_populate_messageview(self):
1409 with gtk_toolbox.gtk_lock():
1410 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1412 self._messagemodel.clear()
1413 self._isPopulated = True
1415 if self._messageType == self.NO_MESSAGES:
1419 messageItems = self._backend.get_messages()
1420 except Exception, e:
1421 self._errorDisplay.push_exception_with_lock()
1422 self._isPopulated = False
1426 (gv_backend.decorate_message(message), message)
1427 for message in gv_backend.sort_messages(messageItems)
1430 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1431 prettyNumber = number[2:] if number.startswith("+1") else number
1432 prettyNumber = make_pretty(prettyNumber)
1434 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1435 expandedMessages = [firstMessage]
1436 expandedMessages.extend(messages)
1437 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1438 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1439 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1440 collapsedMessages = [firstMessage, secondMessage]
1441 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1443 collapsedMessages = expandedMessages
1444 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1446 number = make_ugly(number)
1448 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1449 with gtk_toolbox.gtk_lock():
1450 self._messagemodel.append(row)
1451 except Exception, e:
1452 self._errorDisplay.push_exception_with_lock()
1454 with gtk_toolbox.gtk_lock():
1455 hildonize.show_busy_banner_end(banner)
1456 self._messagemodelfiltered.refilter()
1460 def _on_messageview_row_activated(self, treeview, path, view_column):
1462 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1463 itr = self._messagemodel.get_iter(childPath)
1467 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1468 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1470 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1471 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1472 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1476 contactPhoneNumbers,
1477 messages = description,
1478 defaultIndex = defaultIndex,
1480 self._messageviewselection.unselect_all()
1481 except Exception, e:
1482 self._errorDisplay.push_exception()
1484 def _on_message_type_clicked(self, *args, **kwds):
1486 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1489 newSelectedIndex = hildonize.touch_selector(
1495 except RuntimeError:
1498 if selectedIndex != newSelectedIndex:
1499 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1500 self._messageTypeButton.set_label(self._messageType)
1501 self._messagemodelfiltered.refilter()
1502 except Exception, e:
1503 self._errorDisplay.push_exception()
1505 def _on_message_status_clicked(self, *args, **kwds):
1507 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1510 newSelectedIndex = hildonize.touch_selector(
1513 self.MESSAGE_STATUSES,
1516 except RuntimeError:
1519 if selectedIndex != newSelectedIndex:
1520 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1521 self._messageStatusButton.set_label(self._messageStatus)
1522 self._messagemodelfiltered.refilter()
1523 except Exception, e:
1524 self._errorDisplay.push_exception()
1527 class ContactsView(object):
1529 CONTACT_TYPE_IDX = 0
1530 CONTACT_NAME_IDX = 1
1533 def __init__(self, widgetTree, backend, errorDisplay):
1534 self._errorDisplay = errorDisplay
1535 self._backend = backend
1537 self._addressBook = None
1538 self._selectedComboIndex = 0
1539 self._addressBookFactories = [null_backend.NullAddressBook()]
1541 self._booksList = []
1542 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1544 self._isPopulated = False
1545 self._contactsmodel = gtk.ListStore(
1546 gobject.TYPE_STRING, # Contact Type
1547 gobject.TYPE_STRING, # Contact Name
1548 gobject.TYPE_STRING, # Contact ID
1550 self._contactsviewselection = None
1551 self._contactsview = widgetTree.get_widget("contactsview")
1553 self._contactColumn = gtk.TreeViewColumn("Contact")
1554 displayContactSource = False
1555 if displayContactSource:
1556 textrenderer = gtk.CellRendererText()
1557 self._contactColumn.pack_start(textrenderer, expand=False)
1558 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1559 textrenderer = gtk.CellRendererText()
1560 hildonize.set_cell_thumb_selectable(textrenderer)
1561 self._contactColumn.pack_start(textrenderer, expand=True)
1562 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1563 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1564 self._contactColumn.set_sort_column_id(1)
1565 self._contactColumn.set_visible(True)
1567 self._onContactsviewRowActivatedId = 0
1568 self._onAddressbookButtonChangedId = 0
1569 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1571 self._updateSink = gtk_toolbox.threaded_stage(
1573 self._idly_populate_contactsview,
1574 gtk_toolbox.null_sink(),
1579 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1581 self._contactsview.set_model(self._contactsmodel)
1582 self._contactsview.set_fixed_height_mode(False)
1583 self._contactsview.append_column(self._contactColumn)
1584 self._contactsviewselection = self._contactsview.get_selection()
1585 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1587 del self._booksList[:]
1588 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1589 if factoryName and bookName:
1590 entryName = "%s: %s" % (factoryName, bookName)
1592 entryName = factoryName
1594 entryName = bookName
1596 entryName = "Bad name (%d)" % factoryId
1597 row = (str(factoryId), bookId, entryName)
1598 self._booksList.append(row)
1600 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1601 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1603 if len(self._booksList) <= self._selectedComboIndex:
1604 self._selectedComboIndex = 0
1605 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1607 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1608 selectedBookId = self._booksList[self._selectedComboIndex][1]
1609 self.open_addressbook(selectedFactoryId, selectedBookId)
1612 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1613 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1617 self._bookSelectionButton.set_label("")
1618 self._contactsview.set_model(None)
1619 self._contactsview.remove_column(self._contactColumn)
1621 def add_contact(self, *args, **kwds):
1623 @note Actual dial function is patched in later
1625 raise NotImplementedError("Horrible unknown error has occurred")
1627 def get_addressbooks(self):
1629 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1631 for i, factory in enumerate(self._addressBookFactories):
1632 for bookFactory, bookId, bookName in factory.get_addressbooks():
1633 yield (str(i), bookId), (factory.factory_name(), bookName)
1635 def open_addressbook(self, bookFactoryId, bookId):
1636 bookFactoryIndex = int(bookFactoryId)
1637 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1638 self._addressBook = addressBook
1640 def update(self, force = False):
1641 if not force and self._isPopulated:
1643 self._updateSink.send(())
1647 self._isPopulated = False
1648 self._contactsmodel.clear()
1649 for factory in self._addressBookFactories:
1650 factory.clear_caches()
1651 self._addressBook.clear_caches()
1653 def append(self, book):
1654 self._addressBookFactories.append(book)
1656 def extend(self, books):
1657 self._addressBookFactories.extend(books)
1663 def load_settings(self, config, sectionName):
1665 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1666 except ConfigParser.NoOptionError:
1667 self._selectedComboIndex = 0
1669 def save_settings(self, config, sectionName):
1670 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1672 def _idly_populate_contactsview(self):
1673 with gtk_toolbox.gtk_lock():
1674 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1677 while addressBook is not self._addressBook:
1678 addressBook = self._addressBook
1679 with gtk_toolbox.gtk_lock():
1680 self._contactsview.set_model(None)
1684 contacts = addressBook.get_contacts()
1685 except Exception, e:
1687 self._isPopulated = False
1688 self._errorDisplay.push_exception_with_lock()
1689 for contactId, contactName in contacts:
1690 contactType = addressBook.contact_source_short_name(contactId)
1691 row = contactType, contactName, contactId
1692 self._contactsmodel.append(row)
1694 with gtk_toolbox.gtk_lock():
1695 self._contactsview.set_model(self._contactsmodel)
1697 self._isPopulated = True
1698 except Exception, e:
1699 self._errorDisplay.push_exception_with_lock()
1701 with gtk_toolbox.gtk_lock():
1702 hildonize.show_busy_banner_end(banner)
1705 def _on_addressbook_button_changed(self, *args, **kwds):
1708 newSelectedComboIndex = hildonize.touch_selector(
1711 (("%s" % m[2]) for m in self._booksList),
1712 self._selectedComboIndex,
1714 except RuntimeError:
1717 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1718 selectedBookId = self._booksList[newSelectedComboIndex][1]
1720 oldAddressbook = self._addressBook
1721 self.open_addressbook(selectedFactoryId, selectedBookId)
1722 forceUpdate = True if oldAddressbook is not self._addressBook else False
1723 self.update(force=forceUpdate)
1725 self._selectedComboIndex = newSelectedComboIndex
1726 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1727 except Exception, e:
1728 self._errorDisplay.push_exception()
1730 def _on_contactsview_row_activated(self, treeview, path, view_column):
1732 itr = self._contactsmodel.get_iter(path)
1736 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1737 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1739 contactDetails = self._addressBook.get_contact_details(contactId)
1740 except Exception, e:
1742 self._errorDisplay.push_exception()
1743 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1745 if len(contactPhoneNumbers) == 0:
1750 contactPhoneNumbers,
1751 messages = (contactName, ),
1753 self._contactsviewselection.unselect_all()
1754 except Exception, e:
1755 self._errorDisplay.push_exception()