4 DialCentral - Front end for Google's Grand Central 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 Feature request: The ability to go to relevant thing in web browser
24 from __future__ import with_statement
37 def make_ugly(prettynumber):
39 function to take a phone number and strip out all non-numeric
42 >>> make_ugly("+012-(345)-678-90")
46 uglynumber = re.sub('\D', '', prettynumber)
50 def make_pretty(phonenumber):
52 Function to take a phone number and return the pretty version
54 if phonenumber begins with 0:
56 if phonenumber begins with 1: ( for gizmo callback numbers )
58 if phonenumber is 13 digits:
60 if phonenumber is 10 digits:
64 >>> make_pretty("1234567")
66 >>> make_pretty("2345678901")
68 >>> make_pretty("12345678901")
70 >>> make_pretty("01234567890")
73 if phonenumber is None or phonenumber is "":
76 phonenumber = make_ugly(phonenumber)
78 if len(phonenumber) < 3:
81 if phonenumber[0] == "0":
83 prettynumber += "+%s" % phonenumber[0:3]
84 if 3 < len(phonenumber):
85 prettynumber += "-(%s)" % phonenumber[3:6]
86 if 6 < len(phonenumber):
87 prettynumber += "-%s" % phonenumber[6:9]
88 if 9 < len(phonenumber):
89 prettynumber += "-%s" % phonenumber[9:]
91 elif len(phonenumber) <= 7:
92 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
93 elif len(phonenumber) > 8 and phonenumber[0] == "1":
94 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
95 elif len(phonenumber) > 7:
96 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
100 class MergedAddressBook(object):
102 Merger of all addressbooks
105 def __init__(self, addressbookFactories, sorter = None):
106 self.__addressbookFactories = addressbookFactories
107 self.__addressbooks = None
108 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
110 def clear_caches(self):
111 self.__addressbooks = None
112 for factory in self.__addressbookFactories:
113 factory.clear_caches()
115 def get_addressbooks(self):
117 @returns Iterable of (Address Book Factory, Book Id, Book Name)
121 def open_addressbook(self, bookId):
124 def contact_source_short_name(self, contactId):
125 if self.__addressbooks is None:
127 bookIndex, originalId = contactId.split("-", 1)
128 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
132 return "All Contacts"
134 def get_contacts(self):
136 @returns Iterable of (contact id, contact name)
138 if self.__addressbooks is None:
139 self.__addressbooks = list(
140 factory.open_addressbook(id)
141 for factory in self.__addressbookFactories
142 for (f, id, name) in factory.get_addressbooks()
145 ("-".join([str(bookIndex), contactId]), contactName)
146 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
147 for (contactId, contactName) in addressbook.get_contacts()
149 sortedContacts = self.__sort_contacts(contacts)
150 return sortedContacts
152 def get_contact_details(self, contactId):
154 @returns Iterable of (Phone Type, Phone Number)
156 if self.__addressbooks is None:
158 bookIndex, originalId = contactId.split("-", 1)
159 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
162 def null_sorter(contacts):
164 Good for speed/low memory
169 def basic_firtname_sorter(contacts):
171 Expects names in "First Last" format
174 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
175 for (contactId, contactName) in contacts
177 contactsWithKey.sort()
178 return (contactData for (lastName, contactData) in contactsWithKey)
181 def basic_lastname_sorter(contacts):
183 Expects names in "First Last" format
186 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
187 for (contactId, contactName) in contacts
189 contactsWithKey.sort()
190 return (contactData for (lastName, contactData) in contactsWithKey)
193 def reversed_firtname_sorter(contacts):
195 Expects names in "Last, First" format
198 (contactName.split(", ", 1)[-1], (contactId, contactName))
199 for (contactId, contactName) in contacts
201 contactsWithKey.sort()
202 return (contactData for (lastName, contactData) in contactsWithKey)
205 def reversed_lastname_sorter(contacts):
207 Expects names in "Last, First" format
210 (contactName.split(", ", 1)[0], (contactId, contactName))
211 for (contactId, contactName) in contacts
213 contactsWithKey.sort()
214 return (contactData for (lastName, contactData) in contactsWithKey)
217 def guess_firstname(name):
219 return name.split(", ", 1)[-1]
221 return name.rsplit(" ", 1)[0]
224 def guess_lastname(name):
226 return name.split(", ", 1)[0]
228 return name.rsplit(" ", 1)[-1]
231 def advanced_firstname_sorter(cls, contacts):
233 (cls.guess_firstname(contactName), (contactId, contactName))
234 for (contactId, contactName) in contacts
236 contactsWithKey.sort()
237 return (contactData for (lastName, contactData) in contactsWithKey)
240 def advanced_lastname_sorter(cls, contacts):
242 (cls.guess_lastname(contactName), (contactId, contactName))
243 for (contactId, contactName) in contacts
245 contactsWithKey.sort()
246 return (contactData for (lastName, contactData) in contactsWithKey)
249 class PhoneTypeSelector(object):
251 ACTION_CANCEL = "cancel"
252 ACTION_SELECT = "select"
254 ACTION_SEND_SMS = "sms"
256 def __init__(self, widgetTree, gcBackend):
257 self._gcBackend = gcBackend
258 self._widgetTree = widgetTree
260 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
261 self._smsDialog = SmsEntryDialog(self._widgetTree)
263 self._smsButton = self._widgetTree.get_widget("sms_button")
264 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
266 self._dialButton = self._widgetTree.get_widget("dial_button")
267 self._dialButton.connect("clicked", self._on_phonetype_dial)
269 self._selectButton = self._widgetTree.get_widget("select_button")
270 self._selectButton.connect("clicked", self._on_phonetype_select)
272 self._cancelButton = self._widgetTree.get_widget("cancel_button")
273 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
275 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
276 self._typeviewselection = None
278 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
279 self._typeview = self._widgetTree.get_widget("phonetypes")
280 self._typeview.connect("row-activated", self._on_phonetype_select)
282 self._action = self.ACTION_CANCEL
284 def run(self, contactDetails, message = "", parent = None):
285 self._action = self.ACTION_CANCEL
286 self._typemodel.clear()
287 self._typeview.set_model(self._typemodel)
289 # Add the column to the treeview
290 textrenderer = gtk.CellRendererText()
291 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
292 self._typeview.append_column(numberColumn)
294 textrenderer = gtk.CellRendererText()
295 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
296 self._typeview.append_column(typeColumn)
298 self._typeviewselection = self._typeview.get_selection()
299 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
301 for phoneType, phoneNumber in contactDetails:
302 display = " - ".join((phoneNumber, phoneType))
304 row = (phoneNumber, display)
305 self._typemodel.append(row)
307 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
309 self._message.set_markup(message)
312 self._message.set_markup("")
315 if parent is not None:
316 self._dialog.set_transient_for(parent)
319 userResponse = self._dialog.run()
323 if userResponse == gtk.RESPONSE_OK:
324 phoneNumber = self._get_number()
325 phoneNumber = make_ugly(phoneNumber)
329 self._action = self.ACTION_CANCEL
331 if self._action == self.ACTION_SEND_SMS:
332 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
335 self._action = self.ACTION_CANCEL
339 self._typeviewselection.unselect_all()
340 self._typeview.remove_column(numberColumn)
341 self._typeview.remove_column(typeColumn)
342 self._typeview.set_model(None)
344 return self._action, phoneNumber, smsMessage
346 def _get_number(self):
347 model, itr = self._typeviewselection.get_selected()
351 phoneNumber = self._typemodel.get_value(itr, 0)
354 def _on_phonetype_dial(self, *args):
355 self._dialog.response(gtk.RESPONSE_OK)
356 self._action = self.ACTION_DIAL
358 def _on_phonetype_send_sms(self, *args):
359 self._dialog.response(gtk.RESPONSE_OK)
360 self._action = self.ACTION_SEND_SMS
362 def _on_phonetype_select(self, *args):
363 self._dialog.response(gtk.RESPONSE_OK)
364 self._action = self.ACTION_SELECT
366 def _on_phonetype_cancel(self, *args):
367 self._dialog.response(gtk.RESPONSE_CANCEL)
368 self._action = self.ACTION_CANCEL
371 class SmsEntryDialog(object):
374 @todo Add multi-SMS messages like GoogleVoice
379 def __init__(self, widgetTree):
380 self._widgetTree = widgetTree
381 self._dialog = self._widgetTree.get_widget("smsDialog")
383 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
384 self._smsButton.connect("clicked", self._on_send)
386 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
387 self._cancelButton.connect("clicked", self._on_cancel)
389 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
390 self._message = self._widgetTree.get_widget("smsMessage")
391 self._smsEntry = self._widgetTree.get_widget("smsEntry")
392 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
394 def run(self, number, message = "", parent = None):
396 self._message.set_markup(message)
399 self._message.set_markup("")
401 self._smsEntry.get_buffer().set_text("")
402 self._update_letter_count()
404 if parent is not None:
405 self._dialog.set_transient_for(parent)
408 userResponse = self._dialog.run()
412 if userResponse == gtk.RESPONSE_OK:
413 entryBuffer = self._smsEntry.get_buffer()
414 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
415 enteredMessage = enteredMessage[0:self.MAX_CHAR]
419 return enteredMessage.strip()
421 def _update_letter_count(self, *args):
422 entryLength = self._smsEntry.get_buffer().get_char_count()
423 charsLeft = self.MAX_CHAR - entryLength
424 self._letterCountLabel.set_text(str(charsLeft))
426 self._smsButton.set_sensitive(False)
428 self._smsButton.set_sensitive(True)
430 def _on_entry_changed(self, *args):
431 self._update_letter_count()
433 def _on_send(self, *args):
434 self._dialog.response(gtk.RESPONSE_OK)
436 def _on_cancel(self, *args):
437 self._dialog.response(gtk.RESPONSE_CANCEL)
440 class Dialpad(object):
442 def __init__(self, widgetTree, errorDisplay):
443 self._errorDisplay = errorDisplay
444 self._smsDialog = SmsEntryDialog(widgetTree)
446 self._numberdisplay = widgetTree.get_widget("numberdisplay")
447 self._dialButton = widgetTree.get_widget("dial")
448 self._backButton = widgetTree.get_widget("back")
449 self._phonenumber = ""
450 self._prettynumber = ""
453 "on_dial_clicked": self._on_dial_clicked,
454 "on_sms_clicked": self._on_sms_clicked,
455 "on_digit_clicked": self._on_digit_clicked,
456 "on_clear_number": self._on_clear_number,
458 widgetTree.signal_autoconnect(callbackMapping)
460 self._originalLabel = self._backButton.get_label()
461 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
462 self._backTapHandler.on_tap = self._on_backspace
463 self._backTapHandler.on_hold = self._on_clearall
464 self._backTapHandler.on_holding = self._set_clear_button
465 self._backTapHandler.on_cancel = self._reset_back_button
467 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
470 self._dialButton.grab_focus()
471 self._backTapHandler.enable()
474 self._reset_back_button()
475 self._backTapHandler.disable()
477 def number_selected(self, action, number, message):
479 @note Actual dial function is patched in later
481 raise NotImplementedError("Horrible unknown error has occurred")
483 def get_number(self):
484 return self._phonenumber
486 def set_number(self, number):
488 Set the number to dial
491 self._phonenumber = make_ugly(number)
492 self._prettynumber = make_pretty(self._phonenumber)
493 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
495 self._errorDisplay.push_exception()
504 def load_settings(self, config, section):
507 def save_settings(self, config, section):
509 @note Thread Agnostic
513 def _on_sms_clicked(self, widget):
514 action = PhoneTypeSelector.ACTION_SEND_SMS
515 phoneNumber = self.get_number()
517 message = self._smsDialog.run(phoneNumber, "", self._window)
520 action = PhoneTypeSelector.ACTION_CANCEL
522 if action == PhoneTypeSelector.ACTION_CANCEL:
524 self.number_selected(action, phoneNumber, message)
526 def _on_dial_clicked(self, widget):
527 action = PhoneTypeSelector.ACTION_DIAL
528 phoneNumber = self.get_number()
530 self.number_selected(action, phoneNumber, message)
532 def _on_clear_number(self, *args):
535 def _on_digit_clicked(self, widget):
536 self.set_number(self._phonenumber + widget.get_name()[-1])
538 def _on_backspace(self, taps):
539 self.set_number(self._phonenumber[:-taps])
540 self._reset_back_button()
542 def _on_clearall(self, taps):
544 self._reset_back_button()
547 def _set_clear_button(self):
548 self._backButton.set_label("gtk-clear")
550 def _reset_back_button(self):
551 self._backButton.set_label(self._originalLabel)
554 class AccountInfo(object):
556 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
557 self._errorDisplay = errorDisplay
558 self._backend = backend
559 self._isPopulated = False
560 self._alarmHandler = alarmHandler
561 self._notifyOnMissed = False
562 self._notifyOnVoicemail = False
563 self._notifyOnSms = False
565 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
566 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
567 self._callbackCombo = widgetTree.get_widget("callbackcombo")
568 self._onCallbackentryChangedId = 0
570 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
571 self._minutesEntry = widgetTree.get_widget("minutesEntry")
572 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
573 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
574 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
575 self._onNotifyToggled = 0
576 self._onMinutesChanged = 0
577 self._onMissedToggled = 0
578 self._onVoicemailToggled = 0
579 self._onSmsToggled = 0
580 self._applyAlarmTimeoutId = None
582 self._defaultCallback = ""
585 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
587 self._accountViewNumberDisplay.set_use_markup(True)
588 self.set_account_number("")
590 self._callbackList.clear()
591 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
593 if self._alarmHandler is not None:
594 self._minutesEntry.set_range(1, 60)
595 self._minutesEntry.set_increments(1, 5)
597 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
598 self._minutesEntry.set_value(self._alarmHandler.recurrence)
599 self._missedCheckbox.set_active(self._notifyOnMissed)
600 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
601 self._smsCheckbox.set_active(self._notifyOnSms)
603 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
604 self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
605 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
606 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
607 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
609 self._notifyCheckbox.set_sensitive(False)
610 self._minutesEntry.set_sensitive(False)
611 self._missedCheckbox.set_sensitive(False)
612 self._voicemailCheckbox.set_sensitive(False)
613 self._smsCheckbox.set_sensitive(False)
615 self.update(force=True)
618 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
619 self._onCallbackentryChangedId = 0
621 if self._alarmHandler is not None:
622 self._notifyCheckbox.disconnect(self._onNotifyToggled)
623 self._minutesEntry.disconnect(self._onMinutesChanged)
624 self._missedCheckbox.disconnect(self._onNotifyToggled)
625 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
626 self._smsCheckbox.disconnect(self._onNotifyToggled)
627 self._onNotifyToggled = 0
628 self._onMinutesChanged = 0
629 self._onMissedToggled = 0
630 self._onVoicemailToggled = 0
631 self._onSmsToggled = 0
633 self._notifyCheckbox.set_sensitive(True)
634 self._minutesEntry.set_sensitive(True)
635 self._missedCheckbox.set_sensitive(True)
636 self._voicemailCheckbox.set_sensitive(True)
637 self._smsCheckbox.set_sensitive(True)
640 self._callbackList.clear()
642 def get_selected_callback_number(self):
643 return make_ugly(self._callbackCombo.get_child().get_text())
645 def set_account_number(self, number):
647 Displays current account number
649 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
651 def update(self, force = False):
652 if not force and self._isPopulated:
654 self._populate_callback_combo()
655 self.set_account_number(self._backend.get_account_number())
659 self._callbackCombo.get_child().set_text("")
660 self.set_account_number("")
661 self._isPopulated = False
663 def save_everything(self):
664 raise NotImplementedError
668 return "Account Info"
670 def load_settings(self, config, section):
671 self._defaultCallback = config.get(section, "callback")
672 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
673 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
674 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
676 def save_settings(self, config, section):
678 @note Thread Agnostic
680 callback = self.get_selected_callback_number()
681 config.set(section, "callback", callback)
682 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
683 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
684 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
686 def _populate_callback_combo(self):
687 self._isPopulated = True
688 self._callbackList.clear()
690 callbackNumbers = self._backend.get_callback_numbers()
691 except StandardError, e:
692 self._errorDisplay.push_exception()
693 self._isPopulated = False
696 for number, description in callbackNumbers.iteritems():
697 self._callbackList.append((make_pretty(number),))
699 self._callbackCombo.set_model(self._callbackList)
700 self._callbackCombo.set_text_column(0)
701 #callbackNumber = self._backend.get_callback_number()
702 callbackNumber = self._defaultCallback
703 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
705 def _set_callback_number(self, number):
707 if not self._backend.is_valid_syntax(number):
708 self._errorDisplay.push_message("%s is not a valid callback number" % number)
709 elif number == self._backend.get_callback_number():
711 "Callback number already is %s" % (
712 self._backend.get_callback_number(),
718 self._backend.set_callback_number(number)
719 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
720 make_pretty(number), make_pretty(self._backend.get_callback_number())
723 "Callback number set to %s" % (
724 self._backend.get_callback_number(),
728 except StandardError, e:
729 self._errorDisplay.push_exception()
731 def _update_alarm_settings(self):
733 isEnabled = self._notifyCheckbox.get_active()
734 recurrence = self._minutesEntry.get_value_as_int()
735 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
736 self._alarmHandler.apply_settings(isEnabled, recurrence)
738 self.save_everything()
739 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
740 self._minutesEntry.set_value(self._alarmHandler.recurrence)
742 def _on_callbackentry_changed(self, *args):
743 text = self.get_selected_callback_number()
744 number = make_ugly(text)
745 self._set_callback_number(number)
747 def _on_notify_toggled(self, *args):
748 if self._applyAlarmTimeoutId is not None:
749 gobject.source_remove(self._applyAlarmTimeoutId)
750 self._applyAlarmTimeoutId = None
751 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
753 def _on_minutes_changed(self, *args):
754 if self._applyAlarmTimeoutId is not None:
755 gobject.source_remove(self._applyAlarmTimeoutId)
756 self._applyAlarmTimeoutId = None
757 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
759 def _on_apply_timeout(self, *args):
760 self._applyAlarmTimeoutId = None
762 self._update_alarm_settings()
765 def _on_missed_toggled(self, *args):
766 self._notifyOnMissed = self._missedCheckbox.get_active()
767 self.save_everything()
769 def _on_voicemail_toggled(self, *args):
770 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
771 self.save_everything()
773 def _on_sms_toggled(self, *args):
774 self._notifyOnSms = self._smsCheckbox.get_active()
775 self.save_everything()
778 class RecentCallsView(object):
785 def __init__(self, widgetTree, backend, errorDisplay):
786 self._errorDisplay = errorDisplay
787 self._backend = backend
789 self._isPopulated = False
790 self._recentmodel = gtk.ListStore(
791 gobject.TYPE_STRING, # number
792 gobject.TYPE_STRING, # date
793 gobject.TYPE_STRING, # action
794 gobject.TYPE_STRING, # from
796 self._recentview = widgetTree.get_widget("recentview")
797 self._recentviewselection = None
798 self._onRecentviewRowActivatedId = 0
800 textrenderer = gtk.CellRendererText()
801 textrenderer.set_property("yalign", 0)
802 self._dateColumn = gtk.TreeViewColumn("Date")
803 self._dateColumn.pack_start(textrenderer, expand=True)
804 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
806 textrenderer = gtk.CellRendererText()
807 textrenderer.set_property("yalign", 0)
808 self._actionColumn = gtk.TreeViewColumn("Action")
809 self._actionColumn.pack_start(textrenderer, expand=True)
810 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
812 textrenderer = gtk.CellRendererText()
813 textrenderer.set_property("yalign", 0)
814 textrenderer.set_property("scale", 1.5)
815 self._fromColumn = gtk.TreeViewColumn("From")
816 self._fromColumn.pack_start(textrenderer, expand=True)
817 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
818 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
820 self._window = gtk_toolbox.find_parent_window(self._recentview)
821 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
823 self._updateSink = gtk_toolbox.threaded_stage(
825 self._idly_populate_recentview,
826 gtk_toolbox.null_sink(),
831 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
832 self._recentview.set_model(self._recentmodel)
834 self._recentview.append_column(self._dateColumn)
835 self._recentview.append_column(self._actionColumn)
836 self._recentview.append_column(self._fromColumn)
837 self._recentviewselection = self._recentview.get_selection()
838 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
840 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
843 self._recentview.disconnect(self._onRecentviewRowActivatedId)
847 self._recentview.remove_column(self._dateColumn)
848 self._recentview.remove_column(self._actionColumn)
849 self._recentview.remove_column(self._fromColumn)
850 self._recentview.set_model(None)
852 def number_selected(self, action, number, message):
854 @note Actual dial function is patched in later
856 raise NotImplementedError("Horrible unknown error has occurred")
858 def update(self, force = False):
859 if not force and self._isPopulated:
861 self._updateSink.send(())
865 self._isPopulated = False
866 self._recentmodel.clear()
870 return "Recent Calls"
872 def load_settings(self, config, section):
875 def save_settings(self, config, section):
877 @note Thread Agnostic
881 def _idly_populate_recentview(self):
882 self._recentmodel.clear()
883 self._isPopulated = True
886 recentItems = self._backend.get_recent()
887 except StandardError, e:
888 self._errorDisplay.push_exception_with_lock()
889 self._isPopulated = False
892 for personName, phoneNumber, date, action in recentItems:
894 personName = "Unknown"
895 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
896 prettyNumber = make_pretty(prettyNumber)
897 description = "%s - %s" % (personName, prettyNumber)
898 item = (phoneNumber, date, action.capitalize(), description)
899 with gtk_toolbox.gtk_lock():
900 self._recentmodel.append(item)
904 def _on_recentview_row_activated(self, treeview, path, view_column):
905 model, itr = self._recentviewselection.get_selected()
909 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
910 number = make_ugly(number)
911 contactPhoneNumbers = [("Phone", number)]
912 description = self._recentmodel.get_value(itr, self.FROM_IDX)
914 action, phoneNumber, message = self._phoneTypeSelector.run(
916 message = description,
917 parent = self._window,
919 if action == PhoneTypeSelector.ACTION_CANCEL:
921 assert phoneNumber, "A lack of phone number exists"
923 self.number_selected(action, phoneNumber, message)
924 self._recentviewselection.unselect_all()
927 class MessagesView(object):
934 def __init__(self, widgetTree, backend, errorDisplay):
935 self._errorDisplay = errorDisplay
936 self._backend = backend
938 self._isPopulated = False
939 self._messagemodel = gtk.ListStore(
940 gobject.TYPE_STRING, # number
941 gobject.TYPE_STRING, # date
942 gobject.TYPE_STRING, # header
943 gobject.TYPE_STRING, # message
945 self._messageview = widgetTree.get_widget("messages_view")
946 self._messageviewselection = None
947 self._onMessageviewRowActivatedId = 0
949 textrenderer = gtk.CellRendererText()
950 textrenderer.set_property("yalign", 0)
951 self._dateColumn = gtk.TreeViewColumn("Date")
952 self._dateColumn.pack_start(textrenderer, expand=True)
953 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
955 textrenderer = gtk.CellRendererText()
956 textrenderer.set_property("yalign", 0)
957 self._headerColumn = gtk.TreeViewColumn("From")
958 self._headerColumn.pack_start(textrenderer, expand=True)
959 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
961 textrenderer = gtk.CellRendererText()
962 textrenderer.set_property("yalign", 0)
963 textrenderer.set_property("scale", 1.5)
964 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
965 textrenderer.set_property("wrap-width", 500)
966 self._messageColumn = gtk.TreeViewColumn("Messages")
967 self._messageColumn.pack_start(textrenderer, expand=True)
968 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
969 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
971 self._window = gtk_toolbox.find_parent_window(self._messageview)
972 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
974 self._updateSink = gtk_toolbox.threaded_stage(
976 self._idly_populate_messageview,
977 gtk_toolbox.null_sink(),
982 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
983 self._messageview.set_model(self._messagemodel)
985 self._messageview.append_column(self._dateColumn)
986 self._messageview.append_column(self._headerColumn)
987 self._messageview.append_column(self._messageColumn)
988 self._messageviewselection = self._messageview.get_selection()
989 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
991 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
994 self._messageview.disconnect(self._onMessageviewRowActivatedId)
998 self._messageview.remove_column(self._dateColumn)
999 self._messageview.remove_column(self._headerColumn)
1000 self._messageview.remove_column(self._messageColumn)
1001 self._messageview.set_model(None)
1003 def number_selected(self, action, number, message):
1005 @note Actual dial function is patched in later
1007 raise NotImplementedError("Horrible unknown error has occurred")
1009 def update(self, force = False):
1010 if not force and self._isPopulated:
1012 self._updateSink.send(())
1016 self._isPopulated = False
1017 self._messagemodel.clear()
1023 def load_settings(self, config, section):
1026 def save_settings(self, config, section):
1028 @note Thread Agnostic
1032 def _idly_populate_messageview(self):
1033 self._messagemodel.clear()
1034 self._isPopulated = True
1037 messageItems = self._backend.get_messages()
1038 except StandardError, e:
1039 self._errorDisplay.push_exception_with_lock()
1040 self._isPopulated = False
1043 for header, number, relativeDate, message in messageItems:
1044 number = make_ugly(number)
1045 row = (number, relativeDate, header, message)
1046 with gtk_toolbox.gtk_lock():
1047 self._messagemodel.append(row)
1051 def _on_messageview_row_activated(self, treeview, path, view_column):
1052 model, itr = self._messageviewselection.get_selected()
1056 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1057 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1059 action, phoneNumber, message = self._phoneTypeSelector.run(
1060 contactPhoneNumbers,
1061 message = description,
1062 parent = self._window,
1064 if action == PhoneTypeSelector.ACTION_CANCEL:
1066 assert phoneNumber, "A lock of phone number exists"
1068 self.number_selected(action, phoneNumber, message)
1069 self._messageviewselection.unselect_all()
1072 class ContactsView(object):
1074 def __init__(self, widgetTree, backend, errorDisplay):
1075 self._errorDisplay = errorDisplay
1076 self._backend = backend
1078 self._addressBook = None
1079 self._selectedComboIndex = 0
1080 self._addressBookFactories = [null_backend.NullAddressBook()]
1082 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1083 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1085 self._isPopulated = False
1086 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1087 self._contactsviewselection = None
1088 self._contactsview = widgetTree.get_widget("contactsview")
1090 self._contactColumn = gtk.TreeViewColumn("Contact")
1091 displayContactSource = False
1092 if displayContactSource:
1093 textrenderer = gtk.CellRendererText()
1094 self._contactColumn.pack_start(textrenderer, expand=False)
1095 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1096 textrenderer = gtk.CellRendererText()
1097 textrenderer.set_property("scale", 1.5)
1098 self._contactColumn.pack_start(textrenderer, expand=True)
1099 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1100 textrenderer = gtk.CellRendererText()
1101 self._contactColumn.pack_start(textrenderer, expand=True)
1102 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1103 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1104 self._contactColumn.set_sort_column_id(1)
1105 self._contactColumn.set_visible(True)
1107 self._onContactsviewRowActivatedId = 0
1108 self._onAddressbookComboChangedId = 0
1109 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1110 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1112 self._updateSink = gtk_toolbox.threaded_stage(
1114 self._idly_populate_contactsview,
1115 gtk_toolbox.null_sink(),
1120 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1122 self._contactsview.set_model(self._contactsmodel)
1123 self._contactsview.append_column(self._contactColumn)
1124 self._contactsviewselection = self._contactsview.get_selection()
1125 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1127 self._booksList.clear()
1128 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1129 if factoryName and bookName:
1130 entryName = "%s: %s" % (factoryName, bookName)
1132 entryName = factoryName
1134 entryName = bookName
1136 entryName = "Bad name (%d)" % factoryId
1137 row = (str(factoryId), bookId, entryName)
1138 self._booksList.append(row)
1140 self._booksSelectionBox.set_model(self._booksList)
1141 cell = gtk.CellRendererText()
1142 self._booksSelectionBox.pack_start(cell, True)
1143 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1145 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1146 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1148 if len(self._booksList) <= self._selectedComboIndex:
1149 self._selectedComboIndex = 0
1150 self._booksSelectionBox.set_active(self._selectedComboIndex)
1153 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1154 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1158 self._booksSelectionBox.clear()
1159 self._booksSelectionBox.set_model(None)
1160 self._contactsview.set_model(None)
1161 self._contactsview.remove_column(self._contactColumn)
1163 def number_selected(self, action, number, message):
1165 @note Actual dial function is patched in later
1167 raise NotImplementedError("Horrible unknown error has occurred")
1169 def get_addressbooks(self):
1171 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1173 for i, factory in enumerate(self._addressBookFactories):
1174 for bookFactory, bookId, bookName in factory.get_addressbooks():
1175 yield (str(i), bookId), (factory.factory_name(), bookName)
1177 def open_addressbook(self, bookFactoryId, bookId):
1178 bookFactoryIndex = int(bookFactoryId)
1179 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1181 forceUpdate = True if addressBook is not self._addressBook else False
1183 self._addressBook = addressBook
1184 self.update(force=forceUpdate)
1186 def update(self, force = False):
1187 if not force and self._isPopulated:
1189 self._updateSink.send(())
1193 self._isPopulated = False
1194 self._contactsmodel.clear()
1195 for factory in self._addressBookFactories:
1196 factory.clear_caches()
1197 self._addressBook.clear_caches()
1199 def append(self, book):
1200 self._addressBookFactories.append(book)
1202 def extend(self, books):
1203 self._addressBookFactories.extend(books)
1209 def load_settings(self, config, sectionName):
1211 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1212 except ConfigParser.NoOptionError:
1213 self._selectedComboIndex = 0
1215 def save_settings(self, config, sectionName):
1216 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1218 def _idly_populate_contactsview(self):
1220 while addressBook is not self._addressBook:
1221 addressBook = self._addressBook
1222 with gtk_toolbox.gtk_lock():
1223 self._contactsview.set_model(None)
1227 contacts = addressBook.get_contacts()
1228 except StandardError, e:
1230 self._isPopulated = False
1231 self._errorDisplay.push_exception_with_lock()
1232 for contactId, contactName in contacts:
1233 contactType = (addressBook.contact_source_short_name(contactId), )
1234 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1236 with gtk_toolbox.gtk_lock():
1237 self._contactsview.set_model(self._contactsmodel)
1239 self._isPopulated = True
1242 def _on_addressbook_combo_changed(self, *args, **kwds):
1243 itr = self._booksSelectionBox.get_active_iter()
1246 self._selectedComboIndex = self._booksSelectionBox.get_active()
1247 selectedFactoryId = self._booksList.get_value(itr, 0)
1248 selectedBookId = self._booksList.get_value(itr, 1)
1249 self.open_addressbook(selectedFactoryId, selectedBookId)
1251 def _on_contactsview_row_activated(self, treeview, path, view_column):
1252 model, itr = self._contactsviewselection.get_selected()
1256 contactId = self._contactsmodel.get_value(itr, 3)
1257 contactName = self._contactsmodel.get_value(itr, 1)
1259 contactDetails = self._addressBook.get_contact_details(contactId)
1260 except StandardError, e:
1262 self._errorDisplay.push_exception()
1263 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1265 if len(contactPhoneNumbers) == 0:
1268 action, phoneNumber, message = self._phoneTypeSelector.run(
1269 contactPhoneNumbers,
1270 message = contactName,
1271 parent = self._window,
1273 if action == PhoneTypeSelector.ACTION_CANCEL:
1275 assert phoneNumber, "A lack of phone number exists"
1277 self.number_selected(action, phoneNumber, message)
1278 self._contactsviewselection.unselect_all()