We shouldn't really need to mess with the increment plus it makes hildonization easier
[gc-dialer] / src / gc_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 null_backend
33
34
35 def make_ugly(prettynumber):
36         """
37         function to take a phone number and strip out all non-numeric
38         characters
39
40         >>> make_ugly("+012-(345)-678-90")
41         '01234567890'
42         """
43         import re
44         uglynumber = re.sub('\D', '', prettynumber)
45         return uglynumber
46
47
48 def make_pretty(phonenumber):
49         """
50         Function to take a phone number and return the pretty version
51         pretty numbers:
52                 if phonenumber begins with 0:
53                         ...-(...)-...-....
54                 if phonenumber begins with 1: ( for gizmo callback numbers )
55                         1 (...)-...-....
56                 if phonenumber is 13 digits:
57                         (...)-...-....
58                 if phonenumber is 10 digits:
59                         ...-....
60         >>> make_pretty("12")
61         '12'
62         >>> make_pretty("1234567")
63         '123-4567'
64         >>> make_pretty("2345678901")
65         '(234)-567-8901'
66         >>> make_pretty("12345678901")
67         '1 (234)-567-8901'
68         >>> make_pretty("01234567890")
69         '+012-(345)-678-90'
70         """
71         if phonenumber is None or phonenumber is "":
72                 return ""
73
74         phonenumber = make_ugly(phonenumber)
75
76         if len(phonenumber) < 3:
77                 return phonenumber
78
79         if phonenumber[0] == "0":
80                 prettynumber = ""
81                 prettynumber += "+%s" % phonenumber[0:3]
82                 if 3 < len(phonenumber):
83                         prettynumber += "-(%s)" % phonenumber[3:6]
84                         if 6 < len(phonenumber):
85                                 prettynumber += "-%s" % phonenumber[6:9]
86                                 if 9 < len(phonenumber):
87                                         prettynumber += "-%s" % phonenumber[9:]
88                 return prettynumber
89         elif len(phonenumber) <= 7:
90                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
91         elif len(phonenumber) > 8 and phonenumber[0] == "1":
92                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
93         elif len(phonenumber) > 7:
94                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
95         return prettynumber
96
97
98 class MergedAddressBook(object):
99         """
100         Merger of all addressbooks
101         """
102
103         def __init__(self, addressbookFactories, sorter = None):
104                 self.__addressbookFactories = addressbookFactories
105                 self.__addressbooks = None
106                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
107
108         def clear_caches(self):
109                 self.__addressbooks = None
110                 for factory in self.__addressbookFactories:
111                         factory.clear_caches()
112
113         def get_addressbooks(self):
114                 """
115                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
116                 """
117                 yield self, "", ""
118
119         def open_addressbook(self, bookId):
120                 return self
121
122         def contact_source_short_name(self, contactId):
123                 if self.__addressbooks is None:
124                         return ""
125                 bookIndex, originalId = contactId.split("-", 1)
126                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
127
128         @staticmethod
129         def factory_name():
130                 return "All Contacts"
131
132         def get_contacts(self):
133                 """
134                 @returns Iterable of (contact id, contact name)
135                 """
136                 if self.__addressbooks is None:
137                         self.__addressbooks = list(
138                                 factory.open_addressbook(id)
139                                 for factory in self.__addressbookFactories
140                                 for (f, id, name) in factory.get_addressbooks()
141                         )
142                 contacts = (
143                         ("-".join([str(bookIndex), contactId]), contactName)
144                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
145                                         for (contactId, contactName) in addressbook.get_contacts()
146                 )
147                 sortedContacts = self.__sort_contacts(contacts)
148                 return sortedContacts
149
150         def get_contact_details(self, contactId):
151                 """
152                 @returns Iterable of (Phone Type, Phone Number)
153                 """
154                 if self.__addressbooks is None:
155                         return []
156                 bookIndex, originalId = contactId.split("-", 1)
157                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
158
159         @staticmethod
160         def null_sorter(contacts):
161                 """
162                 Good for speed/low memory
163                 """
164                 return contacts
165
166         @staticmethod
167         def basic_firtname_sorter(contacts):
168                 """
169                 Expects names in "First Last" format
170                 """
171                 contactsWithKey = [
172                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
173                                 for (contactId, contactName) in contacts
174                 ]
175                 contactsWithKey.sort()
176                 return (contactData for (lastName, contactData) in contactsWithKey)
177
178         @staticmethod
179         def basic_lastname_sorter(contacts):
180                 """
181                 Expects names in "First Last" format
182                 """
183                 contactsWithKey = [
184                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
185                                 for (contactId, contactName) in contacts
186                 ]
187                 contactsWithKey.sort()
188                 return (contactData for (lastName, contactData) in contactsWithKey)
189
190         @staticmethod
191         def reversed_firtname_sorter(contacts):
192                 """
193                 Expects names in "Last, First" format
194                 """
195                 contactsWithKey = [
196                         (contactName.split(", ", 1)[-1], (contactId, contactName))
197                                 for (contactId, contactName) in contacts
198                 ]
199                 contactsWithKey.sort()
200                 return (contactData for (lastName, contactData) in contactsWithKey)
201
202         @staticmethod
203         def reversed_lastname_sorter(contacts):
204                 """
205                 Expects names in "Last, First" format
206                 """
207                 contactsWithKey = [
208                         (contactName.split(", ", 1)[0], (contactId, contactName))
209                                 for (contactId, contactName) in contacts
210                 ]
211                 contactsWithKey.sort()
212                 return (contactData for (lastName, contactData) in contactsWithKey)
213
214         @staticmethod
215         def guess_firstname(name):
216                 if ", " in name:
217                         return name.split(", ", 1)[-1]
218                 else:
219                         return name.rsplit(" ", 1)[0]
220
221         @staticmethod
222         def guess_lastname(name):
223                 if ", " in name:
224                         return name.split(", ", 1)[0]
225                 else:
226                         return name.rsplit(" ", 1)[-1]
227
228         @classmethod
229         def advanced_firstname_sorter(cls, contacts):
230                 contactsWithKey = [
231                         (cls.guess_firstname(contactName), (contactId, contactName))
232                                 for (contactId, contactName) in contacts
233                 ]
234                 contactsWithKey.sort()
235                 return (contactData for (lastName, contactData) in contactsWithKey)
236
237         @classmethod
238         def advanced_lastname_sorter(cls, contacts):
239                 contactsWithKey = [
240                         (cls.guess_lastname(contactName), (contactId, contactName))
241                                 for (contactId, contactName) in contacts
242                 ]
243                 contactsWithKey.sort()
244                 return (contactData for (lastName, contactData) in contactsWithKey)
245
246
247 class PhoneTypeSelector(object):
248
249         ACTION_CANCEL = "cancel"
250         ACTION_SELECT = "select"
251         ACTION_DIAL = "dial"
252         ACTION_SEND_SMS = "sms"
253
254         def __init__(self, widgetTree, gcBackend):
255                 self._gcBackend = gcBackend
256                 self._widgetTree = widgetTree
257
258                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
259                 self._smsDialog = SmsEntryDialog(self._widgetTree)
260
261                 self._smsButton = self._widgetTree.get_widget("sms_button")
262                 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
263
264                 self._dialButton = self._widgetTree.get_widget("dial_button")
265                 self._dialButton.connect("clicked", self._on_phonetype_dial)
266
267                 self._selectButton = self._widgetTree.get_widget("select_button")
268                 self._selectButton.connect("clicked", self._on_phonetype_select)
269
270                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
271                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
272
273                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
274                 self._typeviewselection = None
275
276                 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
277                 self._typeview = self._widgetTree.get_widget("phonetypes")
278                 self._typeview.connect("row-activated", self._on_phonetype_select)
279
280                 self._action = self.ACTION_CANCEL
281
282         def run(self, contactDetails, message = "", parent = None):
283                 self._action = self.ACTION_CANCEL
284                 self._typemodel.clear()
285                 self._typeview.set_model(self._typemodel)
286
287                 # Add the column to the treeview
288                 textrenderer = gtk.CellRendererText()
289                 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
290                 self._typeview.append_column(numberColumn)
291
292                 textrenderer = gtk.CellRendererText()
293                 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
294                 self._typeview.append_column(typeColumn)
295
296                 self._typeviewselection = self._typeview.get_selection()
297                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
298
299                 for phoneType, phoneNumber in contactDetails:
300                         display = " - ".join((phoneNumber, phoneType))
301                         display = phoneType
302                         row = (phoneNumber, display)
303                         self._typemodel.append(row)
304
305                 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
306                 if message:
307                         self._message.set_markup(message)
308                         self._message.show()
309                 else:
310                         self._message.set_markup("")
311                         self._message.hide()
312
313                 if parent is not None:
314                         self._dialog.set_transient_for(parent)
315
316                 try:
317                         userResponse = self._dialog.run()
318                 finally:
319                         self._dialog.hide()
320
321                 if userResponse == gtk.RESPONSE_OK:
322                         phoneNumber = self._get_number()
323                         phoneNumber = make_ugly(phoneNumber)
324                 else:
325                         phoneNumber = ""
326                 if not phoneNumber:
327                         self._action = self.ACTION_CANCEL
328
329                 if self._action == self.ACTION_SEND_SMS:
330                         smsMessage = self._smsDialog.run(phoneNumber, message, parent)
331                         if not smsMessage:
332                                 phoneNumber = ""
333                                 self._action = self.ACTION_CANCEL
334                 else:
335                         smsMessage = ""
336
337                 self._typeviewselection.unselect_all()
338                 self._typeview.remove_column(numberColumn)
339                 self._typeview.remove_column(typeColumn)
340                 self._typeview.set_model(None)
341
342                 return self._action, phoneNumber, smsMessage
343
344         def _get_number(self):
345                 model, itr = self._typeviewselection.get_selected()
346                 if not itr:
347                         return ""
348
349                 phoneNumber = self._typemodel.get_value(itr, 0)
350                 return phoneNumber
351
352         def _on_phonetype_dial(self, *args):
353                 self._dialog.response(gtk.RESPONSE_OK)
354                 self._action = self.ACTION_DIAL
355
356         def _on_phonetype_send_sms(self, *args):
357                 self._dialog.response(gtk.RESPONSE_OK)
358                 self._action = self.ACTION_SEND_SMS
359
360         def _on_phonetype_select(self, *args):
361                 self._dialog.response(gtk.RESPONSE_OK)
362                 self._action = self.ACTION_SELECT
363
364         def _on_phonetype_cancel(self, *args):
365                 self._dialog.response(gtk.RESPONSE_CANCEL)
366                 self._action = self.ACTION_CANCEL
367
368
369 class SmsEntryDialog(object):
370
371         """
372         @todo Add multi-SMS messages like GoogleVoice
373         """
374
375         MAX_CHAR = 160
376
377         def __init__(self, widgetTree):
378                 self._widgetTree = widgetTree
379                 self._dialog = self._widgetTree.get_widget("smsDialog")
380
381                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
382                 self._smsButton.connect("clicked", self._on_send)
383
384                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
385                 self._cancelButton.connect("clicked", self._on_cancel)
386
387                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
388                 self._message = self._widgetTree.get_widget("smsMessage")
389                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
390                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
391
392         def run(self, number, message = "", parent = None):
393                 if message:
394                         self._message.set_markup(message)
395                         self._message.show()
396                 else:
397                         self._message.set_markup("")
398                         self._message.hide()
399                 self._smsEntry.get_buffer().set_text("")
400                 self._update_letter_count()
401
402                 if parent is not None:
403                         self._dialog.set_transient_for(parent)
404
405                 try:
406                         userResponse = self._dialog.run()
407                 finally:
408                         self._dialog.hide()
409
410                 if userResponse == gtk.RESPONSE_OK:
411                         entryBuffer = self._smsEntry.get_buffer()
412                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
413                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
414                 else:
415                         enteredMessage = ""
416
417                 return enteredMessage.strip()
418
419         def _update_letter_count(self, *args):
420                 entryLength = self._smsEntry.get_buffer().get_char_count()
421                 charsLeft = self.MAX_CHAR - entryLength
422                 self._letterCountLabel.set_text(str(charsLeft))
423                 if charsLeft < 0:
424                         self._smsButton.set_sensitive(False)
425                 else:
426                         self._smsButton.set_sensitive(True)
427
428         def _on_entry_changed(self, *args):
429                 self._update_letter_count()
430
431         def _on_send(self, *args):
432                 self._dialog.response(gtk.RESPONSE_OK)
433
434         def _on_cancel(self, *args):
435                 self._dialog.response(gtk.RESPONSE_CANCEL)
436
437
438 class Dialpad(object):
439
440         def __init__(self, widgetTree, errorDisplay):
441                 self._errorDisplay = errorDisplay
442                 self._smsDialog = SmsEntryDialog(widgetTree)
443
444                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
445                 self._dialButton = widgetTree.get_widget("dial")
446                 self._backButton = widgetTree.get_widget("back")
447                 self._phonenumber = ""
448                 self._prettynumber = ""
449
450                 callbackMapping = {
451                         "on_dial_clicked": self._on_dial_clicked,
452                         "on_sms_clicked": self._on_sms_clicked,
453                         "on_digit_clicked": self._on_digit_clicked,
454                         "on_clear_number": self._on_clear_number,
455                 }
456                 widgetTree.signal_autoconnect(callbackMapping)
457
458                 self._originalLabel = self._backButton.get_label()
459                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
460                 self._backTapHandler.on_tap = self._on_backspace
461                 self._backTapHandler.on_hold = self._on_clearall
462                 self._backTapHandler.on_holding = self._set_clear_button
463                 self._backTapHandler.on_cancel = self._reset_back_button
464
465                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
466
467         def enable(self):
468                 self.set_number(self._phonenumber)
469                 self._dialButton.grab_focus()
470                 self._backTapHandler.enable()
471
472         def disable(self):
473                 self._reset_back_button()
474                 self._backTapHandler.disable()
475                 self.set_number("")
476
477         def number_selected(self, action, number, message):
478                 """
479                 @note Actual dial function is patched in later
480                 """
481                 raise NotImplementedError("Horrible unknown error has occurred")
482
483         def get_number(self):
484                 return self._phonenumber
485
486         def set_number(self, number):
487                 """
488                 Set the number to dial
489                 """
490                 try:
491                         self._phonenumber = make_ugly(number)
492                         self._prettynumber = make_pretty(self._phonenumber)
493                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
494                 except TypeError, e:
495                         self._errorDisplay.push_exception()
496
497         def clear(self):
498                 self.set_number("")
499
500         @staticmethod
501         def name():
502                 return "Dialpad"
503
504         def load_settings(self, config, section):
505                 pass
506
507         def save_settings(self, config, section):
508                 """
509                 @note Thread Agnostic
510                 """
511                 pass
512
513         def _on_sms_clicked(self, widget):
514                 action = PhoneTypeSelector.ACTION_SEND_SMS
515                 phoneNumber = self.get_number()
516
517                 message = self._smsDialog.run(phoneNumber, "", self._window)
518                 if not message:
519                         phoneNumber = ""
520                         action = PhoneTypeSelector.ACTION_CANCEL
521
522                 if action == PhoneTypeSelector.ACTION_CANCEL:
523                         return
524                 self.number_selected(action, phoneNumber, message)
525
526         def _on_dial_clicked(self, widget):
527                 action = PhoneTypeSelector.ACTION_DIAL
528                 phoneNumber = self.get_number()
529                 message = ""
530                 self.number_selected(action, phoneNumber, message)
531
532         def _on_clear_number(self, *args):
533                 self.clear()
534
535         def _on_digit_clicked(self, widget):
536                 self.set_number(self._phonenumber + widget.get_name()[-1])
537
538         def _on_backspace(self, taps):
539                 self.set_number(self._phonenumber[:-taps])
540                 self._reset_back_button()
541
542         def _on_clearall(self, taps):
543                 self.clear()
544                 self._reset_back_button()
545                 return False
546
547         def _set_clear_button(self):
548                 self._backButton.set_label("gtk-clear")
549
550         def _reset_back_button(self):
551                 self._backButton.set_label(self._originalLabel)
552
553
554 class AccountInfo(object):
555
556         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
557                 self._errorDisplay = errorDisplay
558                 self._backend = backend
559                 self._isPopulated = False
560                 self._alarmHandler = alarmHandler
561                 self._notifyOnMissed = False
562                 self._notifyOnVoicemail = False
563                 self._notifyOnSms = False
564
565                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
566                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
567                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
568                 self._onCallbackentryChangedId = 0
569
570                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
571                 self._minutesEntry = widgetTree.get_widget("minutesEntry")
572                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
573                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
574                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
575                 self._onNotifyToggled = 0
576                 self._onMinutesChanged = 0
577                 self._onMissedToggled = 0
578                 self._onVoicemailToggled = 0
579                 self._onSmsToggled = 0
580                 self._applyAlarmTimeoutId = None
581
582                 self._defaultCallback = ""
583
584         def enable(self):
585                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
586
587                 self._accountViewNumberDisplay.set_use_markup(True)
588                 self.set_account_number("")
589
590                 self._callbackList.clear()
591                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
592
593                 if self._alarmHandler is not None:
594                         self._minutesEntry.set_range(1, 60)
595
596                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
597                         self._minutesEntry.set_value(self._alarmHandler.recurrence)
598                         self._missedCheckbox.set_active(self._notifyOnMissed)
599                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
600                         self._smsCheckbox.set_active(self._notifyOnSms)
601
602                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
603                         self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
604                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
605                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
606                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
607                 else:
608                         self._notifyCheckbox.set_sensitive(False)
609                         self._minutesEntry.set_sensitive(False)
610                         self._missedCheckbox.set_sensitive(False)
611                         self._voicemailCheckbox.set_sensitive(False)
612                         self._smsCheckbox.set_sensitive(False)
613
614                 self.update(force=True)
615
616         def disable(self):
617                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
618                 self._onCallbackentryChangedId = 0
619
620                 if self._alarmHandler is not None:
621                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
622                         self._minutesEntry.disconnect(self._onMinutesChanged)
623                         self._missedCheckbox.disconnect(self._onNotifyToggled)
624                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
625                         self._smsCheckbox.disconnect(self._onNotifyToggled)
626                         self._onNotifyToggled = 0
627                         self._onMinutesChanged = 0
628                         self._onMissedToggled = 0
629                         self._onVoicemailToggled = 0
630                         self._onSmsToggled = 0
631                 else:
632                         self._notifyCheckbox.set_sensitive(True)
633                         self._minutesEntry.set_sensitive(True)
634                         self._missedCheckbox.set_sensitive(True)
635                         self._voicemailCheckbox.set_sensitive(True)
636                         self._smsCheckbox.set_sensitive(True)
637
638                 self.clear()
639                 self._callbackList.clear()
640
641         def get_selected_callback_number(self):
642                 return make_ugly(self._callbackCombo.get_child().get_text())
643
644         def set_account_number(self, number):
645                 """
646                 Displays current account number
647                 """
648                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
649
650         def update(self, force = False):
651                 if not force and self._isPopulated:
652                         return False
653                 self._populate_callback_combo()
654                 self.set_account_number(self._backend.get_account_number())
655                 return True
656
657         def clear(self):
658                 self._callbackCombo.get_child().set_text("")
659                 self.set_account_number("")
660                 self._isPopulated = False
661
662         def save_everything(self):
663                 raise NotImplementedError
664
665         @staticmethod
666         def name():
667                 return "Account Info"
668
669         def load_settings(self, config, section):
670                 self._defaultCallback = config.get(section, "callback")
671                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
672                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
673                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
674
675         def save_settings(self, config, section):
676                 """
677                 @note Thread Agnostic
678                 """
679                 callback = self.get_selected_callback_number()
680                 config.set(section, "callback", callback)
681                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
682                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
683                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
684
685         def _populate_callback_combo(self):
686                 self._isPopulated = True
687                 self._callbackList.clear()
688                 try:
689                         callbackNumbers = self._backend.get_callback_numbers()
690                 except StandardError, e:
691                         self._errorDisplay.push_exception()
692                         self._isPopulated = False
693                         return
694
695                 for number, description in callbackNumbers.iteritems():
696                         self._callbackList.append((make_pretty(number),))
697
698                 self._callbackCombo.set_model(self._callbackList)
699                 self._callbackCombo.set_text_column(0)
700                 #callbackNumber = self._backend.get_callback_number()
701                 callbackNumber = self._defaultCallback
702                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
703
704         def _set_callback_number(self, number):
705                 try:
706                         if not self._backend.is_valid_syntax(number):
707                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
708                         elif number == self._backend.get_callback_number():
709                                 warnings.warn(
710                                         "Callback number already is %s" % (
711                                                 self._backend.get_callback_number(),
712                                         ),
713                                         UserWarning,
714                                         2
715                                 )
716                         else:
717                                 self._backend.set_callback_number(number)
718                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
719                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
720                                 )
721                                 warnings.warn(
722                                         "Callback number set to %s" % (
723                                                 self._backend.get_callback_number(),
724                                         ),
725                                         UserWarning, 2
726                                 )
727                 except StandardError, e:
728                         self._errorDisplay.push_exception()
729
730         def _update_alarm_settings(self):
731                 try:
732                         isEnabled = self._notifyCheckbox.get_active()
733                         recurrence = self._minutesEntry.get_value_as_int()
734                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
735                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
736                 finally:
737                         self.save_everything()
738                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
739                         self._minutesEntry.set_value(self._alarmHandler.recurrence)
740
741         def _on_callbackentry_changed(self, *args):
742                 text = self.get_selected_callback_number()
743                 number = make_ugly(text)
744                 self._set_callback_number(number)
745
746         def _on_notify_toggled(self, *args):
747                 if self._applyAlarmTimeoutId is not None:
748                         gobject.source_remove(self._applyAlarmTimeoutId)
749                         self._applyAlarmTimeoutId = None
750                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
751
752         def _on_minutes_changed(self, *args):
753                 if self._applyAlarmTimeoutId is not None:
754                         gobject.source_remove(self._applyAlarmTimeoutId)
755                         self._applyAlarmTimeoutId = None
756                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
757
758         def _on_apply_timeout(self, *args):
759                 self._applyAlarmTimeoutId = None
760
761                 self._update_alarm_settings()
762                 return False
763
764         def _on_missed_toggled(self, *args):
765                 self._notifyOnMissed = self._missedCheckbox.get_active()
766                 self.save_everything()
767
768         def _on_voicemail_toggled(self, *args):
769                 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
770                 self.save_everything()
771
772         def _on_sms_toggled(self, *args):
773                 self._notifyOnSms = self._smsCheckbox.get_active()
774                 self.save_everything()
775
776
777 class RecentCallsView(object):
778
779         NUMBER_IDX = 0
780         DATE_IDX = 1
781         ACTION_IDX = 2
782         FROM_IDX = 3
783
784         def __init__(self, widgetTree, backend, errorDisplay):
785                 self._errorDisplay = errorDisplay
786                 self._backend = backend
787
788                 self._isPopulated = False
789                 self._recentmodel = gtk.ListStore(
790                         gobject.TYPE_STRING, # number
791                         gobject.TYPE_STRING, # date
792                         gobject.TYPE_STRING, # action
793                         gobject.TYPE_STRING, # from
794                 )
795                 self._recentview = widgetTree.get_widget("recentview")
796                 self._recentviewselection = None
797                 self._onRecentviewRowActivatedId = 0
798
799                 textrenderer = gtk.CellRendererText()
800                 textrenderer.set_property("yalign", 0)
801                 self._dateColumn = gtk.TreeViewColumn("Date")
802                 self._dateColumn.pack_start(textrenderer, expand=True)
803                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
804
805                 textrenderer = gtk.CellRendererText()
806                 textrenderer.set_property("yalign", 0)
807                 self._actionColumn = gtk.TreeViewColumn("Action")
808                 self._actionColumn.pack_start(textrenderer, expand=True)
809                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
810
811                 textrenderer = gtk.CellRendererText()
812                 textrenderer.set_property("yalign", 0)
813                 textrenderer.set_property("scale", 1.5)
814                 self._fromColumn = gtk.TreeViewColumn("From")
815                 self._fromColumn.pack_start(textrenderer, expand=True)
816                 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
817                 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
818
819                 self._window = gtk_toolbox.find_parent_window(self._recentview)
820                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
821
822                 self._updateSink = gtk_toolbox.threaded_stage(
823                         gtk_toolbox.comap(
824                                 self._idly_populate_recentview,
825                                 gtk_toolbox.null_sink(),
826                         )
827                 )
828
829         def enable(self):
830                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
831                 self._recentview.set_model(self._recentmodel)
832
833                 self._recentview.append_column(self._dateColumn)
834                 self._recentview.append_column(self._actionColumn)
835                 self._recentview.append_column(self._fromColumn)
836                 self._recentviewselection = self._recentview.get_selection()
837                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
838
839                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
840
841         def disable(self):
842                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
843
844                 self.clear()
845
846                 self._recentview.remove_column(self._dateColumn)
847                 self._recentview.remove_column(self._actionColumn)
848                 self._recentview.remove_column(self._fromColumn)
849                 self._recentview.set_model(None)
850
851         def number_selected(self, action, number, message):
852                 """
853                 @note Actual dial function is patched in later
854                 """
855                 raise NotImplementedError("Horrible unknown error has occurred")
856
857         def update(self, force = False):
858                 if not force and self._isPopulated:
859                         return False
860                 self._updateSink.send(())
861                 return True
862
863         def clear(self):
864                 self._isPopulated = False
865                 self._recentmodel.clear()
866
867         @staticmethod
868         def name():
869                 return "Recent Calls"
870
871         def load_settings(self, config, section):
872                 pass
873
874         def save_settings(self, config, section):
875                 """
876                 @note Thread Agnostic
877                 """
878                 pass
879
880         def _idly_populate_recentview(self):
881                 self._recentmodel.clear()
882                 self._isPopulated = True
883
884                 try:
885                         recentItems = self._backend.get_recent()
886                 except StandardError, e:
887                         self._errorDisplay.push_exception_with_lock()
888                         self._isPopulated = False
889                         recentItems = []
890
891                 for personName, phoneNumber, date, action in recentItems:
892                         if not personName:
893                                 personName = "Unknown"
894                         prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
895                         prettyNumber = make_pretty(prettyNumber)
896                         description = "%s - %s" % (personName, prettyNumber)
897                         item = (phoneNumber, date, action.capitalize(), description)
898                         with gtk_toolbox.gtk_lock():
899                                 self._recentmodel.append(item)
900
901                 return False
902
903         def _on_recentview_row_activated(self, treeview, path, view_column):
904                 model, itr = self._recentviewselection.get_selected()
905                 if not itr:
906                         return
907
908                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
909                 number = make_ugly(number)
910                 contactPhoneNumbers = [("Phone", number)]
911                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
912
913                 action, phoneNumber, message = self._phoneTypeSelector.run(
914                         contactPhoneNumbers,
915                         message = description,
916                         parent = self._window,
917                 )
918                 if action == PhoneTypeSelector.ACTION_CANCEL:
919                         return
920                 assert phoneNumber, "A lack of phone number exists"
921
922                 self.number_selected(action, phoneNumber, message)
923                 self._recentviewselection.unselect_all()
924
925
926 class MessagesView(object):
927
928         NUMBER_IDX = 0
929         DATE_IDX = 1
930         HEADER_IDX = 2
931         MESSAGE_IDX = 3
932
933         def __init__(self, widgetTree, backend, errorDisplay):
934                 self._errorDisplay = errorDisplay
935                 self._backend = backend
936
937                 self._isPopulated = False
938                 self._messagemodel = gtk.ListStore(
939                         gobject.TYPE_STRING, # number
940                         gobject.TYPE_STRING, # date
941                         gobject.TYPE_STRING, # header
942                         gobject.TYPE_STRING, # message
943                 )
944                 self._messageview = widgetTree.get_widget("messages_view")
945                 self._messageviewselection = None
946                 self._onMessageviewRowActivatedId = 0
947
948                 dateRenderer = gtk.CellRendererText()
949                 dateRenderer.set_property("yalign", 0)
950                 self._dateColumn = gtk.TreeViewColumn("Date")
951                 self._dateColumn.pack_start(dateRenderer, expand=True)
952                 self._dateColumn.add_attribute(dateRenderer, "markup", self.DATE_IDX)
953
954                 self._nameRenderer = gtk.CellRendererText()
955                 self._nameRenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
956                 self._nameRenderer.set_property("yalign", 0)
957                 self._headerColumn = gtk.TreeViewColumn("From")
958                 self._headerColumn.pack_start(self._nameRenderer, expand=True)
959                 self._headerColumn.add_attribute(self._nameRenderer, "markup", self.HEADER_IDX)
960
961                 self._messageRenderer = gtk.CellRendererText()
962                 self._messageRenderer.set_property("yalign", 0)
963                 self._messageRenderer.set_property("scale", 1.5)
964                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
965                 self._messageRenderer.set_property("wrap-width", 500)
966                 self._messageColumn = gtk.TreeViewColumn("Messages")
967                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
968                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
969                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
970
971                 self._window = gtk_toolbox.find_parent_window(self._messageview)
972                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
973
974                 self._updateSink = gtk_toolbox.threaded_stage(
975                         gtk_toolbox.comap(
976                                 self._idly_populate_messageview,
977                                 gtk_toolbox.null_sink(),
978                         )
979                 )
980
981         def enable(self):
982                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
983                 self._messageview.set_model(self._messagemodel)
984
985                 self._messageview.append_column(self._dateColumn)
986                 self._messageview.append_column(self._headerColumn)
987                 self._messageview.append_column(self._messageColumn)
988                 self._messageviewselection = self._messageview.get_selection()
989                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
990
991                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
992
993         def disable(self):
994                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
995
996                 self.clear()
997
998                 self._messageview.remove_column(self._dateColumn)
999                 self._messageview.remove_column(self._headerColumn)
1000                 self._messageview.remove_column(self._messageColumn)
1001                 self._messageview.set_model(None)
1002
1003         def number_selected(self, action, number, message):
1004                 """
1005                 @note Actual dial function is patched in later
1006                 """
1007                 raise NotImplementedError("Horrible unknown error has occurred")
1008
1009         def update(self, force = False):
1010                 if not force and self._isPopulated:
1011                         return False
1012                 self._updateSink.send(())
1013                 return True
1014
1015         def clear(self):
1016                 self._isPopulated = False
1017                 self._messagemodel.clear()
1018
1019         @staticmethod
1020         def name():
1021                 return "Messages"
1022
1023         def load_settings(self, config, section):
1024                 pass
1025
1026         def save_settings(self, config, section):
1027                 """
1028                 @note Thread Agnostic
1029                 """
1030                 pass
1031
1032         def _idly_populate_messageview(self):
1033                 self._messagemodel.clear()
1034                 self._isPopulated = True
1035
1036                 try:
1037                         messageItems = self._backend.get_messages()
1038                 except StandardError, e:
1039                         self._errorDisplay.push_exception_with_lock()
1040                         self._isPopulated = False
1041                         messageItems = []
1042
1043                 maxHeaderLength = 0
1044                 combinedHeaderLength = 0
1045                 headerCount = 0
1046                 for header, number, relativeDate, message in messageItems:
1047                         number = make_ugly(number)
1048                         row = (number, relativeDate, header, message)
1049                         with gtk_toolbox.gtk_lock():
1050                                 self._messagemodel.append(row)
1051
1052                         maxHeaderLength = max(maxHeaderLength, len(header))
1053                         combinedHeaderLength += len(header)
1054                         headerCount += 1
1055
1056                 if 0 < headerCount:
1057                         cellWidth = min(int((4 * combinedHeaderLength) / (3 * headerCount)), maxHeaderLength)
1058                 else:
1059                         cellWidth = 10
1060                 with gtk_toolbox.gtk_lock():
1061                         self._nameRenderer.set_property("width-chars", cellWidth)
1062                         cellPosAndDim = self._messageRenderer.get_size(self._messageview, None)
1063                         self._messageRenderer.set_property("wrap-width", cellPosAndDim[2])
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                 textrenderer.set_property("scale", 1.5)
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()