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)
262 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
264 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
265 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
266 errorClose = self._widgetTree.get_widget("smsErrorClose")
267 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
269 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
270 self._smsButton.connect("clicked", self._on_send)
271 self._dialButton = self._widgetTree.get_widget("dialButton")
272 self._dialButton.connect("clicked", self._on_dial)
274 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
276 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
277 self._messagesView = self._widgetTree.get_widget("smsMessages")
279 textrenderer = gtk.CellRendererText()
280 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
281 textrenderer.set_property("wrap-width", 450)
282 messageColumn = gtk.TreeViewColumn("")
283 messageColumn.pack_start(textrenderer, expand=True)
284 messageColumn.add_attribute(textrenderer, "markup", 0)
285 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
286 self._messagesView.append_column(messageColumn)
287 self._messagesView.set_headers_visible(False)
288 self._messagesView.set_model(self._messagemodel)
289 self._messagesView.set_fixed_height_mode(False)
291 self._conversationView = self._messagesView.get_parent()
292 self._conversationViewPort = self._conversationView.get_parent()
293 self._scrollWindow = self._conversationViewPort.get_parent()
295 self._targetList = self._widgetTree.get_widget("smsTargetList")
296 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
297 self._phoneButton.connect("clicked", self._on_phone)
298 self._smsEntry = self._widgetTree.get_widget("smsEntry")
299 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
300 self._smsEntrySize = None
304 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
305 contactNumbers = list(self._to_contact_numbers(contactDetails))
306 assert contactNumbers, "Contact must have at least one number"
307 contactIndex = defaultIndex if defaultIndex != -1 else 0
308 contact = contactNumbers, contactIndex, messages
309 self._contacts.append(contact)
311 nameLabel = gtk.Label(name)
312 selector = gtk.Button(contactNumbers[0][1])
313 if len(contactNumbers) == 1:
314 selector.set_sensitive(False)
315 removeContact = gtk.Button(stock="gtk-delete")
317 row.pack_start(nameLabel, True, True)
318 row.pack_start(selector, True, True)
319 row.pack_start(removeContact, False, False)
321 self._targetList.pack_start(row)
322 selector.connect("clicked", self._on_choose_phone_n, row)
323 removeContact.connect("clicked", self._on_remove_phone_n, row)
324 self._update_button_state()
325 self._update_context()
327 parentSize = self._parent.get_size()
328 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
330 self._window.present()
332 self._smsEntry.grab_focus()
333 self._scroll_to_bottom()
336 del self._contacts[:]
338 for row in list(self._targetList.get_children()):
339 self._targetList.remove(row)
340 self._smsEntry.get_buffer().set_text("")
341 self._update_letter_count()
342 self._update_context()
344 def fullscreen(self):
345 self._window.fullscreen()
347 def unfullscreen(self):
348 self._window.unfullscreen()
350 def _remove_contact(self, contactIndex):
351 del self._contacts[contactIndex]
353 row = list(self._targetList.get_children())[contactIndex]
354 self._targetList.remove(row)
355 self._update_button_state()
356 self._update_context()
357 self._scroll_to_bottom()
359 def _scroll_to_bottom(self):
360 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
362 adjustment = self._scrollWindow.get_vadjustment()
363 adjustment.value = dx
365 def _update_letter_count(self):
366 if self._smsEntrySize is None:
367 self._smsEntrySize = self._smsEntry.size_request()
369 self._smsEntry.set_size_request(*self._smsEntrySize)
370 entryLength = self._smsEntry.get_buffer().get_char_count()
372 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
374 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
376 self._letterCountLabel.set_text("%s" % (numCharInText, ))
378 self._update_button_state()
380 def _update_context(self):
381 self._messagemodel.clear()
382 if len(self._contacts) == 0:
383 self._messagesView.hide()
384 self._targetList.hide()
385 self._phoneButton.hide()
386 self._phoneButton.set_label("Error: You shouldn't see this")
387 elif len(self._contacts) == 1:
388 contactNumbers, index, messages = self._contacts[0]
390 self._messagesView.show()
391 for message in messages:
393 self._messagemodel.append(row)
394 messagesSelection = self._messagesView.get_selection()
395 messagesSelection.select_path((len(messages)-1, ))
397 self._messagesView.hide()
398 self._targetList.hide()
399 self._phoneButton.show()
400 self._phoneButton.set_label(contactNumbers[index][1])
401 if 1 < len(contactNumbers):
402 self._phoneButton.set_sensitive(True)
404 self._phoneButton.set_sensitive(False)
406 self._messagesView.hide()
407 self._targetList.show()
408 self._phoneButton.hide()
409 self._phoneButton.set_label("Error: You shouldn't see this")
411 def _update_button_state(self):
412 if len(self._contacts) == 0:
413 self._dialButton.set_sensitive(False)
414 self._smsButton.set_sensitive(False)
415 elif len(self._contacts) == 1:
416 entryLength = self._smsEntry.get_buffer().get_char_count()
418 self._dialButton.set_sensitive(True)
419 self._smsButton.set_sensitive(False)
421 self._dialButton.set_sensitive(False)
422 self._smsButton.set_sensitive(True)
424 self._dialButton.set_sensitive(False)
425 self._smsButton.set_sensitive(True)
427 def _to_contact_numbers(self, contactDetails):
428 for phoneType, phoneNumber in contactDetails:
429 display = " - ".join((make_pretty(phoneNumber), phoneType))
430 yield (phoneNumber, display)
432 def _pseudo_destroy(self):
436 def _request_number(self, contactIndex):
437 contactNumbers, index, messages = self._contacts[contactIndex]
438 assert 0 <= index, "%r" % index
440 index = hildonize.touch_selector(
443 (description for (number, description) in contactNumbers),
446 self._contacts[contactIndex] = contactNumbers, index, messages
448 def send_sms(self, numbers, message):
449 raise NotImplementedError()
451 def dial(self, number):
452 raise NotImplementedError()
454 def _on_phone(self, *args):
456 assert len(self._contacts) == 1, "One and only one contact is required"
457 self._request_number(0)
459 contactNumbers, numberIndex, messages = self._contacts[0]
460 self._phoneButton.set_label(contactNumbers[numberIndex][1])
461 row = list(self._targetList.get_children())[0]
462 phoneButton = list(row.get_children())[1]
463 phoneButton.set_label(contactNumbers[numberIndex][1])
465 self._errorDisplay.push_exception()
467 def _on_choose_phone_n(self, button, row):
469 assert 1 < len(self._contacts), "More than one contact required"
470 targetList = list(self._targetList.get_children())
471 index = targetList.index(row)
472 self._request_number(index)
474 contactNumbers, numberIndex, messages = self._contacts[0]
475 phoneButton = list(row.get_children())[1]
476 phoneButton.set_label(contactNumbers[numberIndex][1])
478 self._errorDisplay.push_exception()
480 def _on_remove_phone_n(self, button, row):
482 assert 1 < len(self._contacts), "More than one contact required"
483 targetList = list(self._targetList.get_children())
484 index = targetList.index(row)
486 del self._contacts[index]
487 self._targetList.remove(row)
488 self._update_context()
489 self._update_button_state()
491 self._errorDisplay.push_exception()
493 def _on_entry_changed(self, *args):
495 self._update_letter_count()
497 self._errorDisplay.push_exception()
499 def _on_send(self, *args):
501 assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
503 make_ugly(contact[0][contact[1]][0])
504 for contact in self._contacts
507 entryBuffer = self._smsEntry.get_buffer()
508 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
509 enteredMessage = enteredMessage.strip()
510 assert enteredMessage, "No message provided"
511 self.send_sms(phoneNumbers, enteredMessage)
512 self._pseudo_destroy()
514 self._errorDisplay.push_exception()
516 def _on_dial(self, *args):
518 assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
519 contact = self._contacts[0]
520 contactNumber = contact[0][contact[1]][0]
521 phoneNumber = make_ugly(contactNumber)
522 self.dial(phoneNumber)
523 self._pseudo_destroy()
525 self._errorDisplay.push_exception()
527 def _on_delete(self, *args):
529 self._window.emit_stop_by_name("delete-event")
530 if hildonize.IS_FREMANTLE_SUPPORTED:
533 self._pseudo_destroy()
535 self._errorDisplay.push_exception()
538 def _on_window_state_change(self, widget, event, *args):
540 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
541 self._isFullScreen = True
543 self._isFullScreen = False
545 self._errorDisplay.push_exception()
547 def _on_key_press(self, widget, event):
548 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
551 event.keyval == gtk.keysyms.F6 or
552 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
554 if self._isFullScreen:
555 self._window.unfullscreen()
557 self._window.fullscreen()
558 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
561 for messagePart in self._messagemodel
563 self._clipboard.set_text(str(message))
565 event.keyval == gtk.keysyms.h and
566 event.get_state() & gtk.gdk.CONTROL_MASK
570 event.keyval == gtk.keysyms.w and
571 event.get_state() & gtk.gdk.CONTROL_MASK
573 self._pseudo_destroy()
575 event.keyval == gtk.keysyms.q and
576 event.get_state() & gtk.gdk.CONTROL_MASK
578 self._parent.destroy()
580 self._errorDisplay.push_exception()
583 class Dialpad(object):
585 def __init__(self, widgetTree, errorDisplay):
586 self._clipboard = gtk.clipboard_get()
587 self._errorDisplay = errorDisplay
589 self._numberdisplay = widgetTree.get_widget("numberdisplay")
590 self._callButton = widgetTree.get_widget("dialpadCall")
591 self._sendSMSButton = widgetTree.get_widget("dialpadSMS")
592 self._backButton = widgetTree.get_widget("back")
593 self._plusButton = widgetTree.get_widget("plus")
594 self._phonenumber = ""
595 self._prettynumber = ""
598 "on_digit_clicked": self._on_digit_clicked,
600 widgetTree.signal_autoconnect(callbackMapping)
601 self._sendSMSButton.connect("clicked", self._on_sms_clicked)
602 self._callButton.connect("clicked", self._on_call_clicked)
603 self._plusButton.connect("clicked", self._on_plus)
605 self._originalLabel = self._backButton.get_label()
606 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
607 self._backTapHandler.on_tap = self._on_backspace
608 self._backTapHandler.on_hold = self._on_clearall
609 self._backTapHandler.on_holding = self._set_clear_button
610 self._backTapHandler.on_cancel = self._reset_back_button
612 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
613 self._keyPressEventId = 0
616 self._sendSMSButton.grab_focus()
617 self._backTapHandler.enable()
618 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
621 self._window.disconnect(self._keyPressEventId)
622 self._keyPressEventId = 0
623 self._reset_back_button()
624 self._backTapHandler.disable()
626 def add_contact(self, *args, **kwds):
628 @note Actual function is patched in later
630 raise NotImplementedError("Horrible unknown error has occurred")
632 def dial(self, number):
634 @note Actual function is patched in later
636 raise NotImplementedError("Horrible unknown error has occurred")
638 def get_number(self):
639 return self._phonenumber
641 def set_number(self, number):
643 Set the number to dial
646 self._phonenumber = make_ugly(number)
647 self._prettynumber = make_pretty(self._phonenumber)
648 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
649 if self._phonenumber:
650 self._plusButton.set_sensitive(False)
652 self._plusButton.set_sensitive(True)
654 self._errorDisplay.push_exception()
663 def load_settings(self, config, section):
666 def save_settings(self, config, section):
668 @note Thread Agnostic
672 def _on_key_press(self, widget, event):
674 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
675 contents = self._clipboard.wait_for_text()
676 if contents is not None:
677 self.set_number(contents)
679 self._errorDisplay.push_exception()
681 def _on_call_clicked(self, widget):
683 phoneNumber = self.get_number()
684 self.dial(phoneNumber)
687 self._errorDisplay.push_exception()
689 def _on_sms_clicked(self, widget):
691 phoneNumber = self.get_number()
694 [("Dialer", phoneNumber)], ()
698 self._errorDisplay.push_exception()
700 def _on_digit_clicked(self, widget):
702 self.set_number(self._phonenumber + widget.get_name()[-1])
704 self._errorDisplay.push_exception()
706 def _on_plus(self, *args):
708 self.set_number(self._phonenumber + "+")
710 self._errorDisplay.push_exception()
712 def _on_backspace(self, taps):
714 self.set_number(self._phonenumber[:-taps])
715 self._reset_back_button()
717 self._errorDisplay.push_exception()
719 def _on_clearall(self, taps):
722 self._reset_back_button()
724 self._errorDisplay.push_exception()
727 def _set_clear_button(self):
729 self._backButton.set_label("gtk-clear")
731 self._errorDisplay.push_exception()
733 def _reset_back_button(self):
735 self._backButton.set_label(self._originalLabel)
737 self._errorDisplay.push_exception()
740 class AccountInfo(object):
742 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
743 self._errorDisplay = errorDisplay
744 self._backend = backend
745 self._isPopulated = False
746 self._alarmHandler = alarmHandler
747 self._notifyOnMissed = False
748 self._notifyOnVoicemail = False
749 self._notifyOnSms = False
751 self._callbackList = []
752 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
753 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
754 self._onCallbackSelectChangedId = 0
756 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
757 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
758 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
759 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
760 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
761 self._onNotifyToggled = 0
762 self._onMinutesChanged = 0
763 self._onMissedToggled = 0
764 self._onVoicemailToggled = 0
765 self._onSmsToggled = 0
766 self._applyAlarmTimeoutId = None
768 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
769 self._callbackNumber = ""
772 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
774 self._accountViewNumberDisplay.set_use_markup(True)
775 self.set_account_number("")
777 del self._callbackList[:]
778 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
779 self._set_callback_label("")
781 if self._alarmHandler is not None:
782 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
783 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
784 self._missedCheckbox.set_active(self._notifyOnMissed)
785 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
786 self._smsCheckbox.set_active(self._notifyOnSms)
788 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
789 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
790 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
791 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
792 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
794 self._notifyCheckbox.set_sensitive(False)
795 self._minutesEntryButton.set_sensitive(False)
796 self._missedCheckbox.set_sensitive(False)
797 self._voicemailCheckbox.set_sensitive(False)
798 self._smsCheckbox.set_sensitive(False)
800 self.update(force=True)
803 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
804 self._onCallbackSelectChangedId = 0
805 self._set_callback_label("")
807 if self._alarmHandler is not None:
808 self._notifyCheckbox.disconnect(self._onNotifyToggled)
809 self._minutesEntryButton.disconnect(self._onMinutesChanged)
810 self._missedCheckbox.disconnect(self._onNotifyToggled)
811 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
812 self._smsCheckbox.disconnect(self._onNotifyToggled)
813 self._onNotifyToggled = 0
814 self._onMinutesChanged = 0
815 self._onMissedToggled = 0
816 self._onVoicemailToggled = 0
817 self._onSmsToggled = 0
819 self._notifyCheckbox.set_sensitive(True)
820 self._minutesEntryButton.set_sensitive(True)
821 self._missedCheckbox.set_sensitive(True)
822 self._voicemailCheckbox.set_sensitive(True)
823 self._smsCheckbox.set_sensitive(True)
826 del self._callbackList[:]
828 def set_account_number(self, number):
830 Displays current account number
832 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
834 def update(self, force = False):
835 if not force and self._isPopulated:
837 self._populate_callback_combo()
838 self.set_account_number(self._backend.get_account_number())
842 self._set_callback_label("")
843 self.set_account_number("")
844 self._isPopulated = False
846 def save_everything(self):
847 raise NotImplementedError
851 return "Account Info"
853 def load_settings(self, config, section):
854 self._callbackNumber = make_ugly(config.get(section, "callback"))
855 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
856 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
857 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
859 def save_settings(self, config, section):
861 @note Thread Agnostic
863 config.set(section, "callback", self._callbackNumber)
864 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
865 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
866 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
868 def _populate_callback_combo(self):
869 self._isPopulated = True
870 del self._callbackList[:]
872 callbackNumbers = self._backend.get_callback_numbers()
874 self._errorDisplay.push_exception()
875 self._isPopulated = False
878 if len(callbackNumbers) == 0:
879 callbackNumbers = {"": "No callback numbers available"}
881 for number, description in callbackNumbers.iteritems():
882 self._callbackList.append((make_pretty(number), description))
884 self._set_callback_number(self._callbackNumber)
886 def _set_callback_number(self, number):
888 if not self._backend.is_valid_syntax(number) and 0 < len(number):
889 self._errorDisplay.push_message("%s is not a valid callback number" % number)
890 elif number == self._backend.get_callback_number() and 0 < len(number):
891 _moduleLogger.warning(
892 "Callback number already is %s" % (
893 self._backend.get_callback_number(),
896 self._set_callback_label(number)
898 if number.startswith("1747"): number = "+" + number
899 self._backend.set_callback_number(number)
900 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
901 make_pretty(number), make_pretty(self._backend.get_callback_number())
903 self._callbackNumber = make_ugly(number)
904 self._set_callback_label(number)
906 "Callback number set to %s" % (
907 self._backend.get_callback_number(),
911 self._errorDisplay.push_exception()
913 def _set_callback_label(self, uglyNumber):
914 prettyNumber = make_pretty(uglyNumber)
915 if len(prettyNumber) == 0:
916 prettyNumber = "No Callback Number"
917 self._callbackSelectButton.set_label(prettyNumber)
919 def _update_alarm_settings(self, recurrence):
921 isEnabled = self._notifyCheckbox.get_active()
922 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
923 self._alarmHandler.apply_settings(isEnabled, recurrence)
925 self.save_everything()
926 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
927 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
929 def _on_callbackentry_clicked(self, *args):
931 actualSelection = make_pretty(self._callbackNumber)
934 (number, "%s (%s)" % (number, description))
935 for (number, description) in self._callbackList
937 defaultSelection = userOptions.get(actualSelection, actualSelection)
939 userSelection = hildonize.touch_selector_entry(
942 list(userOptions.itervalues()),
945 reversedUserOptions = dict(
946 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
948 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
950 number = make_ugly(selectedNumber)
951 self._set_callback_number(number)
952 except RuntimeError, e:
953 _moduleLogger.exception("%s" % str(e))
955 self._errorDisplay.push_exception()
957 def _on_notify_toggled(self, *args):
959 if self._applyAlarmTimeoutId is not None:
960 gobject.source_remove(self._applyAlarmTimeoutId)
961 self._applyAlarmTimeoutId = None
962 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
964 self._errorDisplay.push_exception()
966 def _on_minutes_clicked(self, *args):
967 recurrenceChoices = [
983 actualSelection = self._alarmHandler.recurrence
985 closestSelectionIndex = 0
986 for i, possible in enumerate(recurrenceChoices):
987 if possible[0] <= actualSelection:
988 closestSelectionIndex = i
989 recurrenceIndex = hildonize.touch_selector(
992 (("%s" % m[1]) for m in recurrenceChoices),
993 closestSelectionIndex,
995 recurrence = recurrenceChoices[recurrenceIndex][0]
997 self._update_alarm_settings(recurrence)
998 except RuntimeError, e:
999 _moduleLogger.exception("%s" % str(e))
1000 except Exception, e:
1001 self._errorDisplay.push_exception()
1003 def _on_apply_timeout(self, *args):
1005 self._applyAlarmTimeoutId = None
1007 self._update_alarm_settings(self._alarmHandler.recurrence)
1008 except Exception, e:
1009 self._errorDisplay.push_exception()
1012 def _on_missed_toggled(self, *args):
1014 self._notifyOnMissed = self._missedCheckbox.get_active()
1015 self.save_everything()
1016 except Exception, e:
1017 self._errorDisplay.push_exception()
1019 def _on_voicemail_toggled(self, *args):
1021 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1022 self.save_everything()
1023 except Exception, e:
1024 self._errorDisplay.push_exception()
1026 def _on_sms_toggled(self, *args):
1028 self._notifyOnSms = self._smsCheckbox.get_active()
1029 self.save_everything()
1030 except Exception, e:
1031 self._errorDisplay.push_exception()
1034 class CallHistoryView(object):
1042 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1044 def __init__(self, widgetTree, backend, errorDisplay):
1045 self._errorDisplay = errorDisplay
1046 self._backend = backend
1048 self._isPopulated = False
1049 self._historymodel = gtk.ListStore(
1050 gobject.TYPE_STRING, # number
1051 gobject.TYPE_STRING, # date
1052 gobject.TYPE_STRING, # action
1053 gobject.TYPE_STRING, # from
1054 gobject.TYPE_STRING, # from id
1056 self._historymodelfiltered = self._historymodel.filter_new()
1057 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1058 self._historyview = widgetTree.get_widget("historyview")
1059 self._historyviewselection = None
1060 self._onRecentviewRowActivatedId = 0
1062 textrenderer = gtk.CellRendererText()
1063 textrenderer.set_property("yalign", 0)
1064 self._dateColumn = gtk.TreeViewColumn("Date")
1065 self._dateColumn.pack_start(textrenderer, expand=True)
1066 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1068 textrenderer = gtk.CellRendererText()
1069 textrenderer.set_property("yalign", 0)
1070 self._actionColumn = gtk.TreeViewColumn("Action")
1071 self._actionColumn.pack_start(textrenderer, expand=True)
1072 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1074 textrenderer = gtk.CellRendererText()
1075 textrenderer.set_property("yalign", 0)
1076 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1077 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1078 self._numberColumn = gtk.TreeViewColumn("Number")
1079 self._numberColumn.pack_start(textrenderer, expand=True)
1080 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1082 textrenderer = gtk.CellRendererText()
1083 textrenderer.set_property("yalign", 0)
1084 hildonize.set_cell_thumb_selectable(textrenderer)
1085 self._nameColumn = gtk.TreeViewColumn("From")
1086 self._nameColumn.pack_start(textrenderer, expand=True)
1087 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1088 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1090 self._window = gtk_toolbox.find_parent_window(self._historyview)
1092 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1093 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1094 self._selectedFilter = "All"
1096 self._updateSink = gtk_toolbox.threaded_stage(
1098 self._idly_populate_historyview,
1099 gtk_toolbox.null_sink(),
1104 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1105 self._historyFilterSelector.set_label(self._selectedFilter)
1107 self._historyview.set_model(self._historymodelfiltered)
1108 self._historyview.set_fixed_height_mode(False)
1110 self._historyview.append_column(self._dateColumn)
1111 self._historyview.append_column(self._actionColumn)
1112 self._historyview.append_column(self._numberColumn)
1113 self._historyview.append_column(self._nameColumn)
1114 self._historyviewselection = self._historyview.get_selection()
1115 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1117 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1120 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1124 self._historyview.remove_column(self._dateColumn)
1125 self._historyview.remove_column(self._actionColumn)
1126 self._historyview.remove_column(self._nameColumn)
1127 self._historyview.remove_column(self._numberColumn)
1128 self._historyview.set_model(None)
1130 def add_contact(self, *args, **kwds):
1132 @note Actual dial function is patched in later
1134 raise NotImplementedError("Horrible unknown error has occurred")
1136 def update(self, force = False):
1137 if not force and self._isPopulated:
1139 self._updateSink.send(())
1143 self._isPopulated = False
1144 self._historymodel.clear()
1148 return "Recent Calls"
1150 def load_settings(self, config, sectionName):
1152 self._selectedFilter = config.get(sectionName, "filter")
1153 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1154 self._messageType = self.HISTORY_ITEM_TYPES[0]
1155 except ConfigParser.NoOptionError:
1158 def save_settings(self, config, sectionName):
1160 @note Thread Agnostic
1162 config.set(sectionName, "filter", self._selectedFilter)
1164 def _is_history_visible(self, model, iter):
1166 action = model.get_value(iter, self.ACTION_IDX)
1168 return False # this seems weird but oh well
1170 if self._selectedFilter in [action, "All"]:
1174 except Exception, e:
1175 self._errorDisplay.push_exception()
1177 def _idly_populate_historyview(self):
1178 with gtk_toolbox.gtk_lock():
1179 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1181 self._historymodel.clear()
1182 self._isPopulated = True
1185 historyItems = self._backend.get_recent()
1186 except Exception, e:
1187 self._errorDisplay.push_exception_with_lock()
1188 self._isPopulated = False
1192 gv_backend.decorate_recent(data)
1193 for data in gv_backend.sort_messages(historyItems)
1196 for contactId, personName, phoneNumber, date, action in historyItems:
1198 personName = "Unknown"
1199 date = abbrev_relative_date(date)
1200 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1201 prettyNumber = make_pretty(prettyNumber)
1202 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1203 with gtk_toolbox.gtk_lock():
1204 self._historymodel.append(item)
1205 except Exception, e:
1206 self._errorDisplay.push_exception_with_lock()
1208 with gtk_toolbox.gtk_lock():
1209 hildonize.show_busy_banner_end(banner)
1213 def _on_history_filter_clicked(self, *args, **kwds):
1215 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1218 newSelectedComboIndex = hildonize.touch_selector(
1221 self.HISTORY_ITEM_TYPES,
1224 except RuntimeError:
1227 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1228 self._selectedFilter = option
1229 self._historyFilterSelector.set_label(self._selectedFilter)
1230 self._historymodelfiltered.refilter()
1231 except Exception, e:
1232 self._errorDisplay.push_exception()
1234 def _history_summary(self, expectedNumber):
1235 for number, action, date, whoFrom, whoFromId in self._historymodel:
1236 if expectedNumber is not None and expectedNumber == number:
1237 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1239 def _on_historyview_row_activated(self, treeview, path, view_column):
1241 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1242 itr = self._historymodel.get_iter(childPath)
1246 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1247 number = make_ugly(prettyNumber)
1248 description = list(self._history_summary(prettyNumber))
1249 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1250 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1251 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1255 contactPhoneNumbers,
1256 messages = description,
1257 defaultIndex = defaultIndex,
1259 self._historyviewselection.unselect_all()
1260 except Exception, e:
1261 self._errorDisplay.push_exception()
1264 class MessagesView(object):
1272 MESSAGE_DATA_IDX = 6
1274 NO_MESSAGES = "None"
1275 VOICEMAIL_MESSAGES = "Voicemail"
1276 TEXT_MESSAGES = "SMS"
1277 ALL_TYPES = "All Messages"
1278 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1280 UNREAD_STATUS = "Unread"
1281 UNARCHIVED_STATUS = "Inbox"
1283 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1285 def __init__(self, widgetTree, backend, errorDisplay):
1286 self._errorDisplay = errorDisplay
1287 self._backend = backend
1289 self._isPopulated = False
1290 self._messagemodel = gtk.ListStore(
1291 gobject.TYPE_STRING, # number
1292 gobject.TYPE_STRING, # date
1293 gobject.TYPE_STRING, # header
1294 gobject.TYPE_STRING, # message
1296 gobject.TYPE_STRING, # from id
1297 object, # message data
1299 self._messagemodelfiltered = self._messagemodel.filter_new()
1300 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1301 self._messageview = widgetTree.get_widget("messages_view")
1302 self._messageviewselection = None
1303 self._onMessageviewRowActivatedId = 0
1305 self._messageRenderer = gtk.CellRendererText()
1306 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1307 self._messageRenderer.set_property("wrap-width", 500)
1308 self._messageColumn = gtk.TreeViewColumn("Messages")
1309 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1310 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1311 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1313 self._window = gtk_toolbox.find_parent_window(self._messageview)
1315 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1316 self._onMessageTypeClickedId = 0
1317 self._messageType = self.ALL_TYPES
1318 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1319 self._onMessageStatusClickedId = 0
1320 self._messageStatus = self.ALL_STATUS
1322 self._updateSink = gtk_toolbox.threaded_stage(
1324 self._idly_populate_messageview,
1325 gtk_toolbox.null_sink(),
1330 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1331 self._messageview.set_model(self._messagemodelfiltered)
1332 self._messageview.set_headers_visible(False)
1333 self._messageview.set_fixed_height_mode(False)
1335 self._messageview.append_column(self._messageColumn)
1336 self._messageviewselection = self._messageview.get_selection()
1337 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1339 self._messageTypeButton.set_label(self._messageType)
1340 self._messageStatusButton.set_label(self._messageStatus)
1342 self._onMessageviewRowActivatedId = self._messageview.connect(
1343 "row-activated", self._on_messageview_row_activated
1345 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1346 "clicked", self._on_message_type_clicked
1348 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1349 "clicked", self._on_message_status_clicked
1353 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1354 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1355 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1359 self._messageview.remove_column(self._messageColumn)
1360 self._messageview.set_model(None)
1362 def add_contact(self, *args, **kwds):
1364 @note Actual dial function is patched in later
1366 raise NotImplementedError("Horrible unknown error has occurred")
1368 def update(self, force = False):
1369 if not force and self._isPopulated:
1371 self._updateSink.send(())
1375 self._isPopulated = False
1376 self._messagemodel.clear()
1382 def load_settings(self, config, sectionName):
1384 self._messageType = config.get(sectionName, "type")
1385 if self._messageType not in self.MESSAGE_TYPES:
1386 self._messageType = self.ALL_TYPES
1387 self._messageStatus = config.get(sectionName, "status")
1388 if self._messageStatus not in self.MESSAGE_STATUSES:
1389 self._messageStatus = self.ALL_STATUS
1390 except ConfigParser.NoOptionError:
1393 def save_settings(self, config, sectionName):
1395 @note Thread Agnostic
1397 config.set(sectionName, "status", self._messageStatus)
1398 config.set(sectionName, "type", self._messageType)
1400 def _is_message_visible(self, model, iter):
1402 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1404 return False # this seems weird but oh well
1405 return self._filter_messages(message, self._messageType, self._messageStatus)
1406 except Exception, e:
1407 self._errorDisplay.push_exception()
1410 def _filter_messages(cls, message, type, status):
1411 if type == cls.ALL_TYPES:
1414 messageType = message["type"]
1415 isType = messageType == type
1417 if status == cls.ALL_STATUS:
1420 isUnarchived = not message["isArchived"]
1421 isUnread = not message["isRead"]
1422 if status == cls.UNREAD_STATUS:
1423 isStatus = isUnarchived and isUnread
1424 elif status == cls.UNARCHIVED_STATUS:
1425 isStatus = isUnarchived
1427 assert "Status %s is bad for %r" % (status, message)
1429 return isType and isStatus
1431 _MIN_MESSAGES_SHOWN = 4
1433 def _idly_populate_messageview(self):
1434 with gtk_toolbox.gtk_lock():
1435 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1437 self._messagemodel.clear()
1438 self._isPopulated = True
1440 if self._messageType == self.NO_MESSAGES:
1444 messageItems = self._backend.get_messages()
1445 except Exception, e:
1446 self._errorDisplay.push_exception_with_lock()
1447 self._isPopulated = False
1451 (gv_backend.decorate_message(message), message)
1452 for message in gv_backend.sort_messages(messageItems)
1455 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1456 prettyNumber = number[2:] if number.startswith("+1") else number
1457 prettyNumber = make_pretty(prettyNumber)
1459 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1460 expandedMessages = [firstMessage]
1461 expandedMessages.extend(messages)
1462 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1463 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1464 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1465 collapsedMessages = [firstMessage, secondMessage]
1466 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1468 collapsedMessages = expandedMessages
1469 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1471 number = make_ugly(number)
1473 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1474 with gtk_toolbox.gtk_lock():
1475 self._messagemodel.append(row)
1476 except Exception, e:
1477 self._errorDisplay.push_exception_with_lock()
1479 with gtk_toolbox.gtk_lock():
1480 hildonize.show_busy_banner_end(banner)
1481 self._messagemodelfiltered.refilter()
1485 def _on_messageview_row_activated(self, treeview, path, view_column):
1487 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1488 itr = self._messagemodel.get_iter(childPath)
1492 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1493 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1495 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1496 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1497 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1501 contactPhoneNumbers,
1502 messages = description,
1503 defaultIndex = defaultIndex,
1505 self._messageviewselection.unselect_all()
1506 except Exception, e:
1507 self._errorDisplay.push_exception()
1509 def _on_message_type_clicked(self, *args, **kwds):
1511 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1514 newSelectedIndex = hildonize.touch_selector(
1520 except RuntimeError:
1523 if selectedIndex != newSelectedIndex:
1524 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1525 self._messageTypeButton.set_label(self._messageType)
1526 self._messagemodelfiltered.refilter()
1527 except Exception, e:
1528 self._errorDisplay.push_exception()
1530 def _on_message_status_clicked(self, *args, **kwds):
1532 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1535 newSelectedIndex = hildonize.touch_selector(
1538 self.MESSAGE_STATUSES,
1541 except RuntimeError:
1544 if selectedIndex != newSelectedIndex:
1545 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1546 self._messageStatusButton.set_label(self._messageStatus)
1547 self._messagemodelfiltered.refilter()
1548 except Exception, e:
1549 self._errorDisplay.push_exception()
1552 class ContactsView(object):
1554 CONTACT_TYPE_IDX = 0
1555 CONTACT_NAME_IDX = 1
1558 def __init__(self, widgetTree, backend, errorDisplay):
1559 self._errorDisplay = errorDisplay
1560 self._backend = backend
1562 self._addressBook = None
1563 self._selectedComboIndex = 0
1564 self._addressBookFactories = [null_backend.NullAddressBook()]
1566 self._booksList = []
1567 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1569 self._isPopulated = False
1570 self._contactsmodel = gtk.ListStore(
1571 gobject.TYPE_STRING, # Contact Type
1572 gobject.TYPE_STRING, # Contact Name
1573 gobject.TYPE_STRING, # Contact ID
1575 self._contactsviewselection = None
1576 self._contactsview = widgetTree.get_widget("contactsview")
1578 self._contactColumn = gtk.TreeViewColumn("Contact")
1579 displayContactSource = False
1580 if displayContactSource:
1581 textrenderer = gtk.CellRendererText()
1582 self._contactColumn.pack_start(textrenderer, expand=False)
1583 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1584 textrenderer = gtk.CellRendererText()
1585 hildonize.set_cell_thumb_selectable(textrenderer)
1586 self._contactColumn.pack_start(textrenderer, expand=True)
1587 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1588 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1589 self._contactColumn.set_sort_column_id(1)
1590 self._contactColumn.set_visible(True)
1592 self._onContactsviewRowActivatedId = 0
1593 self._onAddressbookButtonChangedId = 0
1594 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1596 self._updateSink = gtk_toolbox.threaded_stage(
1598 self._idly_populate_contactsview,
1599 gtk_toolbox.null_sink(),
1604 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1606 self._contactsview.set_model(self._contactsmodel)
1607 self._contactsview.set_fixed_height_mode(False)
1608 self._contactsview.append_column(self._contactColumn)
1609 self._contactsviewselection = self._contactsview.get_selection()
1610 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1612 del self._booksList[:]
1613 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1614 if factoryName and bookName:
1615 entryName = "%s: %s" % (factoryName, bookName)
1617 entryName = factoryName
1619 entryName = bookName
1621 entryName = "Bad name (%d)" % factoryId
1622 row = (str(factoryId), bookId, entryName)
1623 self._booksList.append(row)
1625 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1626 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1628 if len(self._booksList) <= self._selectedComboIndex:
1629 self._selectedComboIndex = 0
1630 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1632 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1633 selectedBookId = self._booksList[self._selectedComboIndex][1]
1634 self.open_addressbook(selectedFactoryId, selectedBookId)
1637 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1638 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1642 self._bookSelectionButton.set_label("")
1643 self._contactsview.set_model(None)
1644 self._contactsview.remove_column(self._contactColumn)
1646 def add_contact(self, *args, **kwds):
1648 @note Actual dial function is patched in later
1650 raise NotImplementedError("Horrible unknown error has occurred")
1652 def get_addressbooks(self):
1654 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1656 for i, factory in enumerate(self._addressBookFactories):
1657 for bookFactory, bookId, bookName in factory.get_addressbooks():
1658 yield (str(i), bookId), (factory.factory_name(), bookName)
1660 def open_addressbook(self, bookFactoryId, bookId):
1661 bookFactoryIndex = int(bookFactoryId)
1662 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1663 self._addressBook = addressBook
1665 def update(self, force = False):
1666 if not force and self._isPopulated:
1668 self._updateSink.send(())
1672 self._isPopulated = False
1673 self._contactsmodel.clear()
1674 for factory in self._addressBookFactories:
1675 factory.clear_caches()
1676 self._addressBook.clear_caches()
1678 def append(self, book):
1679 self._addressBookFactories.append(book)
1681 def extend(self, books):
1682 self._addressBookFactories.extend(books)
1688 def load_settings(self, config, sectionName):
1690 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1691 except ConfigParser.NoOptionError:
1692 self._selectedComboIndex = 0
1694 def save_settings(self, config, sectionName):
1695 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1697 def _idly_populate_contactsview(self):
1698 with gtk_toolbox.gtk_lock():
1699 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1702 while addressBook is not self._addressBook:
1703 addressBook = self._addressBook
1704 with gtk_toolbox.gtk_lock():
1705 self._contactsview.set_model(None)
1709 contacts = addressBook.get_contacts()
1710 except Exception, e:
1712 self._isPopulated = False
1713 self._errorDisplay.push_exception_with_lock()
1714 for contactId, contactName in contacts:
1715 contactType = addressBook.contact_source_short_name(contactId)
1716 row = contactType, contactName, contactId
1717 self._contactsmodel.append(row)
1719 with gtk_toolbox.gtk_lock():
1720 self._contactsview.set_model(self._contactsmodel)
1722 self._isPopulated = True
1723 except Exception, e:
1724 self._errorDisplay.push_exception_with_lock()
1726 with gtk_toolbox.gtk_lock():
1727 hildonize.show_busy_banner_end(banner)
1730 def _on_addressbook_button_changed(self, *args, **kwds):
1733 newSelectedComboIndex = hildonize.touch_selector(
1736 (("%s" % m[2]) for m in self._booksList),
1737 self._selectedComboIndex,
1739 except RuntimeError:
1742 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1743 selectedBookId = self._booksList[newSelectedComboIndex][1]
1745 oldAddressbook = self._addressBook
1746 self.open_addressbook(selectedFactoryId, selectedBookId)
1747 forceUpdate = True if oldAddressbook is not self._addressBook else False
1748 self.update(force=forceUpdate)
1750 self._selectedComboIndex = newSelectedComboIndex
1751 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1752 except Exception, e:
1753 self._errorDisplay.push_exception()
1755 def _on_contactsview_row_activated(self, treeview, path, view_column):
1757 itr = self._contactsmodel.get_iter(path)
1761 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1762 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1764 contactDetails = self._addressBook.get_contact_details(contactId)
1765 except Exception, e:
1767 self._errorDisplay.push_exception()
1768 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1770 if len(contactPhoneNumbers) == 0:
1775 contactPhoneNumbers,
1776 messages = (contactName, ),
1778 self._contactsviewselection.unselect_all()
1779 except Exception, e:
1780 self._errorDisplay.push_exception()