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
22 from __future__ import with_statement
35 def make_ugly(prettynumber):
37 function to take a phone number and strip out all non-numeric
40 >>> make_ugly("+012-(345)-678-90")
44 uglynumber = re.sub('\D', '', prettynumber)
48 def make_pretty(phonenumber):
50 Function to take a phone number and return the pretty version
52 if phonenumber begins with 0:
54 if phonenumber begins with 1: ( for gizmo callback numbers )
56 if phonenumber is 13 digits:
58 if phonenumber is 10 digits:
62 >>> make_pretty("1234567")
64 >>> make_pretty("2345678901")
66 >>> make_pretty("12345678901")
68 >>> make_pretty("01234567890")
71 if phonenumber is None or phonenumber is "":
74 phonenumber = make_ugly(phonenumber)
76 if len(phonenumber) < 3:
79 if phonenumber[0] == "0":
81 prettynumber += "+%s" % phonenumber[0:3]
82 if 3 < len(phonenumber):
83 prettynumber += "-(%s)" % phonenumber[3:6]
84 if 6 < len(phonenumber):
85 prettynumber += "-%s" % phonenumber[6:9]
86 if 9 < len(phonenumber):
87 prettynumber += "-%s" % phonenumber[9:]
89 elif len(phonenumber) <= 7:
90 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
91 elif len(phonenumber) > 8 and phonenumber[0] == "1":
92 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
93 elif len(phonenumber) > 7:
94 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
98 class MergedAddressBook(object):
100 Merger of all addressbooks
103 def __init__(self, addressbookFactories, sorter = None):
104 self.__addressbookFactories = addressbookFactories
105 self.__addressbooks = None
106 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
108 def clear_caches(self):
109 self.__addressbooks = None
110 for factory in self.__addressbookFactories:
111 factory.clear_caches()
113 def get_addressbooks(self):
115 @returns Iterable of (Address Book Factory, Book Id, Book Name)
119 def open_addressbook(self, bookId):
122 def contact_source_short_name(self, contactId):
123 if self.__addressbooks is None:
125 bookIndex, originalId = contactId.split("-", 1)
126 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
130 return "All Contacts"
132 def get_contacts(self):
134 @returns Iterable of (contact id, contact name)
136 if self.__addressbooks is None:
137 self.__addressbooks = list(
138 factory.open_addressbook(id)
139 for factory in self.__addressbookFactories
140 for (f, id, name) in factory.get_addressbooks()
143 ("-".join([str(bookIndex), contactId]), contactName)
144 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
145 for (contactId, contactName) in addressbook.get_contacts()
147 sortedContacts = self.__sort_contacts(contacts)
148 return sortedContacts
150 def get_contact_details(self, contactId):
152 @returns Iterable of (Phone Type, Phone Number)
154 if self.__addressbooks is None:
156 bookIndex, originalId = contactId.split("-", 1)
157 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
160 def null_sorter(contacts):
162 Good for speed/low memory
167 def basic_firtname_sorter(contacts):
169 Expects names in "First Last" format
172 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
173 for (contactId, contactName) in contacts
175 contactsWithKey.sort()
176 return (contactData for (lastName, contactData) in contactsWithKey)
179 def basic_lastname_sorter(contacts):
181 Expects names in "First Last" format
184 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
185 for (contactId, contactName) in contacts
187 contactsWithKey.sort()
188 return (contactData for (lastName, contactData) in contactsWithKey)
191 def reversed_firtname_sorter(contacts):
193 Expects names in "Last, First" format
196 (contactName.split(", ", 1)[-1], (contactId, contactName))
197 for (contactId, contactName) in contacts
199 contactsWithKey.sort()
200 return (contactData for (lastName, contactData) in contactsWithKey)
203 def reversed_lastname_sorter(contacts):
205 Expects names in "Last, First" format
208 (contactName.split(", ", 1)[0], (contactId, contactName))
209 for (contactId, contactName) in contacts
211 contactsWithKey.sort()
212 return (contactData for (lastName, contactData) in contactsWithKey)
215 def guess_firstname(name):
217 return name.split(", ", 1)[-1]
219 return name.rsplit(" ", 1)[0]
222 def guess_lastname(name):
224 return name.split(", ", 1)[0]
226 return name.rsplit(" ", 1)[-1]
229 def advanced_firstname_sorter(cls, contacts):
231 (cls.guess_firstname(contactName), (contactId, contactName))
232 for (contactId, contactName) in contacts
234 contactsWithKey.sort()
235 return (contactData for (lastName, contactData) in contactsWithKey)
238 def advanced_lastname_sorter(cls, contacts):
240 (cls.guess_lastname(contactName), (contactId, contactName))
241 for (contactId, contactName) in contacts
243 contactsWithKey.sort()
244 return (contactData for (lastName, contactData) in contactsWithKey)
247 class PhoneTypeSelector(object):
249 ACTION_CANCEL = "cancel"
250 ACTION_SELECT = "select"
252 ACTION_SEND_SMS = "sms"
254 def __init__(self, widgetTree, gcBackend):
255 self._gcBackend = gcBackend
256 self._widgetTree = widgetTree
258 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
259 self._smsDialog = SmsEntryDialog(self._widgetTree)
261 self._smsButton = self._widgetTree.get_widget("sms_button")
262 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
264 self._dialButton = self._widgetTree.get_widget("dial_button")
265 self._dialButton.connect("clicked", self._on_phonetype_dial)
267 self._selectButton = self._widgetTree.get_widget("select_button")
268 self._selectButton.connect("clicked", self._on_phonetype_select)
270 self._cancelButton = self._widgetTree.get_widget("cancel_button")
271 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
273 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
274 self._typeviewselection = None
276 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
277 self._typeview = self._widgetTree.get_widget("phonetypes")
278 self._typeview.connect("row-activated", self._on_phonetype_select)
280 self._action = self.ACTION_CANCEL
282 def run(self, contactDetails, message = "", parent = None):
283 self._action = self.ACTION_CANCEL
284 self._typemodel.clear()
285 self._typeview.set_model(self._typemodel)
287 # Add the column to the treeview
288 textrenderer = gtk.CellRendererText()
289 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
290 self._typeview.append_column(numberColumn)
292 textrenderer = gtk.CellRendererText()
293 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
294 self._typeview.append_column(typeColumn)
296 self._typeviewselection = self._typeview.get_selection()
297 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
299 for phoneType, phoneNumber in contactDetails:
300 display = " - ".join((phoneNumber, phoneType))
302 row = (phoneNumber, display)
303 self._typemodel.append(row)
305 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
307 self._message.set_markup(message)
310 self._message.set_markup("")
313 if parent is not None:
314 self._dialog.set_transient_for(parent)
317 userResponse = self._dialog.run()
321 if userResponse == gtk.RESPONSE_OK:
322 phoneNumber = self._get_number()
323 phoneNumber = make_ugly(phoneNumber)
327 self._action = self.ACTION_CANCEL
329 if self._action == self.ACTION_SEND_SMS:
330 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
333 self._action = self.ACTION_CANCEL
337 self._typeviewselection.unselect_all()
338 self._typeview.remove_column(numberColumn)
339 self._typeview.remove_column(typeColumn)
340 self._typeview.set_model(None)
342 return self._action, phoneNumber, smsMessage
344 def _get_number(self):
345 model, itr = self._typeviewselection.get_selected()
349 phoneNumber = self._typemodel.get_value(itr, 0)
352 def _on_phonetype_dial(self, *args):
353 self._dialog.response(gtk.RESPONSE_OK)
354 self._action = self.ACTION_DIAL
356 def _on_phonetype_send_sms(self, *args):
357 self._dialog.response(gtk.RESPONSE_OK)
358 self._action = self.ACTION_SEND_SMS
360 def _on_phonetype_select(self, *args):
361 self._dialog.response(gtk.RESPONSE_OK)
362 self._action = self.ACTION_SELECT
364 def _on_phonetype_cancel(self, *args):
365 self._dialog.response(gtk.RESPONSE_CANCEL)
366 self._action = self.ACTION_CANCEL
369 class SmsEntryDialog(object):
372 @todo Add multi-SMS messages like GoogleVoice
377 def __init__(self, widgetTree):
378 self._widgetTree = widgetTree
379 self._dialog = self._widgetTree.get_widget("smsDialog")
381 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
382 self._smsButton.connect("clicked", self._on_send)
384 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
385 self._cancelButton.connect("clicked", self._on_cancel)
387 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
388 self._message = self._widgetTree.get_widget("smsMessage")
389 self._smsEntry = self._widgetTree.get_widget("smsEntry")
390 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
392 def run(self, number, message = "", parent = None):
394 self._message.set_markup(message)
397 self._message.set_markup("")
399 self._smsEntry.get_buffer().set_text("")
400 self._update_letter_count()
402 if parent is not None:
403 self._dialog.set_transient_for(parent)
406 userResponse = self._dialog.run()
410 if userResponse == gtk.RESPONSE_OK:
411 entryBuffer = self._smsEntry.get_buffer()
412 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
413 enteredMessage = enteredMessage[0:self.MAX_CHAR]
417 return enteredMessage.strip()
419 def _update_letter_count(self, *args):
420 entryLength = self._smsEntry.get_buffer().get_char_count()
421 charsLeft = self.MAX_CHAR - entryLength
422 self._letterCountLabel.set_text(str(charsLeft))
424 self._smsButton.set_sensitive(False)
426 self._smsButton.set_sensitive(True)
428 def _on_entry_changed(self, *args):
429 self._update_letter_count()
431 def _on_send(self, *args):
432 self._dialog.response(gtk.RESPONSE_OK)
434 def _on_cancel(self, *args):
435 self._dialog.response(gtk.RESPONSE_CANCEL)
438 class Dialpad(object):
440 def __init__(self, widgetTree, errorDisplay):
441 self._errorDisplay = errorDisplay
442 self._smsDialog = SmsEntryDialog(widgetTree)
444 self._numberdisplay = widgetTree.get_widget("numberdisplay")
445 self._dialButton = widgetTree.get_widget("dial")
446 self._backButton = widgetTree.get_widget("back")
447 self._phonenumber = ""
448 self._prettynumber = ""
451 "on_dial_clicked": self._on_dial_clicked,
452 "on_sms_clicked": self._on_sms_clicked,
453 "on_digit_clicked": self._on_digit_clicked,
454 "on_clear_number": self._on_clear_number,
456 widgetTree.signal_autoconnect(callbackMapping)
458 self._originalLabel = self._backButton.get_label()
459 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
460 self._backTapHandler.on_tap = self._on_backspace
461 self._backTapHandler.on_hold = self._on_clearall
462 self._backTapHandler.on_holding = self._set_clear_button
463 self._backTapHandler.on_cancel = self._reset_back_button
465 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
468 self.set_number(self._phonenumber)
469 self._dialButton.grab_focus()
470 self._backTapHandler.enable()
473 self._reset_back_button()
474 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 dateRenderer = gtk.CellRendererText()
950 dateRenderer.set_property("yalign", 0)
951 self._dateColumn = gtk.TreeViewColumn("Date")
952 self._dateColumn.pack_start(dateRenderer, expand=True)
953 self._dateColumn.add_attribute(dateRenderer, "markup", self.DATE_IDX)
955 self._nameRenderer = gtk.CellRendererText()
956 self._nameRenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
957 self._nameRenderer.set_property("yalign", 0)
958 self._headerColumn = gtk.TreeViewColumn("From")
959 self._headerColumn.pack_start(self._nameRenderer, expand=True)
960 self._headerColumn.add_attribute(self._nameRenderer, "markup", self.HEADER_IDX)
962 messageRenderer = gtk.CellRendererText()
963 messageRenderer.set_property("yalign", 0)
964 messageRenderer.set_property("scale", 1.5)
965 messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
966 messageRenderer.set_property("wrap-width", 500)
967 self._messageColumn = gtk.TreeViewColumn("Messages")
968 self._messageColumn.pack_start(messageRenderer, expand=True)
969 self._messageColumn.add_attribute(messageRenderer, "markup", self.MESSAGE_IDX)
970 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
972 self._window = gtk_toolbox.find_parent_window(self._messageview)
973 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
975 self._updateSink = gtk_toolbox.threaded_stage(
977 self._idly_populate_messageview,
978 gtk_toolbox.null_sink(),
983 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
984 self._messageview.set_model(self._messagemodel)
986 self._messageview.append_column(self._dateColumn)
987 self._messageview.append_column(self._headerColumn)
988 self._messageview.append_column(self._messageColumn)
989 self._messageviewselection = self._messageview.get_selection()
990 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
992 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
995 self._messageview.disconnect(self._onMessageviewRowActivatedId)
999 self._messageview.remove_column(self._dateColumn)
1000 self._messageview.remove_column(self._headerColumn)
1001 self._messageview.remove_column(self._messageColumn)
1002 self._messageview.set_model(None)
1004 def number_selected(self, action, number, message):
1006 @note Actual dial function is patched in later
1008 raise NotImplementedError("Horrible unknown error has occurred")
1010 def update(self, force = False):
1011 if not force and self._isPopulated:
1013 self._updateSink.send(())
1017 self._isPopulated = False
1018 self._messagemodel.clear()
1024 def load_settings(self, config, section):
1027 def save_settings(self, config, section):
1029 @note Thread Agnostic
1033 def _idly_populate_messageview(self):
1034 self._messagemodel.clear()
1035 self._isPopulated = True
1038 messageItems = self._backend.get_messages()
1039 except StandardError, e:
1040 self._errorDisplay.push_exception_with_lock()
1041 self._isPopulated = False
1044 combinedHeaderLength = 0
1046 for header, number, relativeDate, message in messageItems:
1047 number = make_ugly(number)
1048 row = (number, relativeDate, header, message)
1049 combinedHeaderLength += len(header)
1051 with gtk_toolbox.gtk_lock():
1052 self._messagemodel.append(row)
1053 with gtk_toolbox.gtk_lock():
1055 cellWidth = int((4 * combinedHeaderLength) / (3 * headerCount))
1058 self._nameRenderer.set_property("width-chars", cellWidth)
1062 def _on_messageview_row_activated(self, treeview, path, view_column):
1063 model, itr = self._messageviewselection.get_selected()
1067 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1068 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1070 action, phoneNumber, message = self._phoneTypeSelector.run(
1071 contactPhoneNumbers,
1072 message = description,
1073 parent = self._window,
1075 if action == PhoneTypeSelector.ACTION_CANCEL:
1077 assert phoneNumber, "A lock of phone number exists"
1079 self.number_selected(action, phoneNumber, message)
1080 self._messageviewselection.unselect_all()
1083 class ContactsView(object):
1085 def __init__(self, widgetTree, backend, errorDisplay):
1086 self._errorDisplay = errorDisplay
1087 self._backend = backend
1089 self._addressBook = None
1090 self._selectedComboIndex = 0
1091 self._addressBookFactories = [null_backend.NullAddressBook()]
1093 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1094 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1096 self._isPopulated = False
1097 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1098 self._contactsviewselection = None
1099 self._contactsview = widgetTree.get_widget("contactsview")
1101 self._contactColumn = gtk.TreeViewColumn("Contact")
1102 displayContactSource = False
1103 if displayContactSource:
1104 textrenderer = gtk.CellRendererText()
1105 self._contactColumn.pack_start(textrenderer, expand=False)
1106 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1107 textrenderer = gtk.CellRendererText()
1108 textrenderer.set_property("scale", 1.5)
1109 self._contactColumn.pack_start(textrenderer, expand=True)
1110 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1111 textrenderer = gtk.CellRendererText()
1112 self._contactColumn.pack_start(textrenderer, expand=True)
1113 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1114 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1115 self._contactColumn.set_sort_column_id(1)
1116 self._contactColumn.set_visible(True)
1118 self._onContactsviewRowActivatedId = 0
1119 self._onAddressbookComboChangedId = 0
1120 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1121 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1123 self._updateSink = gtk_toolbox.threaded_stage(
1125 self._idly_populate_contactsview,
1126 gtk_toolbox.null_sink(),
1131 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1133 self._contactsview.set_model(self._contactsmodel)
1134 self._contactsview.append_column(self._contactColumn)
1135 self._contactsviewselection = self._contactsview.get_selection()
1136 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1138 self._booksList.clear()
1139 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1140 if factoryName and bookName:
1141 entryName = "%s: %s" % (factoryName, bookName)
1143 entryName = factoryName
1145 entryName = bookName
1147 entryName = "Bad name (%d)" % factoryId
1148 row = (str(factoryId), bookId, entryName)
1149 self._booksList.append(row)
1151 self._booksSelectionBox.set_model(self._booksList)
1152 cell = gtk.CellRendererText()
1153 self._booksSelectionBox.pack_start(cell, True)
1154 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1156 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1157 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1159 if len(self._booksList) <= self._selectedComboIndex:
1160 self._selectedComboIndex = 0
1161 self._booksSelectionBox.set_active(self._selectedComboIndex)
1164 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1165 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1169 self._booksSelectionBox.clear()
1170 self._booksSelectionBox.set_model(None)
1171 self._contactsview.set_model(None)
1172 self._contactsview.remove_column(self._contactColumn)
1174 def number_selected(self, action, number, message):
1176 @note Actual dial function is patched in later
1178 raise NotImplementedError("Horrible unknown error has occurred")
1180 def get_addressbooks(self):
1182 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1184 for i, factory in enumerate(self._addressBookFactories):
1185 for bookFactory, bookId, bookName in factory.get_addressbooks():
1186 yield (str(i), bookId), (factory.factory_name(), bookName)
1188 def open_addressbook(self, bookFactoryId, bookId):
1189 bookFactoryIndex = int(bookFactoryId)
1190 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1192 forceUpdate = True if addressBook is not self._addressBook else False
1194 self._addressBook = addressBook
1195 self.update(force=forceUpdate)
1197 def update(self, force = False):
1198 if not force and self._isPopulated:
1200 self._updateSink.send(())
1204 self._isPopulated = False
1205 self._contactsmodel.clear()
1206 for factory in self._addressBookFactories:
1207 factory.clear_caches()
1208 self._addressBook.clear_caches()
1210 def append(self, book):
1211 self._addressBookFactories.append(book)
1213 def extend(self, books):
1214 self._addressBookFactories.extend(books)
1220 def load_settings(self, config, sectionName):
1222 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1223 except ConfigParser.NoOptionError:
1224 self._selectedComboIndex = 0
1226 def save_settings(self, config, sectionName):
1227 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1229 def _idly_populate_contactsview(self):
1231 while addressBook is not self._addressBook:
1232 addressBook = self._addressBook
1233 with gtk_toolbox.gtk_lock():
1234 self._contactsview.set_model(None)
1238 contacts = addressBook.get_contacts()
1239 except StandardError, e:
1241 self._isPopulated = False
1242 self._errorDisplay.push_exception_with_lock()
1243 for contactId, contactName in contacts:
1244 contactType = (addressBook.contact_source_short_name(contactId), )
1245 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1247 with gtk_toolbox.gtk_lock():
1248 self._contactsview.set_model(self._contactsmodel)
1250 self._isPopulated = True
1253 def _on_addressbook_combo_changed(self, *args, **kwds):
1254 itr = self._booksSelectionBox.get_active_iter()
1257 self._selectedComboIndex = self._booksSelectionBox.get_active()
1258 selectedFactoryId = self._booksList.get_value(itr, 0)
1259 selectedBookId = self._booksList.get_value(itr, 1)
1260 self.open_addressbook(selectedFactoryId, selectedBookId)
1262 def _on_contactsview_row_activated(self, treeview, path, view_column):
1263 model, itr = self._contactsviewselection.get_selected()
1267 contactId = self._contactsmodel.get_value(itr, 3)
1268 contactName = self._contactsmodel.get_value(itr, 1)
1270 contactDetails = self._addressBook.get_contact_details(contactId)
1271 except StandardError, e:
1273 self._errorDisplay.push_exception()
1274 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1276 if len(contactPhoneNumbers) == 0:
1279 action, phoneNumber, message = self._phoneTypeSelector.run(
1280 contactPhoneNumbers,
1281 message = contactName,
1282 parent = self._window,
1284 if action == PhoneTypeSelector.ACTION_CANCEL:
1286 assert phoneNumber, "A lack of phone number exists"
1288 self.number_selected(action, phoneNumber, message)
1289 self._contactsviewselection.unselect_all()