Various bug fixes and tweaks found through 0, 1, and 2
[gc-dialer] / src / gv_views.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
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.
11
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.
16
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
20 """
21
22 from __future__ import with_statement
23
24 import ConfigParser
25 import warnings
26
27 import gobject
28 import pango
29 import gtk
30
31 import gtk_toolbox
32 import hildonize
33 import null_backend
34
35
36 def make_ugly(prettynumber):
37         """
38         function to take a phone number and strip out all non-numeric
39         characters
40
41         >>> make_ugly("+012-(345)-678-90")
42         '01234567890'
43         """
44         import re
45         uglynumber = re.sub('\D', '', prettynumber)
46         return uglynumber
47
48
49 def make_pretty(phonenumber):
50         """
51         Function to take a phone number and return the pretty version
52         pretty numbers:
53                 if phonenumber begins with 0:
54                         ...-(...)-...-....
55                 if phonenumber begins with 1: ( for gizmo callback numbers )
56                         1 (...)-...-....
57                 if phonenumber is 13 digits:
58                         (...)-...-....
59                 if phonenumber is 10 digits:
60                         ...-....
61         >>> make_pretty("12")
62         '12'
63         >>> make_pretty("1234567")
64         '123-4567'
65         >>> make_pretty("2345678901")
66         '(234)-567-8901'
67         >>> make_pretty("12345678901")
68         '1 (234)-567-8901'
69         >>> make_pretty("01234567890")
70         '+012-(345)-678-90'
71         """
72         if phonenumber is None or phonenumber is "":
73                 return ""
74
75         phonenumber = make_ugly(phonenumber)
76
77         if len(phonenumber) < 3:
78                 return phonenumber
79
80         if phonenumber[0] == "0":
81                 prettynumber = ""
82                 prettynumber += "+%s" % phonenumber[0:3]
83                 if 3 < len(phonenumber):
84                         prettynumber += "-(%s)" % phonenumber[3:6]
85                         if 6 < len(phonenumber):
86                                 prettynumber += "-%s" % phonenumber[6:9]
87                                 if 9 < len(phonenumber):
88                                         prettynumber += "-%s" % phonenumber[9:]
89                 return prettynumber
90         elif len(phonenumber) <= 7:
91                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
92         elif len(phonenumber) > 8 and phonenumber[0] == "1":
93                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
94         elif len(phonenumber) > 7:
95                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
96         return prettynumber
97
98
99 def abbrev_relative_date(date):
100         """
101         >>> abbrev_relative_date("42 hours ago")
102         '42 h'
103         >>> abbrev_relative_date("2 days ago")
104         '2 d'
105         >>> abbrev_relative_date("4 weeks ago")
106         '4 w'
107         """
108         parts = date.split(" ")
109         return "%s %s" % (parts[0], parts[1][0])
110
111
112 class MergedAddressBook(object):
113         """
114         Merger of all addressbooks
115         """
116
117         def __init__(self, addressbookFactories, sorter = None):
118                 self.__addressbookFactories = addressbookFactories
119                 self.__addressbooks = None
120                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
121
122         def clear_caches(self):
123                 self.__addressbooks = None
124                 for factory in self.__addressbookFactories:
125                         factory.clear_caches()
126
127         def get_addressbooks(self):
128                 """
129                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
130                 """
131                 yield self, "", ""
132
133         def open_addressbook(self, bookId):
134                 return self
135
136         def contact_source_short_name(self, contactId):
137                 if self.__addressbooks is None:
138                         return ""
139                 bookIndex, originalId = contactId.split("-", 1)
140                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
141
142         @staticmethod
143         def factory_name():
144                 return "All Contacts"
145
146         def get_contacts(self):
147                 """
148                 @returns Iterable of (contact id, contact name)
149                 """
150                 if self.__addressbooks is None:
151                         self.__addressbooks = list(
152                                 factory.open_addressbook(id)
153                                 for factory in self.__addressbookFactories
154                                 for (f, id, name) in factory.get_addressbooks()
155                         )
156                 contacts = (
157                         ("-".join([str(bookIndex), contactId]), contactName)
158                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
159                                         for (contactId, contactName) in addressbook.get_contacts()
160                 )
161                 sortedContacts = self.__sort_contacts(contacts)
162                 return sortedContacts
163
164         def get_contact_details(self, contactId):
165                 """
166                 @returns Iterable of (Phone Type, Phone Number)
167                 """
168                 if self.__addressbooks is None:
169                         return []
170                 bookIndex, originalId = contactId.split("-", 1)
171                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
172
173         @staticmethod
174         def null_sorter(contacts):
175                 """
176                 Good for speed/low memory
177                 """
178                 return contacts
179
180         @staticmethod
181         def basic_firtname_sorter(contacts):
182                 """
183                 Expects names in "First Last" format
184                 """
185                 contactsWithKey = [
186                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
187                                 for (contactId, contactName) in contacts
188                 ]
189                 contactsWithKey.sort()
190                 return (contactData for (lastName, contactData) in contactsWithKey)
191
192         @staticmethod
193         def basic_lastname_sorter(contacts):
194                 """
195                 Expects names in "First Last" format
196                 """
197                 contactsWithKey = [
198                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
199                                 for (contactId, contactName) in contacts
200                 ]
201                 contactsWithKey.sort()
202                 return (contactData for (lastName, contactData) in contactsWithKey)
203
204         @staticmethod
205         def reversed_firtname_sorter(contacts):
206                 """
207                 Expects names in "Last, First" format
208                 """
209                 contactsWithKey = [
210                         (contactName.split(", ", 1)[-1], (contactId, contactName))
211                                 for (contactId, contactName) in contacts
212                 ]
213                 contactsWithKey.sort()
214                 return (contactData for (lastName, contactData) in contactsWithKey)
215
216         @staticmethod
217         def reversed_lastname_sorter(contacts):
218                 """
219                 Expects names in "Last, First" format
220                 """
221                 contactsWithKey = [
222                         (contactName.split(", ", 1)[0], (contactId, contactName))
223                                 for (contactId, contactName) in contacts
224                 ]
225                 contactsWithKey.sort()
226                 return (contactData for (lastName, contactData) in contactsWithKey)
227
228         @staticmethod
229         def guess_firstname(name):
230                 if ", " in name:
231                         return name.split(", ", 1)[-1]
232                 else:
233                         return name.rsplit(" ", 1)[0]
234
235         @staticmethod
236         def guess_lastname(name):
237                 if ", " in name:
238                         return name.split(", ", 1)[0]
239                 else:
240                         return name.rsplit(" ", 1)[-1]
241
242         @classmethod
243         def advanced_firstname_sorter(cls, contacts):
244                 contactsWithKey = [
245                         (cls.guess_firstname(contactName), (contactId, contactName))
246                                 for (contactId, contactName) in contacts
247                 ]
248                 contactsWithKey.sort()
249                 return (contactData for (lastName, contactData) in contactsWithKey)
250
251         @classmethod
252         def advanced_lastname_sorter(cls, contacts):
253                 contactsWithKey = [
254                         (cls.guess_lastname(contactName), (contactId, contactName))
255                                 for (contactId, contactName) in contacts
256                 ]
257                 contactsWithKey.sort()
258                 return (contactData for (lastName, contactData) in contactsWithKey)
259
260
261 class PhoneTypeSelector(object):
262
263         ACTION_CANCEL = "cancel"
264         ACTION_SELECT = "select"
265         ACTION_DIAL = "dial"
266         ACTION_SEND_SMS = "sms"
267
268         def __init__(self, widgetTree, gcBackend):
269                 self._gcBackend = gcBackend
270                 self._widgetTree = widgetTree
271
272                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
273                 self._smsDialog = SmsEntryDialog(self._widgetTree)
274
275                 self._smsButton = self._widgetTree.get_widget("sms_button")
276                 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
277
278                 self._dialButton = self._widgetTree.get_widget("dial_button")
279                 self._dialButton.connect("clicked", self._on_phonetype_dial)
280
281                 self._selectButton = self._widgetTree.get_widget("select_button")
282                 self._selectButton.connect("clicked", self._on_phonetype_select)
283
284                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
285                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
286
287                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
288                 self._typeviewselection = None
289
290                 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
291                 self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
292                 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
293                 self._typeview = self._widgetTree.get_widget("phonetypes")
294                 self._typeview.connect("row-activated", self._on_phonetype_select)
295
296                 self._action = self.ACTION_CANCEL
297
298         def run(self, contactDetails, message = "", parent = None):
299                 self._action = self.ACTION_CANCEL
300                 self._typemodel.clear()
301                 self._typeview.set_model(self._typemodel)
302
303                 # Add the column to the treeview
304                 textrenderer = gtk.CellRendererText()
305                 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
306                 self._typeview.append_column(numberColumn)
307
308                 textrenderer = gtk.CellRendererText()
309                 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
310                 self._typeview.append_column(typeColumn)
311
312                 self._typeviewselection = self._typeview.get_selection()
313                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
314
315                 for phoneType, phoneNumber in contactDetails:
316                         display = " - ".join((phoneNumber, phoneType))
317                         display = phoneType
318                         row = (phoneNumber, display)
319                         self._typemodel.append(row)
320
321                 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
322                 if message:
323                         self._message.set_markup(message)
324                         self._message.show()
325                 else:
326                         self._message.set_markup("")
327                         self._message.hide()
328
329                 if parent is not None:
330                         self._dialog.set_transient_for(parent)
331
332                 try:
333                         self._dialog.show()
334                         adjustment = self._scrollWindow.get_vadjustment()
335                         dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
336                         dx = max(dx, 0)
337                         adjustment.value = dx
338
339                         userResponse = self._dialog.run()
340                 finally:
341                         self._dialog.hide()
342
343                 if userResponse == gtk.RESPONSE_OK:
344                         phoneNumber = self._get_number()
345                         phoneNumber = make_ugly(phoneNumber)
346                 else:
347                         phoneNumber = ""
348                 if not phoneNumber:
349                         self._action = self.ACTION_CANCEL
350
351                 if self._action == self.ACTION_SEND_SMS:
352                         smsMessage = self._smsDialog.run(phoneNumber, message, parent)
353                         if not smsMessage:
354                                 phoneNumber = ""
355                                 self._action = self.ACTION_CANCEL
356                 else:
357                         smsMessage = ""
358
359                 self._typeviewselection.unselect_all()
360                 self._typeview.remove_column(numberColumn)
361                 self._typeview.remove_column(typeColumn)
362                 self._typeview.set_model(None)
363
364                 return self._action, phoneNumber, smsMessage
365
366         def _get_number(self):
367                 model, itr = self._typeviewselection.get_selected()
368                 if not itr:
369                         return ""
370
371                 phoneNumber = self._typemodel.get_value(itr, 0)
372                 return phoneNumber
373
374         def _on_phonetype_dial(self, *args):
375                 self._dialog.response(gtk.RESPONSE_OK)
376                 self._action = self.ACTION_DIAL
377
378         def _on_phonetype_send_sms(self, *args):
379                 self._dialog.response(gtk.RESPONSE_OK)
380                 self._action = self.ACTION_SEND_SMS
381
382         def _on_phonetype_select(self, *args):
383                 self._dialog.response(gtk.RESPONSE_OK)
384                 self._action = self.ACTION_SELECT
385
386         def _on_phonetype_cancel(self, *args):
387                 self._dialog.response(gtk.RESPONSE_CANCEL)
388                 self._action = self.ACTION_CANCEL
389
390
391 class SmsEntryDialog(object):
392
393         """
394         @todo Add multi-SMS messages like GoogleVoice
395         """
396
397         MAX_CHAR = 160
398
399         def __init__(self, widgetTree):
400                 self._widgetTree = widgetTree
401                 self._dialog = self._widgetTree.get_widget("smsDialog")
402
403                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
404                 self._smsButton.connect("clicked", self._on_send)
405
406                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
407                 self._cancelButton.connect("clicked", self._on_cancel)
408
409                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
410                 self._message = self._widgetTree.get_widget("smsMessage")
411                 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
412                 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
413                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
414                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
415
416         def run(self, number, message = "", parent = None):
417                 if message:
418                         self._message.set_markup(message)
419                         self._message.show()
420                 else:
421                         self._message.set_markup("")
422                         self._message.hide()
423                 self._smsEntry.get_buffer().set_text("")
424                 self._update_letter_count()
425
426                 if parent is not None:
427                         self._dialog.set_transient_for(parent)
428
429                 try:
430                         self._dialog.show()
431                         adjustment = self._scrollWindow.get_vadjustment()
432                         dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
433                         dx = max(dx, 0)
434                         adjustment.value = dx
435
436                         userResponse = self._dialog.run()
437                 finally:
438                         self._dialog.hide()
439
440                 if userResponse == gtk.RESPONSE_OK:
441                         entryBuffer = self._smsEntry.get_buffer()
442                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
443                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
444                 else:
445                         enteredMessage = ""
446
447                 return enteredMessage.strip()
448
449         def _update_letter_count(self, *args):
450                 entryLength = self._smsEntry.get_buffer().get_char_count()
451                 charsLeft = self.MAX_CHAR - entryLength
452                 self._letterCountLabel.set_text(str(charsLeft))
453                 if charsLeft < 0:
454                         self._smsButton.set_sensitive(False)
455                 else:
456                         self._smsButton.set_sensitive(True)
457
458         def _on_entry_changed(self, *args):
459                 self._update_letter_count()
460
461         def _on_send(self, *args):
462                 self._dialog.response(gtk.RESPONSE_OK)
463
464         def _on_cancel(self, *args):
465                 self._dialog.response(gtk.RESPONSE_CANCEL)
466
467
468 class Dialpad(object):
469
470         def __init__(self, widgetTree, errorDisplay):
471                 self._errorDisplay = errorDisplay
472                 self._smsDialog = SmsEntryDialog(widgetTree)
473
474                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
475                 self._dialButton = widgetTree.get_widget("dial")
476                 self._backButton = widgetTree.get_widget("back")
477                 self._phonenumber = ""
478                 self._prettynumber = ""
479
480                 callbackMapping = {
481                         "on_dial_clicked": self._on_dial_clicked,
482                         "on_sms_clicked": self._on_sms_clicked,
483                         "on_digit_clicked": self._on_digit_clicked,
484                         "on_clear_number": self._on_clear_number,
485                 }
486                 widgetTree.signal_autoconnect(callbackMapping)
487
488                 self._originalLabel = self._backButton.get_label()
489                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
490                 self._backTapHandler.on_tap = self._on_backspace
491                 self._backTapHandler.on_hold = self._on_clearall
492                 self._backTapHandler.on_holding = self._set_clear_button
493                 self._backTapHandler.on_cancel = self._reset_back_button
494
495                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
496
497         def enable(self):
498                 self._dialButton.grab_focus()
499                 self._backTapHandler.enable()
500
501         def disable(self):
502                 self._reset_back_button()
503                 self._backTapHandler.disable()
504
505         def number_selected(self, action, number, message):
506                 """
507                 @note Actual dial function is patched in later
508                 """
509                 raise NotImplementedError("Horrible unknown error has occurred")
510
511         def get_number(self):
512                 return self._phonenumber
513
514         def set_number(self, number):
515                 """
516                 Set the number to dial
517                 """
518                 try:
519                         self._phonenumber = make_ugly(number)
520                         self._prettynumber = make_pretty(self._phonenumber)
521                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
522                 except TypeError, e:
523                         self._errorDisplay.push_exception()
524
525         def clear(self):
526                 self.set_number("")
527
528         @staticmethod
529         def name():
530                 return "Dialpad"
531
532         def load_settings(self, config, section):
533                 pass
534
535         def save_settings(self, config, section):
536                 """
537                 @note Thread Agnostic
538                 """
539                 pass
540
541         def _on_sms_clicked(self, widget):
542                 action = PhoneTypeSelector.ACTION_SEND_SMS
543                 phoneNumber = self.get_number()
544
545                 message = self._smsDialog.run(phoneNumber, "", self._window)
546                 if not message:
547                         phoneNumber = ""
548                         action = PhoneTypeSelector.ACTION_CANCEL
549
550                 if action == PhoneTypeSelector.ACTION_CANCEL:
551                         return
552                 self.number_selected(action, phoneNumber, message)
553
554         def _on_dial_clicked(self, widget):
555                 action = PhoneTypeSelector.ACTION_DIAL
556                 phoneNumber = self.get_number()
557                 message = ""
558                 self.number_selected(action, phoneNumber, message)
559
560         def _on_clear_number(self, *args):
561                 self.clear()
562
563         def _on_digit_clicked(self, widget):
564                 self.set_number(self._phonenumber + widget.get_name()[-1])
565
566         def _on_backspace(self, taps):
567                 self.set_number(self._phonenumber[:-taps])
568                 self._reset_back_button()
569
570         def _on_clearall(self, taps):
571                 self.clear()
572                 self._reset_back_button()
573                 return False
574
575         def _set_clear_button(self):
576                 self._backButton.set_label("gtk-clear")
577
578         def _reset_back_button(self):
579                 self._backButton.set_label(self._originalLabel)
580
581
582 class AccountInfo(object):
583
584         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
585                 self._errorDisplay = errorDisplay
586                 self._backend = backend
587                 self._isPopulated = False
588                 self._alarmHandler = alarmHandler
589                 self._notifyOnMissed = False
590                 self._notifyOnVoicemail = False
591                 self._notifyOnSms = False
592
593                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
594                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
595                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
596                 self._onCallbackentryChangedId = 0
597
598                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
599                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
600                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
601                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
602                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
603                 self._onNotifyToggled = 0
604                 self._onMinutesChanged = 0
605                 self._onMissedToggled = 0
606                 self._onVoicemailToggled = 0
607                 self._onSmsToggled = 0
608                 self._applyAlarmTimeoutId = None
609
610                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
611                 self._defaultCallback = ""
612
613         def enable(self):
614                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
615
616                 self._accountViewNumberDisplay.set_use_markup(True)
617                 self.set_account_number("")
618
619                 self._callbackList.clear()
620                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
621
622                 if self._alarmHandler is not None:
623                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
624                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
625                         self._missedCheckbox.set_active(self._notifyOnMissed)
626                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
627                         self._smsCheckbox.set_active(self._notifyOnSms)
628
629                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
630                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_changed)
631                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
632                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
633                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
634                 else:
635                         self._notifyCheckbox.set_sensitive(False)
636                         self._minutesEntryButton.set_sensitive(False)
637                         self._missedCheckbox.set_sensitive(False)
638                         self._voicemailCheckbox.set_sensitive(False)
639                         self._smsCheckbox.set_sensitive(False)
640
641                 self.update(force=True)
642
643         def disable(self):
644                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
645                 self._onCallbackentryChangedId = 0
646
647                 if self._alarmHandler is not None:
648                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
649                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
650                         self._missedCheckbox.disconnect(self._onNotifyToggled)
651                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
652                         self._smsCheckbox.disconnect(self._onNotifyToggled)
653                         self._onNotifyToggled = 0
654                         self._onMinutesChanged = 0
655                         self._onMissedToggled = 0
656                         self._onVoicemailToggled = 0
657                         self._onSmsToggled = 0
658                 else:
659                         self._notifyCheckbox.set_sensitive(True)
660                         self._minutesEntryButton.set_sensitive(True)
661                         self._missedCheckbox.set_sensitive(True)
662                         self._voicemailCheckbox.set_sensitive(True)
663                         self._smsCheckbox.set_sensitive(True)
664
665                 self.clear()
666                 self._callbackList.clear()
667
668         def get_selected_callback_number(self):
669                 return make_ugly(self._callbackCombo.get_child().get_text())
670
671         def set_account_number(self, number):
672                 """
673                 Displays current account number
674                 """
675                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
676
677         def update(self, force = False):
678                 if not force and self._isPopulated:
679                         return False
680                 self._populate_callback_combo()
681                 self.set_account_number(self._backend.get_account_number())
682                 return True
683
684         def clear(self):
685                 self._callbackCombo.get_child().set_text("")
686                 self.set_account_number("")
687                 self._isPopulated = False
688
689         def save_everything(self):
690                 raise NotImplementedError
691
692         @staticmethod
693         def name():
694                 return "Account Info"
695
696         def load_settings(self, config, section):
697                 self._defaultCallback = config.get(section, "callback")
698                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
699                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
700                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
701
702         def save_settings(self, config, section):
703                 """
704                 @note Thread Agnostic
705                 """
706                 callback = self.get_selected_callback_number()
707                 config.set(section, "callback", callback)
708                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
709                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
710                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
711
712         def _populate_callback_combo(self):
713                 self._isPopulated = True
714                 self._callbackList.clear()
715                 try:
716                         callbackNumbers = self._backend.get_callback_numbers()
717                 except StandardError, e:
718                         self._errorDisplay.push_exception()
719                         self._isPopulated = False
720                         return
721
722                 for number, description in callbackNumbers.iteritems():
723                         self._callbackList.append((make_pretty(number),))
724
725                 self._callbackCombo.set_model(self._callbackList)
726                 self._callbackCombo.set_text_column(0)
727                 #callbackNumber = self._backend.get_callback_number()
728                 callbackNumber = self._defaultCallback
729                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
730
731         def _set_callback_number(self, number):
732                 try:
733                         if not self._backend.is_valid_syntax(number):
734                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
735                         elif number == self._backend.get_callback_number():
736                                 warnings.warn(
737                                         "Callback number already is %s" % (
738                                                 self._backend.get_callback_number(),
739                                         ),
740                                         UserWarning,
741                                         2
742                                 )
743                         else:
744                                 self._backend.set_callback_number(number)
745                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
746                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
747                                 )
748                                 warnings.warn(
749                                         "Callback number set to %s" % (
750                                                 self._backend.get_callback_number(),
751                                         ),
752                                         UserWarning, 2
753                                 )
754                 except StandardError, e:
755                         self._errorDisplay.push_exception()
756
757         def _update_alarm_settings(self, recurrence):
758                 try:
759                         isEnabled = self._notifyCheckbox.get_active()
760                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
761                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
762                 finally:
763                         self.save_everything()
764                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
765                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
766
767         def _on_callbackentry_changed(self, *args):
768                 text = self.get_selected_callback_number()
769                 number = make_ugly(text)
770                 self._set_callback_number(number)
771
772         def _on_notify_toggled(self, *args):
773                 if self._applyAlarmTimeoutId is not None:
774                         gobject.source_remove(self._applyAlarmTimeoutId)
775                         self._applyAlarmTimeoutId = None
776                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
777
778         def _on_minutes_changed(self, *args):
779                 recurrence = hildonize.request_number(
780                         self._window, "Minutes", (1, 50), self._alarmHandler.recurrence
781                 )
782                 self._update_alarm_settings(recurrence)
783
784         def _on_apply_timeout(self, *args):
785                 self._applyAlarmTimeoutId = None
786
787                 self._update_alarm_settings(self._alarmHandler.recurrence)
788                 return False
789
790         def _on_missed_toggled(self, *args):
791                 self._notifyOnMissed = self._missedCheckbox.get_active()
792                 self.save_everything()
793
794         def _on_voicemail_toggled(self, *args):
795                 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
796                 self.save_everything()
797
798         def _on_sms_toggled(self, *args):
799                 self._notifyOnSms = self._smsCheckbox.get_active()
800                 self.save_everything()
801
802
803 class RecentCallsView(object):
804
805         NUMBER_IDX = 0
806         DATE_IDX = 1
807         ACTION_IDX = 2
808         FROM_IDX = 3
809
810         def __init__(self, widgetTree, backend, errorDisplay):
811                 self._errorDisplay = errorDisplay
812                 self._backend = backend
813
814                 self._isPopulated = False
815                 self._recentmodel = gtk.ListStore(
816                         gobject.TYPE_STRING, # number
817                         gobject.TYPE_STRING, # date
818                         gobject.TYPE_STRING, # action
819                         gobject.TYPE_STRING, # from
820                 )
821                 self._recentview = widgetTree.get_widget("recentview")
822                 self._recentviewselection = None
823                 self._onRecentviewRowActivatedId = 0
824
825                 textrenderer = gtk.CellRendererText()
826                 textrenderer.set_property("yalign", 0)
827                 self._dateColumn = gtk.TreeViewColumn("Date")
828                 self._dateColumn.pack_start(textrenderer, expand=True)
829                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
830
831                 textrenderer = gtk.CellRendererText()
832                 textrenderer.set_property("yalign", 0)
833                 self._actionColumn = gtk.TreeViewColumn("Action")
834                 self._actionColumn.pack_start(textrenderer, expand=True)
835                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
836
837                 textrenderer = gtk.CellRendererText()
838                 textrenderer.set_property("yalign", 0)
839                 hildonize.set_cell_thumb_selectable(textrenderer)
840                 self._nameColumn = gtk.TreeViewColumn("From")
841                 self._nameColumn.pack_start(textrenderer, expand=True)
842                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
843                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
844
845                 textrenderer = gtk.CellRendererText()
846                 textrenderer.set_property("yalign", 0)
847                 hildonize.set_cell_thumb_selectable(textrenderer)
848                 self._numberColumn = gtk.TreeViewColumn("Number")
849                 self._numberColumn.pack_start(textrenderer, expand=True)
850                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
851
852                 self._window = gtk_toolbox.find_parent_window(self._recentview)
853                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
854
855                 self._updateSink = gtk_toolbox.threaded_stage(
856                         gtk_toolbox.comap(
857                                 self._idly_populate_recentview,
858                                 gtk_toolbox.null_sink(),
859                         )
860                 )
861
862         def enable(self):
863                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
864                 self._recentview.set_model(self._recentmodel)
865
866                 self._recentview.append_column(self._dateColumn)
867                 self._recentview.append_column(self._actionColumn)
868                 self._recentview.append_column(self._numberColumn)
869                 self._recentview.append_column(self._nameColumn)
870                 self._recentviewselection = self._recentview.get_selection()
871                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
872
873                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
874
875         def disable(self):
876                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
877
878                 self.clear()
879
880                 self._recentview.remove_column(self._dateColumn)
881                 self._recentview.remove_column(self._actionColumn)
882                 self._recentview.remove_column(self._nameColumn)
883                 self._recentview.remove_column(self._numberColumn)
884                 self._recentview.set_model(None)
885
886         def number_selected(self, action, number, message):
887                 """
888                 @note Actual dial function is patched in later
889                 """
890                 raise NotImplementedError("Horrible unknown error has occurred")
891
892         def update(self, force = False):
893                 if not force and self._isPopulated:
894                         return False
895                 self._updateSink.send(())
896                 return True
897
898         def clear(self):
899                 self._isPopulated = False
900                 self._recentmodel.clear()
901
902         @staticmethod
903         def name():
904                 return "Recent Calls"
905
906         def load_settings(self, config, section):
907                 pass
908
909         def save_settings(self, config, section):
910                 """
911                 @note Thread Agnostic
912                 """
913                 pass
914
915         def _idly_populate_recentview(self):
916                 self._recentmodel.clear()
917                 self._isPopulated = True
918
919                 try:
920                         recentItems = self._backend.get_recent()
921                 except StandardError, e:
922                         self._errorDisplay.push_exception_with_lock()
923                         self._isPopulated = False
924                         recentItems = []
925
926                 for personName, phoneNumber, date, action in recentItems:
927                         if not personName:
928                                 personName = "Unknown"
929                         date = abbrev_relative_date(date)
930                         prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
931                         prettyNumber = make_pretty(prettyNumber)
932                         item = (prettyNumber, date, action.capitalize(), personName)
933                         with gtk_toolbox.gtk_lock():
934                                 self._recentmodel.append(item)
935
936                 return False
937
938         def _on_recentview_row_activated(self, treeview, path, view_column):
939                 model, itr = self._recentviewselection.get_selected()
940                 if not itr:
941                         return
942
943                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
944                 number = make_ugly(number)
945                 contactPhoneNumbers = [("Phone", number)]
946                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
947
948                 action, phoneNumber, message = self._phoneTypeSelector.run(
949                         contactPhoneNumbers,
950                         message = description,
951                         parent = self._window,
952                 )
953                 if action == PhoneTypeSelector.ACTION_CANCEL:
954                         return
955                 assert phoneNumber, "A lack of phone number exists"
956
957                 self.number_selected(action, phoneNumber, message)
958                 self._recentviewselection.unselect_all()
959
960
961 class MessagesView(object):
962
963         NUMBER_IDX = 0
964         DATE_IDX = 1
965         HEADER_IDX = 2
966         MESSAGE_IDX = 3
967
968         def __init__(self, widgetTree, backend, errorDisplay):
969                 self._errorDisplay = errorDisplay
970                 self._backend = backend
971
972                 self._isPopulated = False
973                 self._messagemodel = gtk.ListStore(
974                         gobject.TYPE_STRING, # number
975                         gobject.TYPE_STRING, # date
976                         gobject.TYPE_STRING, # header
977                         gobject.TYPE_STRING, # message
978                 )
979                 self._messageview = widgetTree.get_widget("messages_view")
980                 self._messageviewselection = None
981                 self._onMessageviewRowActivatedId = 0
982
983                 self._messageRenderer = gtk.CellRendererText()
984                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
985                 self._messageRenderer.set_property("wrap-width", 500)
986                 self._messageColumn = gtk.TreeViewColumn("Messages")
987                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
988                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
989                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
990
991                 self._window = gtk_toolbox.find_parent_window(self._messageview)
992                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
993
994                 self._updateSink = gtk_toolbox.threaded_stage(
995                         gtk_toolbox.comap(
996                                 self._idly_populate_messageview,
997                                 gtk_toolbox.null_sink(),
998                         )
999                 )
1000
1001         def enable(self):
1002                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1003                 self._messageview.set_model(self._messagemodel)
1004
1005                 self._messageview.append_column(self._messageColumn)
1006                 self._messageviewselection = self._messageview.get_selection()
1007                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1008
1009                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1010
1011         def disable(self):
1012                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1013
1014                 self.clear()
1015
1016                 self._messageview.remove_column(self._messageColumn)
1017                 self._messageview.set_model(None)
1018
1019         def number_selected(self, action, number, message):
1020                 """
1021                 @note Actual dial function is patched in later
1022                 """
1023                 raise NotImplementedError("Horrible unknown error has occurred")
1024
1025         def update(self, force = False):
1026                 if not force and self._isPopulated:
1027                         return False
1028                 self._updateSink.send(())
1029                 return True
1030
1031         def clear(self):
1032                 self._isPopulated = False
1033                 self._messagemodel.clear()
1034
1035         @staticmethod
1036         def name():
1037                 return "Messages"
1038
1039         def load_settings(self, config, section):
1040                 pass
1041
1042         def save_settings(self, config, section):
1043                 """
1044                 @note Thread Agnostic
1045                 """
1046                 pass
1047
1048         def _idly_populate_messageview(self):
1049                 self._messagemodel.clear()
1050                 self._isPopulated = True
1051
1052                 try:
1053                         messageItems = self._backend.get_messages()
1054                 except StandardError, e:
1055                         self._errorDisplay.push_exception_with_lock()
1056                         self._isPopulated = False
1057                         messageItems = []
1058
1059                 for header, number, relativeDate, message in messageItems:
1060                         prettyNumber = number[2:] if number.startswith("+1") else number
1061                         prettyNumber = make_pretty(prettyNumber)
1062                         message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1063                         number = make_ugly(number)
1064                         row = (number, relativeDate, header, message)
1065                         with gtk_toolbox.gtk_lock():
1066                                 self._messagemodel.append(row)
1067
1068                 return False
1069
1070         def _on_messageview_row_activated(self, treeview, path, view_column):
1071                 model, itr = self._messageviewselection.get_selected()
1072                 if not itr:
1073                         return
1074
1075                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1076                 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1077
1078                 action, phoneNumber, message = self._phoneTypeSelector.run(
1079                         contactPhoneNumbers,
1080                         message = description,
1081                         parent = self._window,
1082                 )
1083                 if action == PhoneTypeSelector.ACTION_CANCEL:
1084                         return
1085                 assert phoneNumber, "A lock of phone number exists"
1086
1087                 self.number_selected(action, phoneNumber, message)
1088                 self._messageviewselection.unselect_all()
1089
1090
1091 class ContactsView(object):
1092
1093         def __init__(self, widgetTree, backend, errorDisplay):
1094                 self._errorDisplay = errorDisplay
1095                 self._backend = backend
1096
1097                 self._addressBook = None
1098                 self._selectedComboIndex = 0
1099                 self._addressBookFactories = [null_backend.NullAddressBook()]
1100
1101                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1102                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1103
1104                 self._isPopulated = False
1105                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1106                 self._contactsviewselection = None
1107                 self._contactsview = widgetTree.get_widget("contactsview")
1108
1109                 self._contactColumn = gtk.TreeViewColumn("Contact")
1110                 displayContactSource = False
1111                 if displayContactSource:
1112                         textrenderer = gtk.CellRendererText()
1113                         self._contactColumn.pack_start(textrenderer, expand=False)
1114                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1115                 textrenderer = gtk.CellRendererText()
1116                 hildonize.set_cell_thumb_selectable(textrenderer)
1117                 self._contactColumn.pack_start(textrenderer, expand=True)
1118                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1119                 textrenderer = gtk.CellRendererText()
1120                 self._contactColumn.pack_start(textrenderer, expand=True)
1121                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1122                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1123                 self._contactColumn.set_sort_column_id(1)
1124                 self._contactColumn.set_visible(True)
1125
1126                 self._onContactsviewRowActivatedId = 0
1127                 self._onAddressbookComboChangedId = 0
1128                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1129                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1130
1131                 self._updateSink = gtk_toolbox.threaded_stage(
1132                         gtk_toolbox.comap(
1133                                 self._idly_populate_contactsview,
1134                                 gtk_toolbox.null_sink(),
1135                         )
1136                 )
1137
1138         def enable(self):
1139                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1140
1141                 self._contactsview.set_model(self._contactsmodel)
1142                 self._contactsview.append_column(self._contactColumn)
1143                 self._contactsviewselection = self._contactsview.get_selection()
1144                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1145
1146                 self._booksList.clear()
1147                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1148                         if factoryName and bookName:
1149                                 entryName = "%s: %s" % (factoryName, bookName)
1150                         elif factoryName:
1151                                 entryName = factoryName
1152                         elif bookName:
1153                                 entryName = bookName
1154                         else:
1155                                 entryName = "Bad name (%d)" % factoryId
1156                         row = (str(factoryId), bookId, entryName)
1157                         self._booksList.append(row)
1158
1159                 self._booksSelectionBox.set_model(self._booksList)
1160                 cell = gtk.CellRendererText()
1161                 self._booksSelectionBox.pack_start(cell, True)
1162                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1163
1164                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1165                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1166
1167                 if len(self._booksList) <= self._selectedComboIndex:
1168                         self._selectedComboIndex = 0
1169                 self._booksSelectionBox.set_active(self._selectedComboIndex)
1170
1171         def disable(self):
1172                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1173                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1174
1175                 self.clear()
1176
1177                 self._booksSelectionBox.clear()
1178                 self._booksSelectionBox.set_model(None)
1179                 self._contactsview.set_model(None)
1180                 self._contactsview.remove_column(self._contactColumn)
1181
1182         def number_selected(self, action, number, message):
1183                 """
1184                 @note Actual dial function is patched in later
1185                 """
1186                 raise NotImplementedError("Horrible unknown error has occurred")
1187
1188         def get_addressbooks(self):
1189                 """
1190                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1191                 """
1192                 for i, factory in enumerate(self._addressBookFactories):
1193                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1194                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1195
1196         def open_addressbook(self, bookFactoryId, bookId):
1197                 bookFactoryIndex = int(bookFactoryId)
1198                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1199
1200                 forceUpdate = True if addressBook is not self._addressBook else False
1201
1202                 self._addressBook = addressBook
1203                 self.update(force=forceUpdate)
1204
1205         def update(self, force = False):
1206                 if not force and self._isPopulated:
1207                         return False
1208                 self._updateSink.send(())
1209                 return True
1210
1211         def clear(self):
1212                 self._isPopulated = False
1213                 self._contactsmodel.clear()
1214                 for factory in self._addressBookFactories:
1215                         factory.clear_caches()
1216                 self._addressBook.clear_caches()
1217
1218         def append(self, book):
1219                 self._addressBookFactories.append(book)
1220
1221         def extend(self, books):
1222                 self._addressBookFactories.extend(books)
1223
1224         @staticmethod
1225         def name():
1226                 return "Contacts"
1227
1228         def load_settings(self, config, sectionName):
1229                 try:
1230                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1231                 except ConfigParser.NoOptionError:
1232                         self._selectedComboIndex = 0
1233
1234         def save_settings(self, config, sectionName):
1235                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1236
1237         def _idly_populate_contactsview(self):
1238                 addressBook = None
1239                 while addressBook is not self._addressBook:
1240                         addressBook = self._addressBook
1241                         with gtk_toolbox.gtk_lock():
1242                                 self._contactsview.set_model(None)
1243                                 self.clear()
1244
1245                         try:
1246                                 contacts = addressBook.get_contacts()
1247                         except StandardError, e:
1248                                 contacts = []
1249                                 self._isPopulated = False
1250                                 self._errorDisplay.push_exception_with_lock()
1251                         for contactId, contactName in contacts:
1252                                 contactType = (addressBook.contact_source_short_name(contactId), )
1253                                 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1254
1255                         with gtk_toolbox.gtk_lock():
1256                                 self._contactsview.set_model(self._contactsmodel)
1257
1258                 self._isPopulated = True
1259                 return False
1260
1261         def _on_addressbook_combo_changed(self, *args, **kwds):
1262                 itr = self._booksSelectionBox.get_active_iter()
1263                 if itr is None:
1264                         return
1265                 self._selectedComboIndex = self._booksSelectionBox.get_active()
1266                 selectedFactoryId = self._booksList.get_value(itr, 0)
1267                 selectedBookId = self._booksList.get_value(itr, 1)
1268                 self.open_addressbook(selectedFactoryId, selectedBookId)
1269
1270         def _on_contactsview_row_activated(self, treeview, path, view_column):
1271                 model, itr = self._contactsviewselection.get_selected()
1272                 if not itr:
1273                         return
1274
1275                 contactId = self._contactsmodel.get_value(itr, 3)
1276                 contactName = self._contactsmodel.get_value(itr, 1)
1277                 try:
1278                         contactDetails = self._addressBook.get_contact_details(contactId)
1279                 except StandardError, e:
1280                         contactDetails = []
1281                         self._errorDisplay.push_exception()
1282                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1283
1284                 if len(contactPhoneNumbers) == 0:
1285                         return
1286
1287                 action, phoneNumber, message = self._phoneTypeSelector.run(
1288                         contactPhoneNumbers,
1289                         message = contactName,
1290                         parent = self._window,
1291                 )
1292                 if action == PhoneTypeSelector.ACTION_CANCEL:
1293                         return
1294                 assert phoneNumber, "A lack of phone number exists"
1295
1296                 self.number_selected(action, phoneNumber, message)
1297                 self._contactsviewselection.unselect_all()