Adding logging support
[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 logging
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                                 logging.warning(
737                                         "Callback number already is %s" % (
738                                                 self._backend.get_callback_number(),
739                                         ),
740                                 )
741                         else:
742                                 self._backend.set_callback_number(number)
743                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
744                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
745                                 )
746                                 logging.info(
747                                         "Callback number set to %s" % (
748                                                 self._backend.get_callback_number(),
749                                         ),
750                                 )
751                 except StandardError, e:
752                         self._errorDisplay.push_exception()
753
754         def _update_alarm_settings(self, recurrence):
755                 try:
756                         isEnabled = self._notifyCheckbox.get_active()
757                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
758                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
759                 finally:
760                         self.save_everything()
761                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
762                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
763
764         def _on_callbackentry_changed(self, *args):
765                 text = self.get_selected_callback_number()
766                 number = make_ugly(text)
767                 self._set_callback_number(number)
768
769         def _on_notify_toggled(self, *args):
770                 if self._applyAlarmTimeoutId is not None:
771                         gobject.source_remove(self._applyAlarmTimeoutId)
772                         self._applyAlarmTimeoutId = None
773                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
774
775         def _on_minutes_changed(self, *args):
776                 recurrence = hildonize.request_number(
777                         self._window, "Minutes", (1, 50), self._alarmHandler.recurrence
778                 )
779                 self._update_alarm_settings(recurrence)
780
781         def _on_apply_timeout(self, *args):
782                 self._applyAlarmTimeoutId = None
783
784                 self._update_alarm_settings(self._alarmHandler.recurrence)
785                 return False
786
787         def _on_missed_toggled(self, *args):
788                 self._notifyOnMissed = self._missedCheckbox.get_active()
789                 self.save_everything()
790
791         def _on_voicemail_toggled(self, *args):
792                 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
793                 self.save_everything()
794
795         def _on_sms_toggled(self, *args):
796                 self._notifyOnSms = self._smsCheckbox.get_active()
797                 self.save_everything()
798
799
800 class RecentCallsView(object):
801
802         NUMBER_IDX = 0
803         DATE_IDX = 1
804         ACTION_IDX = 2
805         FROM_IDX = 3
806
807         def __init__(self, widgetTree, backend, errorDisplay):
808                 self._errorDisplay = errorDisplay
809                 self._backend = backend
810
811                 self._isPopulated = False
812                 self._recentmodel = gtk.ListStore(
813                         gobject.TYPE_STRING, # number
814                         gobject.TYPE_STRING, # date
815                         gobject.TYPE_STRING, # action
816                         gobject.TYPE_STRING, # from
817                 )
818                 self._recentview = widgetTree.get_widget("recentview")
819                 self._recentviewselection = None
820                 self._onRecentviewRowActivatedId = 0
821
822                 textrenderer = gtk.CellRendererText()
823                 textrenderer.set_property("yalign", 0)
824                 self._dateColumn = gtk.TreeViewColumn("Date")
825                 self._dateColumn.pack_start(textrenderer, expand=True)
826                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
827
828                 textrenderer = gtk.CellRendererText()
829                 textrenderer.set_property("yalign", 0)
830                 self._actionColumn = gtk.TreeViewColumn("Action")
831                 self._actionColumn.pack_start(textrenderer, expand=True)
832                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
833
834                 textrenderer = gtk.CellRendererText()
835                 textrenderer.set_property("yalign", 0)
836                 hildonize.set_cell_thumb_selectable(textrenderer)
837                 self._nameColumn = gtk.TreeViewColumn("From")
838                 self._nameColumn.pack_start(textrenderer, expand=True)
839                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
840                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
841
842                 textrenderer = gtk.CellRendererText()
843                 textrenderer.set_property("yalign", 0)
844                 hildonize.set_cell_thumb_selectable(textrenderer)
845                 self._numberColumn = gtk.TreeViewColumn("Number")
846                 self._numberColumn.pack_start(textrenderer, expand=True)
847                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
848
849                 self._window = gtk_toolbox.find_parent_window(self._recentview)
850                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
851
852                 self._updateSink = gtk_toolbox.threaded_stage(
853                         gtk_toolbox.comap(
854                                 self._idly_populate_recentview,
855                                 gtk_toolbox.null_sink(),
856                         )
857                 )
858
859         def enable(self):
860                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
861                 self._recentview.set_model(self._recentmodel)
862
863                 self._recentview.append_column(self._dateColumn)
864                 self._recentview.append_column(self._actionColumn)
865                 self._recentview.append_column(self._numberColumn)
866                 self._recentview.append_column(self._nameColumn)
867                 self._recentviewselection = self._recentview.get_selection()
868                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
869
870                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
871
872         def disable(self):
873                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
874
875                 self.clear()
876
877                 self._recentview.remove_column(self._dateColumn)
878                 self._recentview.remove_column(self._actionColumn)
879                 self._recentview.remove_column(self._nameColumn)
880                 self._recentview.remove_column(self._numberColumn)
881                 self._recentview.set_model(None)
882
883         def number_selected(self, action, number, message):
884                 """
885                 @note Actual dial function is patched in later
886                 """
887                 raise NotImplementedError("Horrible unknown error has occurred")
888
889         def update(self, force = False):
890                 if not force and self._isPopulated:
891                         return False
892                 self._updateSink.send(())
893                 return True
894
895         def clear(self):
896                 self._isPopulated = False
897                 self._recentmodel.clear()
898
899         @staticmethod
900         def name():
901                 return "Recent Calls"
902
903         def load_settings(self, config, section):
904                 pass
905
906         def save_settings(self, config, section):
907                 """
908                 @note Thread Agnostic
909                 """
910                 pass
911
912         def _idly_populate_recentview(self):
913                 self._recentmodel.clear()
914                 self._isPopulated = True
915
916                 try:
917                         recentItems = self._backend.get_recent()
918                 except StandardError, e:
919                         self._errorDisplay.push_exception_with_lock()
920                         self._isPopulated = False
921                         recentItems = []
922
923                 for personName, phoneNumber, date, action in recentItems:
924                         if not personName:
925                                 personName = "Unknown"
926                         date = abbrev_relative_date(date)
927                         prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
928                         prettyNumber = make_pretty(prettyNumber)
929                         item = (prettyNumber, date, action.capitalize(), personName)
930                         with gtk_toolbox.gtk_lock():
931                                 self._recentmodel.append(item)
932
933                 return False
934
935         def _on_recentview_row_activated(self, treeview, path, view_column):
936                 model, itr = self._recentviewselection.get_selected()
937                 if not itr:
938                         return
939
940                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
941                 number = make_ugly(number)
942                 contactPhoneNumbers = [("Phone", number)]
943                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
944
945                 action, phoneNumber, message = self._phoneTypeSelector.run(
946                         contactPhoneNumbers,
947                         message = description,
948                         parent = self._window,
949                 )
950                 if action == PhoneTypeSelector.ACTION_CANCEL:
951                         return
952                 assert phoneNumber, "A lack of phone number exists"
953
954                 self.number_selected(action, phoneNumber, message)
955                 self._recentviewselection.unselect_all()
956
957
958 class MessagesView(object):
959
960         NUMBER_IDX = 0
961         DATE_IDX = 1
962         HEADER_IDX = 2
963         MESSAGE_IDX = 3
964
965         def __init__(self, widgetTree, backend, errorDisplay):
966                 self._errorDisplay = errorDisplay
967                 self._backend = backend
968
969                 self._isPopulated = False
970                 self._messagemodel = gtk.ListStore(
971                         gobject.TYPE_STRING, # number
972                         gobject.TYPE_STRING, # date
973                         gobject.TYPE_STRING, # header
974                         gobject.TYPE_STRING, # message
975                 )
976                 self._messageview = widgetTree.get_widget("messages_view")
977                 self._messageviewselection = None
978                 self._onMessageviewRowActivatedId = 0
979
980                 self._messageRenderer = gtk.CellRendererText()
981                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
982                 self._messageRenderer.set_property("wrap-width", 500)
983                 self._messageColumn = gtk.TreeViewColumn("Messages")
984                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
985                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
986                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
987
988                 self._window = gtk_toolbox.find_parent_window(self._messageview)
989                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
990
991                 self._updateSink = gtk_toolbox.threaded_stage(
992                         gtk_toolbox.comap(
993                                 self._idly_populate_messageview,
994                                 gtk_toolbox.null_sink(),
995                         )
996                 )
997
998         def enable(self):
999                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1000                 self._messageview.set_model(self._messagemodel)
1001
1002                 self._messageview.append_column(self._messageColumn)
1003                 self._messageviewselection = self._messageview.get_selection()
1004                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1005
1006                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1007
1008         def disable(self):
1009                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1010
1011                 self.clear()
1012
1013                 self._messageview.remove_column(self._messageColumn)
1014                 self._messageview.set_model(None)
1015
1016         def number_selected(self, action, number, message):
1017                 """
1018                 @note Actual dial function is patched in later
1019                 """
1020                 raise NotImplementedError("Horrible unknown error has occurred")
1021
1022         def update(self, force = False):
1023                 if not force and self._isPopulated:
1024                         return False
1025                 self._updateSink.send(())
1026                 return True
1027
1028         def clear(self):
1029                 self._isPopulated = False
1030                 self._messagemodel.clear()
1031
1032         @staticmethod
1033         def name():
1034                 return "Messages"
1035
1036         def load_settings(self, config, section):
1037                 pass
1038
1039         def save_settings(self, config, section):
1040                 """
1041                 @note Thread Agnostic
1042                 """
1043                 pass
1044
1045         def _idly_populate_messageview(self):
1046                 self._messagemodel.clear()
1047                 self._isPopulated = True
1048
1049                 try:
1050                         messageItems = self._backend.get_messages()
1051                 except StandardError, e:
1052                         self._errorDisplay.push_exception_with_lock()
1053                         self._isPopulated = False
1054                         messageItems = []
1055
1056                 for header, number, relativeDate, message in messageItems:
1057                         prettyNumber = number[2:] if number.startswith("+1") else number
1058                         prettyNumber = make_pretty(prettyNumber)
1059                         message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1060                         number = make_ugly(number)
1061                         row = (number, relativeDate, header, message)
1062                         with gtk_toolbox.gtk_lock():
1063                                 self._messagemodel.append(row)
1064
1065                 return False
1066
1067         def _on_messageview_row_activated(self, treeview, path, view_column):
1068                 model, itr = self._messageviewselection.get_selected()
1069                 if not itr:
1070                         return
1071
1072                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1073                 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1074
1075                 action, phoneNumber, message = self._phoneTypeSelector.run(
1076                         contactPhoneNumbers,
1077                         message = description,
1078                         parent = self._window,
1079                 )
1080                 if action == PhoneTypeSelector.ACTION_CANCEL:
1081                         return
1082                 assert phoneNumber, "A lock of phone number exists"
1083
1084                 self.number_selected(action, phoneNumber, message)
1085                 self._messageviewselection.unselect_all()
1086
1087
1088 class ContactsView(object):
1089
1090         def __init__(self, widgetTree, backend, errorDisplay):
1091                 self._errorDisplay = errorDisplay
1092                 self._backend = backend
1093
1094                 self._addressBook = None
1095                 self._selectedComboIndex = 0
1096                 self._addressBookFactories = [null_backend.NullAddressBook()]
1097
1098                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1099                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1100
1101                 self._isPopulated = False
1102                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1103                 self._contactsviewselection = None
1104                 self._contactsview = widgetTree.get_widget("contactsview")
1105
1106                 self._contactColumn = gtk.TreeViewColumn("Contact")
1107                 displayContactSource = False
1108                 if displayContactSource:
1109                         textrenderer = gtk.CellRendererText()
1110                         self._contactColumn.pack_start(textrenderer, expand=False)
1111                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1112                 textrenderer = gtk.CellRendererText()
1113                 hildonize.set_cell_thumb_selectable(textrenderer)
1114                 self._contactColumn.pack_start(textrenderer, expand=True)
1115                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1116                 textrenderer = gtk.CellRendererText()
1117                 self._contactColumn.pack_start(textrenderer, expand=True)
1118                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1119                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1120                 self._contactColumn.set_sort_column_id(1)
1121                 self._contactColumn.set_visible(True)
1122
1123                 self._onContactsviewRowActivatedId = 0
1124                 self._onAddressbookComboChangedId = 0
1125                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1126                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1127
1128                 self._updateSink = gtk_toolbox.threaded_stage(
1129                         gtk_toolbox.comap(
1130                                 self._idly_populate_contactsview,
1131                                 gtk_toolbox.null_sink(),
1132                         )
1133                 )
1134
1135         def enable(self):
1136                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1137
1138                 self._contactsview.set_model(self._contactsmodel)
1139                 self._contactsview.append_column(self._contactColumn)
1140                 self._contactsviewselection = self._contactsview.get_selection()
1141                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1142
1143                 self._booksList.clear()
1144                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1145                         if factoryName and bookName:
1146                                 entryName = "%s: %s" % (factoryName, bookName)
1147                         elif factoryName:
1148                                 entryName = factoryName
1149                         elif bookName:
1150                                 entryName = bookName
1151                         else:
1152                                 entryName = "Bad name (%d)" % factoryId
1153                         row = (str(factoryId), bookId, entryName)
1154                         self._booksList.append(row)
1155
1156                 self._booksSelectionBox.set_model(self._booksList)
1157                 cell = gtk.CellRendererText()
1158                 self._booksSelectionBox.pack_start(cell, True)
1159                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1160
1161                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1162                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1163
1164                 if len(self._booksList) <= self._selectedComboIndex:
1165                         self._selectedComboIndex = 0
1166                 self._booksSelectionBox.set_active(self._selectedComboIndex)
1167
1168         def disable(self):
1169                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1170                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1171
1172                 self.clear()
1173
1174                 self._booksSelectionBox.clear()
1175                 self._booksSelectionBox.set_model(None)
1176                 self._contactsview.set_model(None)
1177                 self._contactsview.remove_column(self._contactColumn)
1178
1179         def number_selected(self, action, number, message):
1180                 """
1181                 @note Actual dial function is patched in later
1182                 """
1183                 raise NotImplementedError("Horrible unknown error has occurred")
1184
1185         def get_addressbooks(self):
1186                 """
1187                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1188                 """
1189                 for i, factory in enumerate(self._addressBookFactories):
1190                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1191                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1192
1193         def open_addressbook(self, bookFactoryId, bookId):
1194                 bookFactoryIndex = int(bookFactoryId)
1195                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1196
1197                 forceUpdate = True if addressBook is not self._addressBook else False
1198
1199                 self._addressBook = addressBook
1200                 self.update(force=forceUpdate)
1201
1202         def update(self, force = False):
1203                 if not force and self._isPopulated:
1204                         return False
1205                 self._updateSink.send(())
1206                 return True
1207
1208         def clear(self):
1209                 self._isPopulated = False
1210                 self._contactsmodel.clear()
1211                 for factory in self._addressBookFactories:
1212                         factory.clear_caches()
1213                 self._addressBook.clear_caches()
1214
1215         def append(self, book):
1216                 self._addressBookFactories.append(book)
1217
1218         def extend(self, books):
1219                 self._addressBookFactories.extend(books)
1220
1221         @staticmethod
1222         def name():
1223                 return "Contacts"
1224
1225         def load_settings(self, config, sectionName):
1226                 try:
1227                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1228                 except ConfigParser.NoOptionError:
1229                         self._selectedComboIndex = 0
1230
1231         def save_settings(self, config, sectionName):
1232                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1233
1234         def _idly_populate_contactsview(self):
1235                 addressBook = None
1236                 while addressBook is not self._addressBook:
1237                         addressBook = self._addressBook
1238                         with gtk_toolbox.gtk_lock():
1239                                 self._contactsview.set_model(None)
1240                                 self.clear()
1241
1242                         try:
1243                                 contacts = addressBook.get_contacts()
1244                         except StandardError, e:
1245                                 contacts = []
1246                                 self._isPopulated = False
1247                                 self._errorDisplay.push_exception_with_lock()
1248                         for contactId, contactName in contacts:
1249                                 contactType = (addressBook.contact_source_short_name(contactId), )
1250                                 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1251
1252                         with gtk_toolbox.gtk_lock():
1253                                 self._contactsview.set_model(self._contactsmodel)
1254
1255                 self._isPopulated = True
1256                 return False
1257
1258         def _on_addressbook_combo_changed(self, *args, **kwds):
1259                 itr = self._booksSelectionBox.get_active_iter()
1260                 if itr is None:
1261                         return
1262                 self._selectedComboIndex = self._booksSelectionBox.get_active()
1263                 selectedFactoryId = self._booksList.get_value(itr, 0)
1264                 selectedBookId = self._booksList.get_value(itr, 1)
1265                 self.open_addressbook(selectedFactoryId, selectedBookId)
1266
1267         def _on_contactsview_row_activated(self, treeview, path, view_column):
1268                 model, itr = self._contactsviewselection.get_selected()
1269                 if not itr:
1270                         return
1271
1272                 contactId = self._contactsmodel.get_value(itr, 3)
1273                 contactName = self._contactsmodel.get_value(itr, 1)
1274                 try:
1275                         contactDetails = self._addressBook.get_contact_details(contactId)
1276                 except StandardError, e:
1277                         contactDetails = []
1278                         self._errorDisplay.push_exception()
1279                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1280
1281                 if len(contactPhoneNumbers) == 0:
1282                         return
1283
1284                 action, phoneNumber, message = self._phoneTypeSelector.run(
1285                         contactPhoneNumbers,
1286                         message = contactName,
1287                         parent = self._window,
1288                 )
1289                 if action == PhoneTypeSelector.ACTION_CANCEL:
1290                         return
1291                 assert phoneNumber, "A lack of phone number exists"
1292
1293                 self.number_selected(action, phoneNumber, message)
1294                 self._contactsviewselection.unselect_all()