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._window = self._widgetTree.get_widget("smsWindow")
254 self._window.connect("delete-event", self._on_delete)
255 self._window.connect("key-press-event", self._on_key_press)
256 self._window.connect("window-state-event", self._on_window_state_change)
257 self._isFullScreen = False
258 self._parent = parent
260 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
261 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
262 errorClose = self._widgetTree.get_widget("smsErrorClose")
263 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
265 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
266 self._smsButton.connect("clicked", self._on_send)
267 self._dialButton = self._widgetTree.get_widget("dialButton")
268 self._dialButton.connect("clicked", self._on_dial)
270 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
272 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
273 self._messagesView = self._widgetTree.get_widget("smsMessages")
275 textrenderer = gtk.CellRendererText()
276 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
277 textrenderer.set_property("wrap-width", 450)
278 messageColumn = gtk.TreeViewColumn("")
279 messageColumn.pack_start(textrenderer, expand=True)
280 messageColumn.add_attribute(textrenderer, "markup", 0)
281 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
282 self._messagesView.append_column(messageColumn)
283 self._messagesView.set_headers_visible(False)
284 self._messagesView.set_model(self._messagemodel)
285 self._messagesView.set_fixed_height_mode(False)
287 self._conversationView = self._messagesView.get_parent()
288 self._conversationViewPort = self._conversationView.get_parent()
289 self._scrollWindow = self._conversationViewPort.get_parent()
291 self._targetList = self._widgetTree.get_widget("smsTargetList")
292 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
293 self._phoneButton.connect("clicked", self._on_phone)
294 self._smsEntry = self._widgetTree.get_widget("smsEntry")
295 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
296 self._smsEntrySize = None
299 self._window = hildonize.hildonize_window(self._app, self._window)
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)
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)
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)
520 self._errorDisplay.push_exception()
522 def _on_delete(self, *args):
524 self._window.emit_stop_by_name("delete-event")
527 self._errorDisplay.push_exception()
530 def _on_window_state_change(self, widget, event, *args):
532 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
533 self._isFullScreen = True
535 self._isFullScreen = False
537 self._errorDisplay.push_exception()
539 def _on_key_press(self, widget, event):
540 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
543 event.keyval == gtk.keysyms.F6 or
544 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
546 if self._isFullScreen:
547 self._window.unfullscreen()
549 self._window.fullscreen()
550 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
553 for messagePart in self._messagemodel
555 self._clipboard.set_text(str(message))
557 event.keyval == gtk.keysyms.w and
558 event.get_state() & gtk.gdk.CONTROL_MASK
562 event.keyval == gtk.keysyms.q and
563 event.get_state() & gtk.gdk.CONTROL_MASK
565 self._parent.destroy()
567 self._errorDisplay.push_exception()
570 class Dialpad(object):
572 def __init__(self, widgetTree, errorDisplay):
573 self._clipboard = gtk.clipboard_get()
574 self._errorDisplay = errorDisplay
576 self._numberdisplay = widgetTree.get_widget("numberdisplay")
577 self._okButton = widgetTree.get_widget("dialpadOk")
578 self._backButton = widgetTree.get_widget("back")
579 self._plusButton = widgetTree.get_widget("plus")
580 self._phonenumber = ""
581 self._prettynumber = ""
584 "on_digit_clicked": self._on_digit_clicked,
586 widgetTree.signal_autoconnect(callbackMapping)
587 self._okButton.connect("clicked", self._on_ok_clicked)
588 self._plusButton.connect("clicked", self._on_plus)
590 self._originalLabel = self._backButton.get_label()
591 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
592 self._backTapHandler.on_tap = self._on_backspace
593 self._backTapHandler.on_hold = self._on_clearall
594 self._backTapHandler.on_holding = self._set_clear_button
595 self._backTapHandler.on_cancel = self._reset_back_button
597 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
598 self._keyPressEventId = 0
601 self._okButton.grab_focus()
602 self._backTapHandler.enable()
603 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
606 self._window.disconnect(self._keyPressEventId)
607 self._keyPressEventId = 0
608 self._reset_back_button()
609 self._backTapHandler.disable()
611 def add_contact(self, *args, **kwds):
613 @note Actual dial function is patched in later
615 raise NotImplementedError("Horrible unknown error has occurred")
617 def get_number(self):
618 return self._phonenumber
620 def set_number(self, number):
622 Set the number to dial
625 self._phonenumber = make_ugly(number)
626 self._prettynumber = make_pretty(self._phonenumber)
627 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
629 self._errorDisplay.push_exception()
638 def load_settings(self, config, section):
641 def save_settings(self, config, section):
643 @note Thread Agnostic
647 def _on_key_press(self, widget, event):
649 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
650 contents = self._clipboard.wait_for_text()
651 if contents is not None:
652 self.set_number(contents)
654 self._errorDisplay.push_exception()
656 def _on_ok_clicked(self, widget):
658 phoneNumber = self.get_number()
661 [("Dialer", phoneNumber)], ()
665 self._errorDisplay.push_exception()
667 def _on_digit_clicked(self, widget):
669 self.set_number(self._phonenumber + widget.get_name()[-1])
671 self._errorDisplay.push_exception()
673 def _on_plus(self, *args):
675 self.set_number(self._phonenumber + "+")
677 self._errorDisplay.push_exception()
679 def _on_backspace(self, taps):
681 self.set_number(self._phonenumber[:-taps])
682 self._reset_back_button()
684 self._errorDisplay.push_exception()
686 def _on_clearall(self, taps):
689 self._reset_back_button()
691 self._errorDisplay.push_exception()
694 def _set_clear_button(self):
696 self._backButton.set_label("gtk-clear")
698 self._errorDisplay.push_exception()
700 def _reset_back_button(self):
702 self._backButton.set_label(self._originalLabel)
704 self._errorDisplay.push_exception()
707 class AccountInfo(object):
709 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
710 self._errorDisplay = errorDisplay
711 self._backend = backend
712 self._isPopulated = False
713 self._alarmHandler = alarmHandler
714 self._notifyOnMissed = False
715 self._notifyOnVoicemail = False
716 self._notifyOnSms = False
718 self._callbackList = []
719 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
720 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
721 self._onCallbackSelectChangedId = 0
723 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
724 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
725 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
726 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
727 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
728 self._onNotifyToggled = 0
729 self._onMinutesChanged = 0
730 self._onMissedToggled = 0
731 self._onVoicemailToggled = 0
732 self._onSmsToggled = 0
733 self._applyAlarmTimeoutId = None
735 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
736 self._callbackNumber = ""
739 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
741 self._accountViewNumberDisplay.set_use_markup(True)
742 self.set_account_number("")
744 del self._callbackList[:]
745 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
746 self._set_callback_label("")
748 if self._alarmHandler is not None:
749 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
750 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
751 self._missedCheckbox.set_active(self._notifyOnMissed)
752 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
753 self._smsCheckbox.set_active(self._notifyOnSms)
755 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
756 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
757 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
758 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
759 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
761 self._notifyCheckbox.set_sensitive(False)
762 self._minutesEntryButton.set_sensitive(False)
763 self._missedCheckbox.set_sensitive(False)
764 self._voicemailCheckbox.set_sensitive(False)
765 self._smsCheckbox.set_sensitive(False)
767 self.update(force=True)
770 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
771 self._onCallbackSelectChangedId = 0
772 self._set_callback_label("")
774 if self._alarmHandler is not None:
775 self._notifyCheckbox.disconnect(self._onNotifyToggled)
776 self._minutesEntryButton.disconnect(self._onMinutesChanged)
777 self._missedCheckbox.disconnect(self._onNotifyToggled)
778 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
779 self._smsCheckbox.disconnect(self._onNotifyToggled)
780 self._onNotifyToggled = 0
781 self._onMinutesChanged = 0
782 self._onMissedToggled = 0
783 self._onVoicemailToggled = 0
784 self._onSmsToggled = 0
786 self._notifyCheckbox.set_sensitive(True)
787 self._minutesEntryButton.set_sensitive(True)
788 self._missedCheckbox.set_sensitive(True)
789 self._voicemailCheckbox.set_sensitive(True)
790 self._smsCheckbox.set_sensitive(True)
793 del self._callbackList[:]
795 def set_account_number(self, number):
797 Displays current account number
799 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
801 def update(self, force = False):
802 if not force and self._isPopulated:
804 self._populate_callback_combo()
805 self.set_account_number(self._backend.get_account_number())
809 self._set_callback_label("")
810 self.set_account_number("")
811 self._isPopulated = False
813 def save_everything(self):
814 raise NotImplementedError
818 return "Account Info"
820 def load_settings(self, config, section):
821 self._callbackNumber = make_ugly(config.get(section, "callback"))
822 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
823 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
824 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
826 def save_settings(self, config, section):
828 @note Thread Agnostic
830 config.set(section, "callback", self._callbackNumber)
831 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
832 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
833 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
835 def _populate_callback_combo(self):
836 self._isPopulated = True
837 del self._callbackList[:]
839 callbackNumbers = self._backend.get_callback_numbers()
841 self._errorDisplay.push_exception()
842 self._isPopulated = False
845 if len(callbackNumbers) == 0:
846 callbackNumbers = {"": "No callback numbers available"}
848 for number, description in callbackNumbers.iteritems():
849 self._callbackList.append((make_pretty(number), description))
851 self._set_callback_number(self._callbackNumber)
853 def _set_callback_number(self, number):
855 if not self._backend.is_valid_syntax(number) and 0 < len(number):
856 self._errorDisplay.push_message("%s is not a valid callback number" % number)
857 elif number == self._backend.get_callback_number() and 0 < len(number):
858 _moduleLogger.warning(
859 "Callback number already is %s" % (
860 self._backend.get_callback_number(),
863 self._set_callback_label(number)
865 if number.startswith("1747"): number = "+" + number
866 self._backend.set_callback_number(number)
867 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
868 make_pretty(number), make_pretty(self._backend.get_callback_number())
870 self._callbackNumber = make_ugly(number)
871 self._set_callback_label(number)
873 "Callback number set to %s" % (
874 self._backend.get_callback_number(),
878 self._errorDisplay.push_exception()
880 def _set_callback_label(self, uglyNumber):
881 prettyNumber = make_pretty(uglyNumber)
882 if len(prettyNumber) == 0:
883 prettyNumber = "No Callback Number"
884 self._callbackSelectButton.set_label(prettyNumber)
886 def _update_alarm_settings(self, recurrence):
888 isEnabled = self._notifyCheckbox.get_active()
889 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
890 self._alarmHandler.apply_settings(isEnabled, recurrence)
892 self.save_everything()
893 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
894 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
896 def _on_callbackentry_clicked(self, *args):
898 actualSelection = make_pretty(self._callbackNumber)
901 (number, "%s (%s)" % (number, description))
902 for (number, description) in self._callbackList
904 defaultSelection = userOptions.get(actualSelection, actualSelection)
906 userSelection = hildonize.touch_selector_entry(
909 list(userOptions.itervalues()),
912 reversedUserOptions = dict(
913 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
915 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
917 number = make_ugly(selectedNumber)
918 self._set_callback_number(number)
919 except RuntimeError, e:
920 _moduleLogger.exception("%s" % str(e))
922 self._errorDisplay.push_exception()
924 def _on_notify_toggled(self, *args):
926 if self._applyAlarmTimeoutId is not None:
927 gobject.source_remove(self._applyAlarmTimeoutId)
928 self._applyAlarmTimeoutId = None
929 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
931 self._errorDisplay.push_exception()
933 def _on_minutes_clicked(self, *args):
934 recurrenceChoices = [
950 actualSelection = self._alarmHandler.recurrence
952 closestSelectionIndex = 0
953 for i, possible in enumerate(recurrenceChoices):
954 if possible[0] <= actualSelection:
955 closestSelectionIndex = i
956 recurrenceIndex = hildonize.touch_selector(
959 (("%s" % m[1]) for m in recurrenceChoices),
960 closestSelectionIndex,
962 recurrence = recurrenceChoices[recurrenceIndex][0]
964 self._update_alarm_settings(recurrence)
965 except RuntimeError, e:
966 _moduleLogger.exception("%s" % str(e))
968 self._errorDisplay.push_exception()
970 def _on_apply_timeout(self, *args):
972 self._applyAlarmTimeoutId = None
974 self._update_alarm_settings(self._alarmHandler.recurrence)
976 self._errorDisplay.push_exception()
979 def _on_missed_toggled(self, *args):
981 self._notifyOnMissed = self._missedCheckbox.get_active()
982 self.save_everything()
984 self._errorDisplay.push_exception()
986 def _on_voicemail_toggled(self, *args):
988 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
989 self.save_everything()
991 self._errorDisplay.push_exception()
993 def _on_sms_toggled(self, *args):
995 self._notifyOnSms = self._smsCheckbox.get_active()
996 self.save_everything()
998 self._errorDisplay.push_exception()
1001 class CallHistoryView(object):
1009 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1011 def __init__(self, widgetTree, backend, errorDisplay):
1012 self._errorDisplay = errorDisplay
1013 self._backend = backend
1015 self._isPopulated = False
1016 self._historymodel = gtk.ListStore(
1017 gobject.TYPE_STRING, # number
1018 gobject.TYPE_STRING, # date
1019 gobject.TYPE_STRING, # action
1020 gobject.TYPE_STRING, # from
1021 gobject.TYPE_STRING, # from id
1023 self._historymodelfiltered = self._historymodel.filter_new()
1024 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1025 self._historyview = widgetTree.get_widget("historyview")
1026 self._historyviewselection = None
1027 self._onRecentviewRowActivatedId = 0
1029 textrenderer = gtk.CellRendererText()
1030 textrenderer.set_property("yalign", 0)
1031 self._dateColumn = gtk.TreeViewColumn("Date")
1032 self._dateColumn.pack_start(textrenderer, expand=True)
1033 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1035 textrenderer = gtk.CellRendererText()
1036 textrenderer.set_property("yalign", 0)
1037 self._actionColumn = gtk.TreeViewColumn("Action")
1038 self._actionColumn.pack_start(textrenderer, expand=True)
1039 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1041 textrenderer = gtk.CellRendererText()
1042 textrenderer.set_property("yalign", 0)
1043 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1044 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1045 self._numberColumn = gtk.TreeViewColumn("Number")
1046 self._numberColumn.pack_start(textrenderer, expand=True)
1047 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1049 textrenderer = gtk.CellRendererText()
1050 textrenderer.set_property("yalign", 0)
1051 hildonize.set_cell_thumb_selectable(textrenderer)
1052 self._nameColumn = gtk.TreeViewColumn("From")
1053 self._nameColumn.pack_start(textrenderer, expand=True)
1054 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1055 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1057 self._window = gtk_toolbox.find_parent_window(self._historyview)
1059 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1060 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1061 self._selectedFilter = "All"
1063 self._updateSink = gtk_toolbox.threaded_stage(
1065 self._idly_populate_historyview,
1066 gtk_toolbox.null_sink(),
1071 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1072 self._historyFilterSelector.set_label(self._selectedFilter)
1074 self._historyview.set_model(self._historymodelfiltered)
1075 self._historyview.set_fixed_height_mode(False)
1077 self._historyview.append_column(self._dateColumn)
1078 self._historyview.append_column(self._actionColumn)
1079 self._historyview.append_column(self._numberColumn)
1080 self._historyview.append_column(self._nameColumn)
1081 self._historyviewselection = self._historyview.get_selection()
1082 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1084 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1087 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1091 self._historyview.remove_column(self._dateColumn)
1092 self._historyview.remove_column(self._actionColumn)
1093 self._historyview.remove_column(self._nameColumn)
1094 self._historyview.remove_column(self._numberColumn)
1095 self._historyview.set_model(None)
1097 def add_contact(self, *args, **kwds):
1099 @note Actual dial function is patched in later
1101 raise NotImplementedError("Horrible unknown error has occurred")
1103 def update(self, force = False):
1104 if not force and self._isPopulated:
1106 self._updateSink.send(())
1110 self._isPopulated = False
1111 self._historymodel.clear()
1115 return "Recent Calls"
1117 def load_settings(self, config, sectionName):
1119 self._selectedFilter = config.get(sectionName, "filter")
1120 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1121 self._messageType = self.HISTORY_ITEM_TYPES[0]
1122 except ConfigParser.NoOptionError:
1125 def save_settings(self, config, sectionName):
1127 @note Thread Agnostic
1129 config.set(sectionName, "filter", self._selectedFilter)
1131 def _is_history_visible(self, model, iter):
1133 action = model.get_value(iter, self.ACTION_IDX)
1135 return False # this seems weird but oh well
1137 if self._selectedFilter in [action, "All"]:
1141 except Exception, e:
1142 self._errorDisplay.push_exception()
1144 def _idly_populate_historyview(self):
1145 with gtk_toolbox.gtk_lock():
1146 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1148 self._historymodel.clear()
1149 self._isPopulated = True
1152 historyItems = self._backend.get_recent()
1153 except Exception, e:
1154 self._errorDisplay.push_exception_with_lock()
1155 self._isPopulated = False
1159 gv_backend.decorate_recent(data)
1160 for data in gv_backend.sort_messages(historyItems)
1163 for contactId, personName, phoneNumber, date, action in historyItems:
1165 personName = "Unknown"
1166 date = abbrev_relative_date(date)
1167 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1168 prettyNumber = make_pretty(prettyNumber)
1169 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1170 with gtk_toolbox.gtk_lock():
1171 self._historymodel.append(item)
1172 except Exception, e:
1173 self._errorDisplay.push_exception_with_lock()
1175 with gtk_toolbox.gtk_lock():
1176 hildonize.show_busy_banner_end(banner)
1180 def _on_history_filter_clicked(self, *args, **kwds):
1182 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1185 newSelectedComboIndex = hildonize.touch_selector(
1188 self.HISTORY_ITEM_TYPES,
1191 except RuntimeError:
1194 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1195 self._selectedFilter = option
1196 self._historyFilterSelector.set_label(self._selectedFilter)
1197 self._historymodelfiltered.refilter()
1198 except Exception, e:
1199 self._errorDisplay.push_exception()
1201 def _history_summary(self, expectedNumber):
1202 for number, action, date, whoFrom, whoFromId in self._historymodel:
1203 if expectedNumber is not None and expectedNumber == number:
1204 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1206 def _on_historyview_row_activated(self, treeview, path, view_column):
1208 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1209 itr = self._historymodel.get_iter(childPath)
1213 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1214 number = make_ugly(prettyNumber)
1215 description = list(self._history_summary(prettyNumber))
1216 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1217 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1218 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1222 contactPhoneNumbers,
1223 messages = description,
1224 defaultIndex = defaultIndex,
1226 self._historyviewselection.unselect_all()
1227 except Exception, e:
1228 self._errorDisplay.push_exception()
1231 class MessagesView(object):
1239 MESSAGE_DATA_IDX = 6
1241 NO_MESSAGES = "None"
1242 VOICEMAIL_MESSAGES = "Voicemail"
1243 TEXT_MESSAGES = "SMS"
1244 ALL_TYPES = "All Messages"
1245 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1247 UNREAD_STATUS = "Unread"
1248 UNARCHIVED_STATUS = "Inbox"
1250 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1252 def __init__(self, widgetTree, backend, errorDisplay):
1253 self._errorDisplay = errorDisplay
1254 self._backend = backend
1256 self._isPopulated = False
1257 self._messagemodel = gtk.ListStore(
1258 gobject.TYPE_STRING, # number
1259 gobject.TYPE_STRING, # date
1260 gobject.TYPE_STRING, # header
1261 gobject.TYPE_STRING, # message
1263 gobject.TYPE_STRING, # from id
1264 object, # message data
1266 self._messagemodelfiltered = self._messagemodel.filter_new()
1267 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1268 self._messageview = widgetTree.get_widget("messages_view")
1269 self._messageviewselection = None
1270 self._onMessageviewRowActivatedId = 0
1272 self._messageRenderer = gtk.CellRendererText()
1273 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1274 self._messageRenderer.set_property("wrap-width", 500)
1275 self._messageColumn = gtk.TreeViewColumn("Messages")
1276 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1277 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1278 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1280 self._window = gtk_toolbox.find_parent_window(self._messageview)
1282 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1283 self._onMessageTypeClickedId = 0
1284 self._messageType = self.ALL_TYPES
1285 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1286 self._onMessageStatusClickedId = 0
1287 self._messageStatus = self.ALL_STATUS
1289 self._updateSink = gtk_toolbox.threaded_stage(
1291 self._idly_populate_messageview,
1292 gtk_toolbox.null_sink(),
1297 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1298 self._messageview.set_model(self._messagemodelfiltered)
1299 self._messageview.set_headers_visible(False)
1300 self._messageview.set_fixed_height_mode(False)
1302 self._messageview.append_column(self._messageColumn)
1303 self._messageviewselection = self._messageview.get_selection()
1304 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1306 self._messageTypeButton.set_label(self._messageType)
1307 self._messageStatusButton.set_label(self._messageStatus)
1309 self._onMessageviewRowActivatedId = self._messageview.connect(
1310 "row-activated", self._on_messageview_row_activated
1312 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1313 "clicked", self._on_message_type_clicked
1315 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1316 "clicked", self._on_message_status_clicked
1320 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1321 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1322 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1326 self._messageview.remove_column(self._messageColumn)
1327 self._messageview.set_model(None)
1329 def add_contact(self, *args, **kwds):
1331 @note Actual dial function is patched in later
1333 raise NotImplementedError("Horrible unknown error has occurred")
1335 def update(self, force = False):
1336 if not force and self._isPopulated:
1338 self._updateSink.send(())
1342 self._isPopulated = False
1343 self._messagemodel.clear()
1349 def load_settings(self, config, sectionName):
1351 self._messageType = config.get(sectionName, "type")
1352 if self._messageType not in self.MESSAGE_TYPES:
1353 self._messageType = self.ALL_TYPES
1354 self._messageStatus = config.get(sectionName, "status")
1355 if self._messageStatus not in self.MESSAGE_STATUSES:
1356 self._messageStatus = self.ALL_STATUS
1357 except ConfigParser.NoOptionError:
1360 def save_settings(self, config, sectionName):
1362 @note Thread Agnostic
1364 config.set(sectionName, "status", self._messageStatus)
1365 config.set(sectionName, "type", self._messageType)
1367 def _is_message_visible(self, model, iter):
1369 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1371 return False # this seems weird but oh well
1372 return self._filter_messages(message, self._messageType, self._messageStatus)
1373 except Exception, e:
1374 self._errorDisplay.push_exception()
1377 def _filter_messages(cls, message, type, status):
1378 if type == cls.ALL_TYPES:
1381 messageType = message["type"]
1382 isType = messageType == type
1384 if status == cls.ALL_STATUS:
1387 isUnarchived = not message["isArchived"]
1388 isUnread = not message["isRead"]
1389 if status == cls.UNREAD_STATUS:
1390 isStatus = isUnarchived and isUnread
1391 elif status == cls.UNARCHIVED_STATUS:
1392 isStatus = isUnarchived
1394 assert "Status %s is bad for %r" % (status, message)
1396 return isType and isStatus
1398 _MIN_MESSAGES_SHOWN = 4
1400 def _idly_populate_messageview(self):
1401 with gtk_toolbox.gtk_lock():
1402 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1404 self._messagemodel.clear()
1405 self._isPopulated = True
1407 if self._messageType == self.NO_MESSAGES:
1411 messageItems = self._backend.get_messages()
1412 except Exception, e:
1413 self._errorDisplay.push_exception_with_lock()
1414 self._isPopulated = False
1418 (gv_backend.decorate_message(message), message)
1419 for message in gv_backend.sort_messages(messageItems)
1422 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1423 prettyNumber = number[2:] if number.startswith("+1") else number
1424 prettyNumber = make_pretty(prettyNumber)
1426 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1427 expandedMessages = [firstMessage]
1428 expandedMessages.extend(messages)
1429 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1430 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1431 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1432 collapsedMessages = [firstMessage, secondMessage]
1433 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1435 collapsedMessages = expandedMessages
1436 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1438 number = make_ugly(number)
1440 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1441 with gtk_toolbox.gtk_lock():
1442 self._messagemodel.append(row)
1443 except Exception, e:
1444 self._errorDisplay.push_exception_with_lock()
1446 with gtk_toolbox.gtk_lock():
1447 hildonize.show_busy_banner_end(banner)
1448 self._messagemodelfiltered.refilter()
1452 def _on_messageview_row_activated(self, treeview, path, view_column):
1454 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1455 itr = self._messagemodel.get_iter(childPath)
1459 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1460 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1462 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1463 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1464 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1468 contactPhoneNumbers,
1469 messages = description,
1470 defaultIndex = defaultIndex,
1472 self._messageviewselection.unselect_all()
1473 except Exception, e:
1474 self._errorDisplay.push_exception()
1476 def _on_message_type_clicked(self, *args, **kwds):
1478 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1481 newSelectedIndex = hildonize.touch_selector(
1487 except RuntimeError:
1490 if selectedIndex != newSelectedIndex:
1491 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1492 self._messageTypeButton.set_label(self._messageType)
1493 self._messagemodelfiltered.refilter()
1494 except Exception, e:
1495 self._errorDisplay.push_exception()
1497 def _on_message_status_clicked(self, *args, **kwds):
1499 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1502 newSelectedIndex = hildonize.touch_selector(
1505 self.MESSAGE_STATUSES,
1508 except RuntimeError:
1511 if selectedIndex != newSelectedIndex:
1512 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1513 self._messageStatusButton.set_label(self._messageStatus)
1514 self._messagemodelfiltered.refilter()
1515 except Exception, e:
1516 self._errorDisplay.push_exception()
1519 class ContactsView(object):
1521 CONTACT_TYPE_IDX = 0
1522 CONTACT_NAME_IDX = 1
1525 def __init__(self, widgetTree, backend, errorDisplay):
1526 self._errorDisplay = errorDisplay
1527 self._backend = backend
1529 self._addressBook = None
1530 self._selectedComboIndex = 0
1531 self._addressBookFactories = [null_backend.NullAddressBook()]
1533 self._booksList = []
1534 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1536 self._isPopulated = False
1537 self._contactsmodel = gtk.ListStore(
1538 gobject.TYPE_STRING, # Contact Type
1539 gobject.TYPE_STRING, # Contact Name
1540 gobject.TYPE_STRING, # Contact ID
1542 self._contactsviewselection = None
1543 self._contactsview = widgetTree.get_widget("contactsview")
1545 self._contactColumn = gtk.TreeViewColumn("Contact")
1546 displayContactSource = False
1547 if displayContactSource:
1548 textrenderer = gtk.CellRendererText()
1549 self._contactColumn.pack_start(textrenderer, expand=False)
1550 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1551 textrenderer = gtk.CellRendererText()
1552 hildonize.set_cell_thumb_selectable(textrenderer)
1553 self._contactColumn.pack_start(textrenderer, expand=True)
1554 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1555 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1556 self._contactColumn.set_sort_column_id(1)
1557 self._contactColumn.set_visible(True)
1559 self._onContactsviewRowActivatedId = 0
1560 self._onAddressbookButtonChangedId = 0
1561 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1563 self._updateSink = gtk_toolbox.threaded_stage(
1565 self._idly_populate_contactsview,
1566 gtk_toolbox.null_sink(),
1571 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1573 self._contactsview.set_model(self._contactsmodel)
1574 self._contactsview.set_fixed_height_mode(False)
1575 self._contactsview.append_column(self._contactColumn)
1576 self._contactsviewselection = self._contactsview.get_selection()
1577 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1579 del self._booksList[:]
1580 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1581 if factoryName and bookName:
1582 entryName = "%s: %s" % (factoryName, bookName)
1584 entryName = factoryName
1586 entryName = bookName
1588 entryName = "Bad name (%d)" % factoryId
1589 row = (str(factoryId), bookId, entryName)
1590 self._booksList.append(row)
1592 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1593 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1595 if len(self._booksList) <= self._selectedComboIndex:
1596 self._selectedComboIndex = 0
1597 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1599 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1600 selectedBookId = self._booksList[self._selectedComboIndex][1]
1601 self.open_addressbook(selectedFactoryId, selectedBookId)
1604 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1605 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1609 self._bookSelectionButton.set_label("")
1610 self._contactsview.set_model(None)
1611 self._contactsview.remove_column(self._contactColumn)
1613 def add_contact(self, *args, **kwds):
1615 @note Actual dial function is patched in later
1617 raise NotImplementedError("Horrible unknown error has occurred")
1619 def get_addressbooks(self):
1621 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1623 for i, factory in enumerate(self._addressBookFactories):
1624 for bookFactory, bookId, bookName in factory.get_addressbooks():
1625 yield (str(i), bookId), (factory.factory_name(), bookName)
1627 def open_addressbook(self, bookFactoryId, bookId):
1628 bookFactoryIndex = int(bookFactoryId)
1629 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1630 self._addressBook = addressBook
1632 def update(self, force = False):
1633 if not force and self._isPopulated:
1635 self._updateSink.send(())
1639 self._isPopulated = False
1640 self._contactsmodel.clear()
1641 for factory in self._addressBookFactories:
1642 factory.clear_caches()
1643 self._addressBook.clear_caches()
1645 def append(self, book):
1646 self._addressBookFactories.append(book)
1648 def extend(self, books):
1649 self._addressBookFactories.extend(books)
1655 def load_settings(self, config, sectionName):
1657 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1658 except ConfigParser.NoOptionError:
1659 self._selectedComboIndex = 0
1661 def save_settings(self, config, sectionName):
1662 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1664 def _idly_populate_contactsview(self):
1665 with gtk_toolbox.gtk_lock():
1666 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1669 while addressBook is not self._addressBook:
1670 addressBook = self._addressBook
1671 with gtk_toolbox.gtk_lock():
1672 self._contactsview.set_model(None)
1676 contacts = addressBook.get_contacts()
1677 except Exception, e:
1679 self._isPopulated = False
1680 self._errorDisplay.push_exception_with_lock()
1681 for contactId, contactName in contacts:
1682 contactType = addressBook.contact_source_short_name(contactId)
1683 row = contactType, contactName, contactId
1684 self._contactsmodel.append(row)
1686 with gtk_toolbox.gtk_lock():
1687 self._contactsview.set_model(self._contactsmodel)
1689 self._isPopulated = True
1690 except Exception, e:
1691 self._errorDisplay.push_exception_with_lock()
1693 with gtk_toolbox.gtk_lock():
1694 hildonize.show_busy_banner_end(banner)
1697 def _on_addressbook_button_changed(self, *args, **kwds):
1700 newSelectedComboIndex = hildonize.touch_selector(
1703 (("%s" % m[2]) for m in self._booksList),
1704 self._selectedComboIndex,
1706 except RuntimeError:
1709 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1710 selectedBookId = self._booksList[newSelectedComboIndex][1]
1712 oldAddressbook = self._addressBook
1713 self.open_addressbook(selectedFactoryId, selectedBookId)
1714 forceUpdate = True if oldAddressbook is not self._addressBook else False
1715 self.update(force=forceUpdate)
1717 self._selectedComboIndex = newSelectedComboIndex
1718 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1719 except Exception, e:
1720 self._errorDisplay.push_exception()
1722 def _on_contactsview_row_activated(self, treeview, path, view_column):
1724 itr = self._contactsmodel.get_iter(path)
1728 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1729 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1731 contactDetails = self._addressBook.get_contact_details(contactId)
1732 except Exception, e:
1734 self._errorDisplay.push_exception()
1735 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1737 if len(contactPhoneNumbers) == 0:
1742 contactPhoneNumbers,
1743 messages = (contactName, ),
1745 self._contactsviewselection.unselect_all()
1746 except Exception, e:
1747 self._errorDisplay.push_exception()