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
22 @todo Alternate UI for dialogs (stackables)
25 from __future__ import with_statement
38 from backends import gv_backend
39 from backends import null_backend
42 _moduleLogger = logging.getLogger("gv_views")
45 def make_ugly(prettynumber):
47 function to take a phone number and strip out all non-numeric
50 >>> make_ugly("+012-(345)-678-90")
53 return normalize_number(prettynumber)
56 def normalize_number(prettynumber):
58 function to take a phone number and strip out all non-numeric
61 >>> normalize_number("+012-(345)-678-90")
63 >>> normalize_number("1-(345)-678-9000")
65 >>> normalize_number("+1-(345)-678-9000")
68 uglynumber = re.sub('[^0-9+]', '', prettynumber)
69 if uglynumber.startswith("+"):
71 elif uglynumber.startswith("1") and len(uglynumber) == 11:
72 uglynumber = "+"+uglynumber
73 elif len(uglynumber) == 10:
74 uglynumber = "+1"+uglynumber
78 #validateRe = re.compile("^\+?[0-9]{10,}$")
79 #assert validateRe.match(uglynumber) is not None
84 def _make_pretty_with_areacodde(phonenumber):
85 prettynumber = "(%s)" % (phonenumber[0:3], )
86 if 3 < len(phonenumber):
87 prettynumber += " %s" % (phonenumber[3:6], )
88 if 6 < len(phonenumber):
89 prettynumber += "-%s" % (phonenumber[6:], )
93 def _make_pretty_local(phonenumber):
94 prettynumber = "%s" % (phonenumber[0:3], )
95 if 3 < len(phonenumber):
96 prettynumber += "-%s" % (phonenumber[3:], )
100 def _make_pretty_international(phonenumber):
101 prettynumber = phonenumber
102 if phonenumber.startswith("0"):
103 prettynumber = "+%s " % (phonenumber[0:3], )
104 if 3 < len(phonenumber):
105 prettynumber += _make_pretty_with_areacodde(phonenumber[3:])
106 if phonenumber.startswith("1"):
108 prettynumber += _make_pretty_with_areacodde(phonenumber[1:])
112 def make_pretty(phonenumber):
114 Function to take a phone number and return the pretty version
116 if phonenumber begins with 0:
118 if phonenumber begins with 1: ( for gizmo callback numbers )
120 if phonenumber is 13 digits:
122 if phonenumber is 10 digits:
124 >>> make_pretty("12")
126 >>> make_pretty("1234567")
128 >>> make_pretty("2345678901")
130 >>> make_pretty("12345678901")
132 >>> make_pretty("01234567890")
134 >>> make_pretty("+01234567890")
136 >>> make_pretty("+12")
138 >>> make_pretty("+123")
140 >>> make_pretty("+1234")
143 if phonenumber is None or phonenumber is "":
146 phonenumber = normalize_number(phonenumber)
148 if phonenumber[0] == "+":
149 prettynumber = _make_pretty_international(phonenumber[1:])
150 if not prettynumber.startswith("+"):
151 prettynumber = "+"+prettynumber
152 elif 8 < len(phonenumber) and phonenumber[0] in ("0", "1"):
153 prettynumber = _make_pretty_international(phonenumber)
154 elif 7 < len(phonenumber):
155 prettynumber = _make_pretty_with_areacodde(phonenumber)
156 elif 3 < len(phonenumber):
157 prettynumber = _make_pretty_local(phonenumber)
159 prettynumber = phonenumber
160 return prettynumber.strip()
163 def abbrev_relative_date(date):
165 >>> abbrev_relative_date("42 hours ago")
167 >>> abbrev_relative_date("2 days ago")
169 >>> abbrev_relative_date("4 weeks ago")
172 parts = date.split(" ")
173 return "%s %s" % (parts[0], parts[1][0])
176 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
179 numLines = len(messageLines)
180 for line in messageLines[0:min(maxLines, numLines)]:
181 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
182 allowedLines = maxLines - lines
183 acceptedLines = min(allowedLines, linesPerLine)
184 acceptedChars = acceptedLines * maxCharsPerLine
186 if acceptedChars < (len(line) + 3):
189 acceptedChars = len(line) # eh, might as well complete the line
191 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
194 lines += acceptedLines
195 if maxLines <= lines:
199 def collapse_message(message, maxCharsPerLine, maxLines):
201 >>> collapse_message("Hello", 60, 2)
203 >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
204 'Hello world how are you doing today? 01234567890123456789012...'
205 >>> collapse_message('''Hello world how are you doing today?
206 ... 01234567890123456789
207 ... 01234567890123456789
208 ... 01234567890123456789
209 ... 01234567890123456789''', 60, 2)
210 'Hello world how are you doing today?\n01234567890123456789'
211 >>> collapse_message('''
212 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
213 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
214 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
215 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
216 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
217 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
218 '\nHello world how are you doing today? 01234567890123456789012...'
220 messageLines = message.split("\n")
221 return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
224 class SmsEntryDialog(object):
226 @todo Add multi-SMS messages like GoogleVoice
229 ACTION_CANCEL = "cancel"
231 ACTION_SEND_SMS = "sms"
235 def __init__(self, widgetTree):
236 self._clipboard = gtk.clipboard_get()
237 self._widgetTree = widgetTree
238 self._dialog = self._widgetTree.get_widget("smsDialog")
240 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
241 self._smsButton.connect("clicked", self._on_send)
242 self._dialButton = self._widgetTree.get_widget("dialButton")
243 self._dialButton.connect("clicked", self._on_dial)
244 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
245 self._cancelButton.connect("clicked", self._on_cancel)
247 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
249 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
250 self._messagesView = self._widgetTree.get_widget("smsMessages")
252 self._conversationView = self._messagesView.get_parent()
253 self._conversationViewPort = self._conversationView.get_parent()
254 self._scrollWindow = self._conversationViewPort.get_parent()
256 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
257 self._smsEntry = self._widgetTree.get_widget("smsEntry")
258 self._smsEntrySize = None
260 self._action = self.ACTION_CANCEL
262 self._numberIndex = -1
263 self._contactDetails = []
265 def run(self, contactDetails, messages = (), parent = None, defaultIndex = -1):
266 entryConnectId = self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
267 phoneConnectId = self._phoneButton.connect("clicked", self._on_phone)
268 keyConnectId = self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
270 # Setup the phone selection button
271 del self._contactDetails[:]
272 for phoneType, phoneNumber in contactDetails:
273 display = " - ".join((make_pretty(phoneNumber), phoneType))
274 row = (phoneNumber, display)
275 self._contactDetails.append(row)
276 if 0 < len(self._contactDetails):
277 self._numberIndex = defaultIndex if defaultIndex != -1 else 0
278 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
280 self._numberIndex = -1
281 self._phoneButton.set_label("Error: No Number Available")
283 # Add the column to the messages tree view
284 self._messagemodel.clear()
285 self._messagesView.set_model(self._messagemodel)
286 self._messagesView.set_fixed_height_mode(False)
288 textrenderer = gtk.CellRendererText()
289 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
290 textrenderer.set_property("wrap-width", 450)
291 messageColumn = gtk.TreeViewColumn("")
292 messageColumn.pack_start(textrenderer, expand=True)
293 messageColumn.add_attribute(textrenderer, "markup", 0)
294 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
295 self._messagesView.append_column(messageColumn)
296 self._messagesView.set_headers_visible(False)
299 for message in messages:
301 self._messagemodel.append(row)
302 self._messagesView.show()
303 self._scrollWindow.show()
304 messagesSelection = self._messagesView.get_selection()
305 messagesSelection.select_path((len(messages)-1, ))
307 self._messagesView.hide()
308 self._scrollWindow.hide()
310 self._smsEntry.get_buffer().set_text("")
311 self._update_letter_count()
313 if parent is not None:
314 self._dialog.set_transient_for(parent)
315 parentSize = parent.get_size()
316 self._dialog.resize(parentSize[0], max(parentSize[1]-10, 100))
320 self._dialog.show_all()
321 self._smsEntry.grab_focus()
322 adjustment = self._scrollWindow.get_vadjustment()
323 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
325 adjustment.value = dx
327 if 1 < len(self._contactDetails):
328 if defaultIndex == -1:
329 self._request_number()
330 self._phoneButton.set_sensitive(True)
332 self._phoneButton.set_sensitive(False)
334 userResponse = self._dialog.run()
336 self._dialog.hide_all()
338 # Process the users response
339 if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
340 phoneNumber = self._contactDetails[self._numberIndex][0]
341 phoneNumbers = [make_ugly(phoneNumber)]
345 self._action = self.ACTION_CANCEL
346 if self._action == self.ACTION_SEND_SMS:
347 entryBuffer = self._smsEntry.get_buffer()
348 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
349 enteredMessage = enteredMessage.strip()
350 if not enteredMessage:
352 self._action = self.ACTION_CANCEL
356 self._messagesView.remove_column(messageColumn)
357 self._messagesView.set_model(None)
359 return self._action, phoneNumbers, enteredMessage
361 self._smsEntry.get_buffer().disconnect(entryConnectId)
362 self._phoneButton.disconnect(phoneConnectId)
363 self._keyPressEventId = self._dialog.disconnect(keyConnectId)
365 def _update_letter_count(self, *args):
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 charsLeft = self.MAX_CHAR - entryLength
373 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
375 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
377 self._letterCountLabel.set_text("%s" % (numCharInText, ))
380 self._dialButton.set_sensitive(True)
381 self._smsButton.set_sensitive(False)
383 self._dialButton.set_sensitive(False)
384 self._smsButton.set_sensitive(True)
386 def _request_number(self):
388 assert 0 <= self._numberIndex, "%r" % self._numberIndex
390 self._numberIndex = hildonize.touch_selector(
393 (description for (number, description) in self._contactDetails),
396 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
398 _moduleLogger.exception("%s" % str(e))
400 def _on_phone(self, *args):
401 self._request_number()
403 def _on_entry_changed(self, *args):
404 self._update_letter_count()
406 def _on_send(self, *args):
407 self._dialog.response(gtk.RESPONSE_OK)
408 self._action = self.ACTION_SEND_SMS
410 def _on_dial(self, *args):
411 self._dialog.response(gtk.RESPONSE_OK)
412 self._action = self.ACTION_DIAL
414 def _on_cancel(self, *args):
415 self._dialog.response(gtk.RESPONSE_CANCEL)
416 self._action = self.ACTION_CANCEL
418 def _on_key_press(self, widget, event):
420 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
423 for messagePart in self._messagemodel
425 self._clipboard.set_text(str(message))
427 _moduleLogger.exception(str(e))
430 class Dialpad(object):
432 def __init__(self, widgetTree, errorDisplay):
433 self._clipboard = gtk.clipboard_get()
434 self._errorDisplay = errorDisplay
435 self._smsDialog = SmsEntryDialog(widgetTree)
437 self._numberdisplay = widgetTree.get_widget("numberdisplay")
438 self._smsButton = widgetTree.get_widget("sms")
439 self._dialButton = widgetTree.get_widget("dial")
440 self._backButton = widgetTree.get_widget("back")
441 self._zeroOrPlusButton = widgetTree.get_widget("digit0")
442 self._phonenumber = ""
443 self._prettynumber = ""
446 "on_digit_clicked": self._on_digit_clicked,
448 widgetTree.signal_autoconnect(callbackMapping)
449 self._dialButton.connect("clicked", self._on_dial_clicked)
450 self._smsButton.connect("clicked", self._on_sms_clicked)
452 self._originalLabel = self._backButton.get_label()
453 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
454 self._backTapHandler.on_tap = self._on_backspace
455 self._backTapHandler.on_hold = self._on_clearall
456 self._backTapHandler.on_holding = self._set_clear_button
457 self._backTapHandler.on_cancel = self._reset_back_button
458 self._zeroOrPlusTapHandler = gtk_toolbox.TapOrHold(self._zeroOrPlusButton)
459 self._zeroOrPlusTapHandler.on_tap = self._on_zero
460 self._zeroOrPlusTapHandler.on_hold = self._on_plus
462 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
463 self._keyPressEventId = 0
466 self._dialButton.grab_focus()
467 self._backTapHandler.enable()
468 self._zeroOrPlusTapHandler.enable()
469 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
472 self._window.disconnect(self._keyPressEventId)
473 self._keyPressEventId = 0
474 self._reset_back_button()
475 self._backTapHandler.disable()
476 self._zeroOrPlusTapHandler.disable()
478 def number_selected(self, action, numbers, message):
480 @note Actual dial function is patched in later
482 raise NotImplementedError("Horrible unknown error has occurred")
484 def get_number(self):
485 return self._phonenumber
487 def set_number(self, number):
489 Set the number to dial
492 self._phonenumber = make_ugly(number)
493 self._prettynumber = make_pretty(self._phonenumber)
494 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
496 self._errorDisplay.push_exception()
505 def load_settings(self, config, section):
508 def save_settings(self, config, section):
510 @note Thread Agnostic
514 def _on_key_press(self, widget, event):
516 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
517 contents = self._clipboard.wait_for_text()
518 if contents is not None:
519 self.set_number(contents)
521 self._errorDisplay.push_exception()
523 def _on_sms_clicked(self, widget):
525 phoneNumber = self.get_number()
526 action, phoneNumbers, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
528 if action == SmsEntryDialog.ACTION_CANCEL:
530 self.number_selected(action, phoneNumbers, message)
532 self._errorDisplay.push_exception()
534 def _on_dial_clicked(self, widget):
536 action = SmsEntryDialog.ACTION_DIAL
537 phoneNumbers = [self.get_number()]
539 self.number_selected(action, phoneNumbers, message)
541 self._errorDisplay.push_exception()
543 def _on_digit_clicked(self, widget):
545 self.set_number(self._phonenumber + widget.get_name()[-1])
547 self._errorDisplay.push_exception()
549 def _on_zero(self, *args):
551 self.set_number(self._phonenumber + "0")
553 self._errorDisplay.push_exception()
555 def _on_plus(self, *args):
557 self.set_number(self._phonenumber + "+")
559 self._errorDisplay.push_exception()
561 def _on_backspace(self, taps):
563 self.set_number(self._phonenumber[:-taps])
564 self._reset_back_button()
566 self._errorDisplay.push_exception()
568 def _on_clearall(self, taps):
571 self._reset_back_button()
573 self._errorDisplay.push_exception()
576 def _set_clear_button(self):
578 self._backButton.set_label("gtk-clear")
580 self._errorDisplay.push_exception()
582 def _reset_back_button(self):
584 self._backButton.set_label(self._originalLabel)
586 self._errorDisplay.push_exception()
589 class AccountInfo(object):
591 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
592 self._errorDisplay = errorDisplay
593 self._backend = backend
594 self._isPopulated = False
595 self._alarmHandler = alarmHandler
596 self._notifyOnMissed = False
597 self._notifyOnVoicemail = False
598 self._notifyOnSms = False
600 self._callbackList = []
601 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
602 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
603 self._onCallbackSelectChangedId = 0
605 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
606 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
607 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
608 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
609 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
610 self._onNotifyToggled = 0
611 self._onMinutesChanged = 0
612 self._onMissedToggled = 0
613 self._onVoicemailToggled = 0
614 self._onSmsToggled = 0
615 self._applyAlarmTimeoutId = None
617 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
618 self._callbackNumber = ""
621 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
623 self._accountViewNumberDisplay.set_use_markup(True)
624 self.set_account_number("")
626 del self._callbackList[:]
627 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
628 self._set_callback_label("")
630 if self._alarmHandler is not None:
631 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
632 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
633 self._missedCheckbox.set_active(self._notifyOnMissed)
634 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
635 self._smsCheckbox.set_active(self._notifyOnSms)
637 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
638 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
639 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
640 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
641 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
643 self._notifyCheckbox.set_sensitive(False)
644 self._minutesEntryButton.set_sensitive(False)
645 self._missedCheckbox.set_sensitive(False)
646 self._voicemailCheckbox.set_sensitive(False)
647 self._smsCheckbox.set_sensitive(False)
649 self.update(force=True)
652 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
653 self._onCallbackSelectChangedId = 0
654 self._set_callback_label("")
656 if self._alarmHandler is not None:
657 self._notifyCheckbox.disconnect(self._onNotifyToggled)
658 self._minutesEntryButton.disconnect(self._onMinutesChanged)
659 self._missedCheckbox.disconnect(self._onNotifyToggled)
660 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
661 self._smsCheckbox.disconnect(self._onNotifyToggled)
662 self._onNotifyToggled = 0
663 self._onMinutesChanged = 0
664 self._onMissedToggled = 0
665 self._onVoicemailToggled = 0
666 self._onSmsToggled = 0
668 self._notifyCheckbox.set_sensitive(True)
669 self._minutesEntryButton.set_sensitive(True)
670 self._missedCheckbox.set_sensitive(True)
671 self._voicemailCheckbox.set_sensitive(True)
672 self._smsCheckbox.set_sensitive(True)
675 del self._callbackList[:]
677 def set_account_number(self, number):
679 Displays current account number
681 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
683 def update(self, force = False):
684 if not force and self._isPopulated:
686 self._populate_callback_combo()
687 self.set_account_number(self._backend.get_account_number())
691 self._set_callback_label("")
692 self.set_account_number("")
693 self._isPopulated = False
695 def save_everything(self):
696 raise NotImplementedError
700 return "Account Info"
702 def load_settings(self, config, section):
703 self._callbackNumber = make_ugly(config.get(section, "callback"))
704 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
705 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
706 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
708 def save_settings(self, config, section):
710 @note Thread Agnostic
712 config.set(section, "callback", self._callbackNumber)
713 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
714 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
715 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
717 def _populate_callback_combo(self):
718 self._isPopulated = True
719 del self._callbackList[:]
721 callbackNumbers = self._backend.get_callback_numbers()
723 self._errorDisplay.push_exception()
724 self._isPopulated = False
727 if len(callbackNumbers) == 0:
728 callbackNumbers = {"": "No callback numbers available"}
730 for number, description in callbackNumbers.iteritems():
731 self._callbackList.append((make_pretty(number), description))
733 self._set_callback_number(self._callbackNumber)
735 def _set_callback_number(self, number):
737 if not self._backend.is_valid_syntax(number) and 0 < len(number):
738 self._errorDisplay.push_message("%s is not a valid callback number" % number)
739 elif number == self._backend.get_callback_number() and 0 < len(number):
740 _moduleLogger.warning(
741 "Callback number already is %s" % (
742 self._backend.get_callback_number(),
745 self._set_callback_label(number)
747 if number.startswith("1747"): number = "+" + number
748 self._backend.set_callback_number(number)
749 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
750 make_pretty(number), make_pretty(self._backend.get_callback_number())
752 self._callbackNumber = make_ugly(number)
753 self._set_callback_label(number)
755 "Callback number set to %s" % (
756 self._backend.get_callback_number(),
760 self._errorDisplay.push_exception()
762 def _set_callback_label(self, uglyNumber):
763 prettyNumber = make_pretty(uglyNumber)
764 if len(prettyNumber) == 0:
765 prettyNumber = "No Callback Number"
766 self._callbackSelectButton.set_label(prettyNumber)
768 def _update_alarm_settings(self, recurrence):
770 isEnabled = self._notifyCheckbox.get_active()
771 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
772 self._alarmHandler.apply_settings(isEnabled, recurrence)
774 self.save_everything()
775 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
776 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
778 def _on_callbackentry_clicked(self, *args):
780 actualSelection = make_pretty(self._callbackNumber)
783 (number, "%s (%s)" % (number, description))
784 for (number, description) in self._callbackList
786 defaultSelection = userOptions.get(actualSelection, actualSelection)
788 userSelection = hildonize.touch_selector_entry(
791 list(userOptions.itervalues()),
794 reversedUserOptions = dict(
795 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
797 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
799 number = make_ugly(selectedNumber)
800 self._set_callback_number(number)
801 except RuntimeError, e:
802 _moduleLogger.exception("%s" % str(e))
804 self._errorDisplay.push_exception()
806 def _on_notify_toggled(self, *args):
808 if self._applyAlarmTimeoutId is not None:
809 gobject.source_remove(self._applyAlarmTimeoutId)
810 self._applyAlarmTimeoutId = None
811 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
813 self._errorDisplay.push_exception()
815 def _on_minutes_clicked(self, *args):
816 recurrenceChoices = [
832 actualSelection = self._alarmHandler.recurrence
834 closestSelectionIndex = 0
835 for i, possible in enumerate(recurrenceChoices):
836 if possible[0] <= actualSelection:
837 closestSelectionIndex = i
838 recurrenceIndex = hildonize.touch_selector(
841 (("%s" % m[1]) for m in recurrenceChoices),
842 closestSelectionIndex,
844 recurrence = recurrenceChoices[recurrenceIndex][0]
846 self._update_alarm_settings(recurrence)
847 except RuntimeError, e:
848 _moduleLogger.exception("%s" % str(e))
850 self._errorDisplay.push_exception()
852 def _on_apply_timeout(self, *args):
854 self._applyAlarmTimeoutId = None
856 self._update_alarm_settings(self._alarmHandler.recurrence)
858 self._errorDisplay.push_exception()
861 def _on_missed_toggled(self, *args):
863 self._notifyOnMissed = self._missedCheckbox.get_active()
864 self.save_everything()
866 self._errorDisplay.push_exception()
868 def _on_voicemail_toggled(self, *args):
870 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
871 self.save_everything()
873 self._errorDisplay.push_exception()
875 def _on_sms_toggled(self, *args):
877 self._notifyOnSms = self._smsCheckbox.get_active()
878 self.save_everything()
880 self._errorDisplay.push_exception()
883 class CallHistoryView(object):
891 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
893 def __init__(self, widgetTree, backend, errorDisplay):
894 self._errorDisplay = errorDisplay
895 self._backend = backend
897 self._isPopulated = False
898 self._historymodel = gtk.ListStore(
899 gobject.TYPE_STRING, # number
900 gobject.TYPE_STRING, # date
901 gobject.TYPE_STRING, # action
902 gobject.TYPE_STRING, # from
903 gobject.TYPE_STRING, # from id
905 self._historymodelfiltered = self._historymodel.filter_new()
906 self._historymodelfiltered.set_visible_func(self._is_history_visible)
907 self._historyview = widgetTree.get_widget("historyview")
908 self._historyviewselection = None
909 self._onRecentviewRowActivatedId = 0
911 textrenderer = gtk.CellRendererText()
912 textrenderer.set_property("yalign", 0)
913 self._dateColumn = gtk.TreeViewColumn("Date")
914 self._dateColumn.pack_start(textrenderer, expand=True)
915 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
917 textrenderer = gtk.CellRendererText()
918 textrenderer.set_property("yalign", 0)
919 self._actionColumn = gtk.TreeViewColumn("Action")
920 self._actionColumn.pack_start(textrenderer, expand=True)
921 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
923 textrenderer = gtk.CellRendererText()
924 textrenderer.set_property("yalign", 0)
925 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
926 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
927 self._numberColumn = gtk.TreeViewColumn("Number")
928 self._numberColumn.pack_start(textrenderer, expand=True)
929 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
931 textrenderer = gtk.CellRendererText()
932 textrenderer.set_property("yalign", 0)
933 hildonize.set_cell_thumb_selectable(textrenderer)
934 self._nameColumn = gtk.TreeViewColumn("From")
935 self._nameColumn.pack_start(textrenderer, expand=True)
936 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
937 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
939 self._window = gtk_toolbox.find_parent_window(self._historyview)
940 self._smsDialog = SmsEntryDialog(widgetTree)
942 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
943 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
944 self._selectedFilter = "All"
946 self._updateSink = gtk_toolbox.threaded_stage(
948 self._idly_populate_historyview,
949 gtk_toolbox.null_sink(),
954 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
955 self._historyFilterSelector.set_label(self._selectedFilter)
957 self._historyview.set_model(self._historymodelfiltered)
958 self._historyview.set_fixed_height_mode(False)
960 self._historyview.append_column(self._dateColumn)
961 self._historyview.append_column(self._actionColumn)
962 self._historyview.append_column(self._numberColumn)
963 self._historyview.append_column(self._nameColumn)
964 self._historyviewselection = self._historyview.get_selection()
965 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
967 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
970 self._historyview.disconnect(self._onRecentviewRowActivatedId)
974 self._historyview.remove_column(self._dateColumn)
975 self._historyview.remove_column(self._actionColumn)
976 self._historyview.remove_column(self._nameColumn)
977 self._historyview.remove_column(self._numberColumn)
978 self._historyview.set_model(None)
980 def number_selected(self, action, numbers, message):
982 @note Actual dial function is patched in later
984 raise NotImplementedError("Horrible unknown error has occurred")
986 def update(self, force = False):
987 if not force and self._isPopulated:
989 self._updateSink.send(())
993 self._isPopulated = False
994 self._historymodel.clear()
998 return "Recent Calls"
1000 def load_settings(self, config, sectionName):
1002 self._selectedFilter = config.get(sectionName, "filter")
1003 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1004 self._messageType = self.HISTORY_ITEM_TYPES[0]
1005 except ConfigParser.NoOptionError:
1008 def save_settings(self, config, sectionName):
1010 @note Thread Agnostic
1012 config.set(sectionName, "filter", self._selectedFilter)
1014 def _is_history_visible(self, model, iter):
1016 action = model.get_value(iter, self.ACTION_IDX)
1018 return False # this seems weird but oh well
1020 if self._selectedFilter in [action, "All"]:
1024 except Exception, e:
1025 self._errorDisplay.push_exception()
1027 def _idly_populate_historyview(self):
1028 with gtk_toolbox.gtk_lock():
1029 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1031 self._historymodel.clear()
1032 self._isPopulated = True
1035 historyItems = self._backend.get_recent()
1036 except Exception, e:
1037 self._errorDisplay.push_exception_with_lock()
1038 self._isPopulated = False
1042 gv_backend.decorate_recent(data)
1043 for data in gv_backend.sort_messages(historyItems)
1046 for contactId, personName, phoneNumber, date, action in historyItems:
1048 personName = "Unknown"
1049 date = abbrev_relative_date(date)
1050 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1051 prettyNumber = make_pretty(prettyNumber)
1052 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1053 with gtk_toolbox.gtk_lock():
1054 self._historymodel.append(item)
1055 except Exception, e:
1056 self._errorDisplay.push_exception_with_lock()
1058 with gtk_toolbox.gtk_lock():
1059 hildonize.show_busy_banner_end(banner)
1063 def _on_history_filter_clicked(self, *args, **kwds):
1065 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1068 newSelectedComboIndex = hildonize.touch_selector(
1071 self.HISTORY_ITEM_TYPES,
1074 except RuntimeError:
1077 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1078 self._selectedFilter = option
1079 self._historyFilterSelector.set_label(self._selectedFilter)
1080 self._historymodelfiltered.refilter()
1081 except Exception, e:
1082 self._errorDisplay.push_exception()
1084 def _on_historyview_row_activated(self, treeview, path, view_column):
1086 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1087 itr = self._historymodel.get_iter(childPath)
1091 number = self._historymodel.get_value(itr, self.NUMBER_IDX)
1092 number = make_ugly(number)
1093 description = self._historymodel.get_value(itr, self.FROM_IDX)
1094 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1096 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1098 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1099 for (numberDescription, contactNumber) in contactPhoneNumbers
1102 defaultIndex = defaultMatches.index(True)
1104 contactPhoneNumbers.append(("Other", number))
1105 defaultIndex = len(contactPhoneNumbers)-1
1107 "Could not find contact %r's number %s among %r" % (
1108 contactId, number, contactPhoneNumbers
1112 contactPhoneNumbers = [("Phone", number)]
1115 action, phoneNumbers, message = self._smsDialog.run(
1116 contactPhoneNumbers,
1117 messages = (description, ),
1118 parent = self._window,
1119 defaultIndex = defaultIndex,
1121 if action == SmsEntryDialog.ACTION_CANCEL:
1123 assert phoneNumbers, "A lack of phone number exists"
1125 self.number_selected(action, phoneNumbers, message)
1126 self._historyviewselection.unselect_all()
1127 except Exception, e:
1128 self._errorDisplay.push_exception()
1131 class MessagesView(object):
1139 MESSAGE_DATA_IDX = 6
1141 NO_MESSAGES = "None"
1142 VOICEMAIL_MESSAGES = "Voicemail"
1143 TEXT_MESSAGES = "Texts"
1144 ALL_TYPES = "All Messages"
1145 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1147 UNREAD_STATUS = "Unread"
1148 UNARCHIVED_STATUS = "Inbox"
1150 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1152 def __init__(self, widgetTree, backend, errorDisplay):
1153 self._errorDisplay = errorDisplay
1154 self._backend = backend
1156 self._isPopulated = False
1157 self._messagemodel = gtk.ListStore(
1158 gobject.TYPE_STRING, # number
1159 gobject.TYPE_STRING, # date
1160 gobject.TYPE_STRING, # header
1161 gobject.TYPE_STRING, # message
1163 gobject.TYPE_STRING, # from id
1164 object, # message data
1166 self._messagemodelfiltered = self._messagemodel.filter_new()
1167 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1168 self._messageview = widgetTree.get_widget("messages_view")
1169 self._messageviewselection = None
1170 self._onMessageviewRowActivatedId = 0
1172 self._messageRenderer = gtk.CellRendererText()
1173 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1174 self._messageRenderer.set_property("wrap-width", 500)
1175 self._messageColumn = gtk.TreeViewColumn("Messages")
1176 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1177 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1178 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1180 self._window = gtk_toolbox.find_parent_window(self._messageview)
1181 self._smsDialog = SmsEntryDialog(widgetTree)
1183 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1184 self._onMessageTypeClickedId = 0
1185 self._messageType = self.ALL_TYPES
1186 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1187 self._onMessageStatusClickedId = 0
1188 self._messageStatus = self.ALL_STATUS
1190 self._updateSink = gtk_toolbox.threaded_stage(
1192 self._idly_populate_messageview,
1193 gtk_toolbox.null_sink(),
1198 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1199 self._messageview.set_model(self._messagemodelfiltered)
1200 self._messageview.set_headers_visible(False)
1201 self._messageview.set_fixed_height_mode(False)
1203 self._messageview.append_column(self._messageColumn)
1204 self._messageviewselection = self._messageview.get_selection()
1205 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1207 self._messageTypeButton.set_label(self._messageType)
1208 self._messageStatusButton.set_label(self._messageStatus)
1210 self._onMessageviewRowActivatedId = self._messageview.connect(
1211 "row-activated", self._on_messageview_row_activated
1213 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1214 "clicked", self._on_message_type_clicked
1216 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1217 "clicked", self._on_message_status_clicked
1221 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1222 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1223 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1227 self._messageview.remove_column(self._messageColumn)
1228 self._messageview.set_model(None)
1230 def number_selected(self, action, numbers, message):
1232 @note Actual dial function is patched in later
1234 raise NotImplementedError("Horrible unknown error has occurred")
1236 def update(self, force = False):
1237 if not force and self._isPopulated:
1239 self._updateSink.send(())
1243 self._isPopulated = False
1244 self._messagemodel.clear()
1250 def load_settings(self, config, sectionName):
1252 self._messageType = config.get(sectionName, "type")
1253 if self._messageType not in self.MESSAGE_TYPES:
1254 self._messageType = self.ALL_TYPES
1255 self._messageStatus = config.get(sectionName, "status")
1256 if self._messageStatus not in self.MESSAGE_STATUSES:
1257 self._messageStatus = self.ALL_STATUS
1258 except ConfigParser.NoOptionError:
1261 def save_settings(self, config, sectionName):
1263 @note Thread Agnostic
1265 config.set(sectionName, "status", self._messageStatus)
1266 config.set(sectionName, "type", self._messageType)
1268 def _is_message_visible(self, model, iter):
1270 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1272 return False # this seems weird but oh well
1273 return self._filter_messages(message, self._messageType, self._messageStatus)
1274 except Exception, e:
1275 self._errorDisplay.push_exception()
1278 def _filter_messages(cls, message, type, status):
1279 if type == cls.ALL_TYPES:
1282 messageType = message["type"]
1283 isType = messageType == type
1285 if status == cls.ALL_STATUS:
1288 isUnarchived = not message["isArchived"]
1289 isUnread = not message["isRead"]
1290 if status == cls.UNREAD_STATUS:
1291 isStatus = isUnarchived and isUnread
1292 elif status == cls.UNARCHIVED_STATUS:
1293 isStatus = isUnarchived
1295 assert "Status %s is bad for %r" % (status, message)
1297 return isType and isStatus
1299 _MIN_MESSAGES_SHOWN = 4
1301 def _idly_populate_messageview(self):
1302 with gtk_toolbox.gtk_lock():
1303 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1305 self._messagemodel.clear()
1306 self._isPopulated = True
1308 if self._messageType == self.NO_MESSAGES:
1312 messageItems = self._backend.get_messages()
1313 except Exception, e:
1314 self._errorDisplay.push_exception_with_lock()
1315 self._isPopulated = False
1319 (gv_backend.decorate_message(message), message)
1320 for message in gv_backend.sort_messages(messageItems)
1323 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1324 prettyNumber = number[2:] if number.startswith("+1") else number
1325 prettyNumber = make_pretty(prettyNumber)
1327 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1328 expandedMessages = [firstMessage]
1329 expandedMessages.extend(messages)
1330 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1331 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1332 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1333 collapsedMessages = [firstMessage, secondMessage]
1334 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1336 collapsedMessages = expandedMessages
1337 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1339 number = make_ugly(number)
1341 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1342 with gtk_toolbox.gtk_lock():
1343 self._messagemodel.append(row)
1344 except Exception, e:
1345 self._errorDisplay.push_exception_with_lock()
1347 with gtk_toolbox.gtk_lock():
1348 hildonize.show_busy_banner_end(banner)
1349 self._messagemodelfiltered.refilter()
1353 def _on_messageview_row_activated(self, treeview, path, view_column):
1355 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1356 itr = self._messagemodel.get_iter(childPath)
1360 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1361 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1363 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1365 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1367 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1368 for (numberDescription, contactNumber) in contactPhoneNumbers
1371 defaultIndex = defaultMatches.index(True)
1373 contactPhoneNumbers.append(("Other", number))
1374 defaultIndex = len(contactPhoneNumbers)-1
1376 "Could not find contact %r's number %s among %r" % (
1377 contactId, number, contactPhoneNumbers
1381 contactPhoneNumbers = [("Phone", number)]
1384 action, phoneNumbers, message = self._smsDialog.run(
1385 contactPhoneNumbers,
1386 messages = description,
1387 parent = self._window,
1388 defaultIndex = defaultIndex,
1390 if action == SmsEntryDialog.ACTION_CANCEL:
1392 assert phoneNumbers, "A lock of phone number exists"
1394 self.number_selected(action, phoneNumbers, message)
1395 self._messageviewselection.unselect_all()
1396 except Exception, e:
1397 self._errorDisplay.push_exception()
1399 def _on_message_type_clicked(self, *args, **kwds):
1401 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1404 newSelectedIndex = hildonize.touch_selector(
1410 except RuntimeError:
1413 if selectedIndex != newSelectedIndex:
1414 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1415 self._messageTypeButton.set_label(self._messageType)
1416 self._messagemodelfiltered.refilter()
1417 except Exception, e:
1418 self._errorDisplay.push_exception()
1420 def _on_message_status_clicked(self, *args, **kwds):
1422 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1425 newSelectedIndex = hildonize.touch_selector(
1428 self.MESSAGE_STATUSES,
1431 except RuntimeError:
1434 if selectedIndex != newSelectedIndex:
1435 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1436 self._messageStatusButton.set_label(self._messageStatus)
1437 self._messagemodelfiltered.refilter()
1438 except Exception, e:
1439 self._errorDisplay.push_exception()
1442 class ContactsView(object):
1444 CONTACT_TYPE_IDX = 0
1445 CONTACT_NAME_IDX = 1
1448 def __init__(self, widgetTree, backend, errorDisplay):
1449 self._errorDisplay = errorDisplay
1450 self._backend = backend
1452 self._addressBook = None
1453 self._selectedComboIndex = 0
1454 self._addressBookFactories = [null_backend.NullAddressBook()]
1456 self._booksList = []
1457 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1459 self._isPopulated = False
1460 self._contactsmodel = gtk.ListStore(
1461 gobject.TYPE_STRING, # Contact Type
1462 gobject.TYPE_STRING, # Contact Name
1463 gobject.TYPE_STRING, # Contact ID
1465 self._contactsviewselection = None
1466 self._contactsview = widgetTree.get_widget("contactsview")
1468 self._contactColumn = gtk.TreeViewColumn("Contact")
1469 displayContactSource = False
1470 if displayContactSource:
1471 textrenderer = gtk.CellRendererText()
1472 self._contactColumn.pack_start(textrenderer, expand=False)
1473 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1474 textrenderer = gtk.CellRendererText()
1475 hildonize.set_cell_thumb_selectable(textrenderer)
1476 self._contactColumn.pack_start(textrenderer, expand=True)
1477 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1478 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1479 self._contactColumn.set_sort_column_id(1)
1480 self._contactColumn.set_visible(True)
1482 self._onContactsviewRowActivatedId = 0
1483 self._onAddressbookButtonChangedId = 0
1484 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1485 self._smsDialog = SmsEntryDialog(widgetTree)
1487 self._updateSink = gtk_toolbox.threaded_stage(
1489 self._idly_populate_contactsview,
1490 gtk_toolbox.null_sink(),
1495 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1497 self._contactsview.set_model(self._contactsmodel)
1498 self._contactsview.set_fixed_height_mode(False)
1499 self._contactsview.append_column(self._contactColumn)
1500 self._contactsviewselection = self._contactsview.get_selection()
1501 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1503 del self._booksList[:]
1504 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1505 if factoryName and bookName:
1506 entryName = "%s: %s" % (factoryName, bookName)
1508 entryName = factoryName
1510 entryName = bookName
1512 entryName = "Bad name (%d)" % factoryId
1513 row = (str(factoryId), bookId, entryName)
1514 self._booksList.append(row)
1516 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1517 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1519 if len(self._booksList) <= self._selectedComboIndex:
1520 self._selectedComboIndex = 0
1521 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1523 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1524 selectedBookId = self._booksList[self._selectedComboIndex][1]
1525 self.open_addressbook(selectedFactoryId, selectedBookId)
1528 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1529 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1533 self._bookSelectionButton.set_label("")
1534 self._contactsview.set_model(None)
1535 self._contactsview.remove_column(self._contactColumn)
1537 def number_selected(self, action, numbers, message):
1539 @note Actual dial function is patched in later
1541 raise NotImplementedError("Horrible unknown error has occurred")
1543 def get_addressbooks(self):
1545 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1547 for i, factory in enumerate(self._addressBookFactories):
1548 for bookFactory, bookId, bookName in factory.get_addressbooks():
1549 yield (str(i), bookId), (factory.factory_name(), bookName)
1551 def open_addressbook(self, bookFactoryId, bookId):
1552 bookFactoryIndex = int(bookFactoryId)
1553 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1554 self._addressBook = addressBook
1556 def update(self, force = False):
1557 if not force and self._isPopulated:
1559 self._updateSink.send(())
1563 self._isPopulated = False
1564 self._contactsmodel.clear()
1565 for factory in self._addressBookFactories:
1566 factory.clear_caches()
1567 self._addressBook.clear_caches()
1569 def append(self, book):
1570 self._addressBookFactories.append(book)
1572 def extend(self, books):
1573 self._addressBookFactories.extend(books)
1579 def load_settings(self, config, sectionName):
1581 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1582 except ConfigParser.NoOptionError:
1583 self._selectedComboIndex = 0
1585 def save_settings(self, config, sectionName):
1586 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1588 def _idly_populate_contactsview(self):
1589 with gtk_toolbox.gtk_lock():
1590 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1593 while addressBook is not self._addressBook:
1594 addressBook = self._addressBook
1595 with gtk_toolbox.gtk_lock():
1596 self._contactsview.set_model(None)
1600 contacts = addressBook.get_contacts()
1601 except Exception, e:
1603 self._isPopulated = False
1604 self._errorDisplay.push_exception_with_lock()
1605 for contactId, contactName in contacts:
1606 contactType = addressBook.contact_source_short_name(contactId)
1607 row = contactType, contactName, contactId
1608 self._contactsmodel.append(row)
1610 with gtk_toolbox.gtk_lock():
1611 self._contactsview.set_model(self._contactsmodel)
1613 self._isPopulated = True
1614 except Exception, e:
1615 self._errorDisplay.push_exception_with_lock()
1617 with gtk_toolbox.gtk_lock():
1618 hildonize.show_busy_banner_end(banner)
1621 def _on_addressbook_button_changed(self, *args, **kwds):
1624 newSelectedComboIndex = hildonize.touch_selector(
1627 (("%s" % m[2]) for m in self._booksList),
1628 self._selectedComboIndex,
1630 except RuntimeError:
1633 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1634 selectedBookId = self._booksList[newSelectedComboIndex][1]
1636 oldAddressbook = self._addressBook
1637 self.open_addressbook(selectedFactoryId, selectedBookId)
1638 forceUpdate = True if oldAddressbook is not self._addressBook else False
1639 self.update(force=forceUpdate)
1641 self._selectedComboIndex = newSelectedComboIndex
1642 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1643 except Exception, e:
1644 self._errorDisplay.push_exception()
1646 def _on_contactsview_row_activated(self, treeview, path, view_column):
1648 itr = self._contactsmodel.get_iter(path)
1652 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1653 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1655 contactDetails = self._addressBook.get_contact_details(contactId)
1656 except Exception, e:
1658 self._errorDisplay.push_exception()
1659 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1661 if len(contactPhoneNumbers) == 0:
1664 action, phoneNumbers, message = self._smsDialog.run(
1665 contactPhoneNumbers,
1666 messages = (contactName, ),
1667 parent = self._window,
1669 if action == SmsEntryDialog.ACTION_CANCEL:
1671 assert phoneNumbers, "A lack of phone number exists"
1673 self.number_selected(action, phoneNumbers, message)
1674 self._contactsviewselection.unselect_all()
1675 except Exception, e:
1676 self._errorDisplay.push_exception()