e32660dde801ff1b89d62ed43b8246d671ea0add
[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                         self._minutesEntry.set_increments(1, 5)
596
597                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
598                         self._minutesEntry.set_value(self._alarmHandler.recurrence)
599                         self._missedCheckbox.set_active(self._notifyOnMissed)
600                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
601                         self._smsCheckbox.set_active(self._notifyOnSms)
602
603                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
604                         self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
605                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
606                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
607                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
608                 else:
609                         self._notifyCheckbox.set_sensitive(False)
610                         self._minutesEntry.set_sensitive(False)
611                         self._missedCheckbox.set_sensitive(False)
612                         self._voicemailCheckbox.set_sensitive(False)
613                         self._smsCheckbox.set_sensitive(False)
614
615                 self.update(force=True)
616
617         def disable(self):
618                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
619                 self._onCallbackentryChangedId = 0
620
621                 if self._alarmHandler is not None:
622                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
623                         self._minutesEntry.disconnect(self._onMinutesChanged)
624                         self._missedCheckbox.disconnect(self._onNotifyToggled)
625                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
626                         self._smsCheckbox.disconnect(self._onNotifyToggled)
627                         self._onNotifyToggled = 0
628                         self._onMinutesChanged = 0
629                         self._onMissedToggled = 0
630                         self._onVoicemailToggled = 0
631                         self._onSmsToggled = 0
632                 else:
633                         self._notifyCheckbox.set_sensitive(True)
634                         self._minutesEntry.set_sensitive(True)
635                         self._missedCheckbox.set_sensitive(True)
636                         self._voicemailCheckbox.set_sensitive(True)
637                         self._smsCheckbox.set_sensitive(True)
638
639                 self.clear()
640                 self._callbackList.clear()
641
642         def get_selected_callback_number(self):
643                 return make_ugly(self._callbackCombo.get_child().get_text())
644
645         def set_account_number(self, number):
646                 """
647                 Displays current account number
648                 """
649                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
650
651         def update(self, force = False):
652                 if not force and self._isPopulated:
653                         return False
654                 self._populate_callback_combo()
655                 self.set_account_number(self._backend.get_account_number())
656                 return True
657
658         def clear(self):
659                 self._callbackCombo.get_child().set_text("")
660                 self.set_account_number("")
661                 self._isPopulated = False
662
663         def save_everything(self):
664                 raise NotImplementedError
665
666         @staticmethod
667         def name():
668                 return "Account Info"
669
670         def load_settings(self, config, section):
671                 self._defaultCallback = config.get(section, "callback")
672                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
673                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
674                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
675
676         def save_settings(self, config, section):
677                 """
678                 @note Thread Agnostic
679                 """
680                 callback = self.get_selected_callback_number()
681                 config.set(section, "callback", callback)
682                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
683                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
684                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
685
686         def _populate_callback_combo(self):
687                 self._isPopulated = True
688                 self._callbackList.clear()
689                 try:
690                         callbackNumbers = self._backend.get_callback_numbers()
691                 except StandardError, e:
692                         self._errorDisplay.push_exception()
693                         self._isPopulated = False
694                         return
695
696                 for number, description in callbackNumbers.iteritems():
697                         self._callbackList.append((make_pretty(number),))
698
699                 self._callbackCombo.set_model(self._callbackList)
700                 self._callbackCombo.set_text_column(0)
701                 #callbackNumber = self._backend.get_callback_number()
702                 callbackNumber = self._defaultCallback
703                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
704
705         def _set_callback_number(self, number):
706                 try:
707                         if not self._backend.is_valid_syntax(number):
708                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
709                         elif number == self._backend.get_callback_number():
710                                 warnings.warn(
711                                         "Callback number already is %s" % (
712                                                 self._backend.get_callback_number(),
713                                         ),
714                                         UserWarning,
715                                         2
716                                 )
717                         else:
718                                 self._backend.set_callback_number(number)
719                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
720                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
721                                 )
722                                 warnings.warn(
723                                         "Callback number set to %s" % (
724                                                 self._backend.get_callback_number(),
725                                         ),
726                                         UserWarning, 2
727                                 )
728                 except StandardError, e:
729                         self._errorDisplay.push_exception()
730
731         def _update_alarm_settings(self):
732                 try:
733                         isEnabled = self._notifyCheckbox.get_active()
734                         recurrence = self._minutesEntry.get_value_as_int()
735                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
736                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
737                 finally:
738                         self.save_everything()
739                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
740                         self._minutesEntry.set_value(self._alarmHandler.recurrence)
741
742         def _on_callbackentry_changed(self, *args):
743                 text = self.get_selected_callback_number()
744                 number = make_ugly(text)
745                 self._set_callback_number(number)
746
747         def _on_notify_toggled(self, *args):
748                 if self._applyAlarmTimeoutId is not None:
749                         gobject.source_remove(self._applyAlarmTimeoutId)
750                         self._applyAlarmTimeoutId = None
751                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
752
753         def _on_minutes_changed(self, *args):
754                 if self._applyAlarmTimeoutId is not None:
755                         gobject.source_remove(self._applyAlarmTimeoutId)
756                         self._applyAlarmTimeoutId = None
757                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
758
759         def _on_apply_timeout(self, *args):
760                 self._applyAlarmTimeoutId = None
761
762                 self._update_alarm_settings()
763                 return False
764
765         def _on_missed_toggled(self, *args):
766                 self._notifyOnMissed = self._missedCheckbox.get_active()
767                 self.save_everything()
768
769         def _on_voicemail_toggled(self, *args):
770                 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
771                 self.save_everything()
772
773         def _on_sms_toggled(self, *args):
774                 self._notifyOnSms = self._smsCheckbox.get_active()
775                 self.save_everything()
776
777
778 class RecentCallsView(object):
779
780         NUMBER_IDX = 0
781         DATE_IDX = 1
782         ACTION_IDX = 2
783         FROM_IDX = 3
784
785         def __init__(self, widgetTree, backend, errorDisplay):
786                 self._errorDisplay = errorDisplay
787                 self._backend = backend
788
789                 self._isPopulated = False
790                 self._recentmodel = gtk.ListStore(
791                         gobject.TYPE_STRING, # number
792                         gobject.TYPE_STRING, # date
793                         gobject.TYPE_STRING, # action
794                         gobject.TYPE_STRING, # from
795                 )
796                 self._recentview = widgetTree.get_widget("recentview")
797                 self._recentviewselection = None
798                 self._onRecentviewRowActivatedId = 0
799
800                 textrenderer = gtk.CellRendererText()
801                 textrenderer.set_property("yalign", 0)
802                 self._dateColumn = gtk.TreeViewColumn("Date")
803                 self._dateColumn.pack_start(textrenderer, expand=True)
804                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
805
806                 textrenderer = gtk.CellRendererText()
807                 textrenderer.set_property("yalign", 0)
808                 self._actionColumn = gtk.TreeViewColumn("Action")
809                 self._actionColumn.pack_start(textrenderer, expand=True)
810                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
811
812                 textrenderer = gtk.CellRendererText()
813                 textrenderer.set_property("yalign", 0)
814                 textrenderer.set_property("scale", 1.5)
815                 self._fromColumn = gtk.TreeViewColumn("From")
816                 self._fromColumn.pack_start(textrenderer, expand=True)
817                 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
818                 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
819
820                 self._window = gtk_toolbox.find_parent_window(self._recentview)
821                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
822
823                 self._updateSink = gtk_toolbox.threaded_stage(
824                         gtk_toolbox.comap(
825                                 self._idly_populate_recentview,
826                                 gtk_toolbox.null_sink(),
827                         )
828                 )
829
830         def enable(self):
831                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
832                 self._recentview.set_model(self._recentmodel)
833
834                 self._recentview.append_column(self._dateColumn)
835                 self._recentview.append_column(self._actionColumn)
836                 self._recentview.append_column(self._fromColumn)
837                 self._recentviewselection = self._recentview.get_selection()
838                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
839
840                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
841
842         def disable(self):
843                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
844
845                 self.clear()
846
847                 self._recentview.remove_column(self._dateColumn)
848                 self._recentview.remove_column(self._actionColumn)
849                 self._recentview.remove_column(self._fromColumn)
850                 self._recentview.set_model(None)
851
852         def number_selected(self, action, number, message):
853                 """
854                 @note Actual dial function is patched in later
855                 """
856                 raise NotImplementedError("Horrible unknown error has occurred")
857
858         def update(self, force = False):
859                 if not force and self._isPopulated:
860                         return False
861                 self._updateSink.send(())
862                 return True
863
864         def clear(self):
865                 self._isPopulated = False
866                 self._recentmodel.clear()
867
868         @staticmethod
869         def name():
870                 return "Recent Calls"
871
872         def load_settings(self, config, section):
873                 pass
874
875         def save_settings(self, config, section):
876                 """
877                 @note Thread Agnostic
878                 """
879                 pass
880
881         def _idly_populate_recentview(self):
882                 self._recentmodel.clear()
883                 self._isPopulated = True
884
885                 try:
886                         recentItems = self._backend.get_recent()
887                 except StandardError, e:
888                         self._errorDisplay.push_exception_with_lock()
889                         self._isPopulated = False
890                         recentItems = []
891
892                 for personName, phoneNumber, date, action in recentItems:
893                         if not personName:
894                                 personName = "Unknown"
895                         prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
896                         prettyNumber = make_pretty(prettyNumber)
897                         description = "%s - %s" % (personName, prettyNumber)
898                         item = (phoneNumber, date, action.capitalize(), description)
899                         with gtk_toolbox.gtk_lock():
900                                 self._recentmodel.append(item)
901
902                 return False
903
904         def _on_recentview_row_activated(self, treeview, path, view_column):
905                 model, itr = self._recentviewselection.get_selected()
906                 if not itr:
907                         return
908
909                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
910                 number = make_ugly(number)
911                 contactPhoneNumbers = [("Phone", number)]
912                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
913
914                 action, phoneNumber, message = self._phoneTypeSelector.run(
915                         contactPhoneNumbers,
916                         message = description,
917                         parent = self._window,
918                 )
919                 if action == PhoneTypeSelector.ACTION_CANCEL:
920                         return
921                 assert phoneNumber, "A lack of phone number exists"
922
923                 self.number_selected(action, phoneNumber, message)
924                 self._recentviewselection.unselect_all()
925
926
927 class MessagesView(object):
928
929         NUMBER_IDX = 0
930         DATE_IDX = 1
931         HEADER_IDX = 2
932         MESSAGE_IDX = 3
933
934         def __init__(self, widgetTree, backend, errorDisplay):
935                 self._errorDisplay = errorDisplay
936                 self._backend = backend
937
938                 self._isPopulated = False
939                 self._messagemodel = gtk.ListStore(
940                         gobject.TYPE_STRING, # number
941                         gobject.TYPE_STRING, # date
942                         gobject.TYPE_STRING, # header
943                         gobject.TYPE_STRING, # message
944                 )
945                 self._messageview = widgetTree.get_widget("messages_view")
946                 self._messageviewselection = None
947                 self._onMessageviewRowActivatedId = 0
948
949                 dateRenderer = gtk.CellRendererText()
950                 dateRenderer.set_property("yalign", 0)
951                 self._dateColumn = gtk.TreeViewColumn("Date")
952                 self._dateColumn.pack_start(dateRenderer, expand=True)
953                 self._dateColumn.add_attribute(dateRenderer, "markup", self.DATE_IDX)
954
955                 self._nameRenderer = gtk.CellRendererText()
956                 self._nameRenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
957                 self._nameRenderer.set_property("yalign", 0)
958                 self._headerColumn = gtk.TreeViewColumn("From")
959                 self._headerColumn.pack_start(self._nameRenderer, expand=True)
960                 self._headerColumn.add_attribute(self._nameRenderer, "markup", self.HEADER_IDX)
961
962                 self._messageRenderer = gtk.CellRendererText()
963                 self._messageRenderer.set_property("yalign", 0)
964                 self._messageRenderer.set_property("scale", 1.5)
965                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
966                 self._messageRenderer.set_property("wrap-width", 500)
967                 self._messageColumn = gtk.TreeViewColumn("Messages")
968                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
969                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
970                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
971
972                 self._window = gtk_toolbox.find_parent_window(self._messageview)
973                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
974
975                 self._updateSink = gtk_toolbox.threaded_stage(
976                         gtk_toolbox.comap(
977                                 self._idly_populate_messageview,
978                                 gtk_toolbox.null_sink(),
979                         )
980                 )
981
982         def enable(self):
983                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
984                 self._messageview.set_model(self._messagemodel)
985
986                 self._messageview.append_column(self._dateColumn)
987                 self._messageview.append_column(self._headerColumn)
988                 self._messageview.append_column(self._messageColumn)
989                 self._messageviewselection = self._messageview.get_selection()
990                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
991
992                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
993
994         def disable(self):
995                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
996
997                 self.clear()
998
999                 self._messageview.remove_column(self._dateColumn)
1000                 self._messageview.remove_column(self._headerColumn)
1001                 self._messageview.remove_column(self._messageColumn)
1002                 self._messageview.set_model(None)
1003
1004         def number_selected(self, action, number, message):
1005                 """
1006                 @note Actual dial function is patched in later
1007                 """
1008                 raise NotImplementedError("Horrible unknown error has occurred")
1009
1010         def update(self, force = False):
1011                 if not force and self._isPopulated:
1012                         return False
1013                 self._updateSink.send(())
1014                 return True
1015
1016         def clear(self):
1017                 self._isPopulated = False
1018                 self._messagemodel.clear()
1019
1020         @staticmethod
1021         def name():
1022                 return "Messages"
1023
1024         def load_settings(self, config, section):
1025                 pass
1026
1027         def save_settings(self, config, section):
1028                 """
1029                 @note Thread Agnostic
1030                 """
1031                 pass
1032
1033         def _idly_populate_messageview(self):
1034                 self._messagemodel.clear()
1035                 self._isPopulated = True
1036
1037                 try:
1038                         messageItems = self._backend.get_messages()
1039                 except StandardError, e:
1040                         self._errorDisplay.push_exception_with_lock()
1041                         self._isPopulated = False
1042                         messageItems = []
1043
1044                 maxHeaderLength = 0
1045                 combinedHeaderLength = 0
1046                 headerCount = 0
1047                 for header, number, relativeDate, message in messageItems:
1048                         number = make_ugly(number)
1049                         row = (number, relativeDate, header, message)
1050                         with gtk_toolbox.gtk_lock():
1051                                 self._messagemodel.append(row)
1052
1053                         maxHeaderLength = max(maxHeaderLength, len(header))
1054                         combinedHeaderLength += len(header)
1055                         headerCount += 1
1056
1057                 if 0 < headerCount:
1058                         cellWidth = min(int((4 * combinedHeaderLength) / (3 * headerCount)), maxHeaderLength)
1059                 else:
1060                         cellWidth = 10
1061                 with gtk_toolbox.gtk_lock():
1062                         self._nameRenderer.set_property("width-chars", cellWidth)
1063                         cellPosAndDim = self._messageRenderer.get_size(self._messageview, None)
1064                         self._messageRenderer.set_property("wrap-width", cellPosAndDim[2])
1065
1066                 return False
1067
1068         def _on_messageview_row_activated(self, treeview, path, view_column):
1069                 model, itr = self._messageviewselection.get_selected()
1070                 if not itr:
1071                         return
1072
1073                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1074                 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1075
1076                 action, phoneNumber, message = self._phoneTypeSelector.run(
1077                         contactPhoneNumbers,
1078                         message = description,
1079                         parent = self._window,
1080                 )
1081                 if action == PhoneTypeSelector.ACTION_CANCEL:
1082                         return
1083                 assert phoneNumber, "A lock of phone number exists"
1084
1085                 self.number_selected(action, phoneNumber, message)
1086                 self._messageviewselection.unselect_all()
1087
1088
1089 class ContactsView(object):
1090
1091         def __init__(self, widgetTree, backend, errorDisplay):
1092                 self._errorDisplay = errorDisplay
1093                 self._backend = backend
1094
1095                 self._addressBook = None
1096                 self._selectedComboIndex = 0
1097                 self._addressBookFactories = [null_backend.NullAddressBook()]
1098
1099                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1100                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1101
1102                 self._isPopulated = False
1103                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1104                 self._contactsviewselection = None
1105                 self._contactsview = widgetTree.get_widget("contactsview")
1106
1107                 self._contactColumn = gtk.TreeViewColumn("Contact")
1108                 displayContactSource = False
1109                 if displayContactSource:
1110                         textrenderer = gtk.CellRendererText()
1111                         self._contactColumn.pack_start(textrenderer, expand=False)
1112                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1113                 textrenderer = gtk.CellRendererText()
1114                 textrenderer.set_property("scale", 1.5)
1115                 self._contactColumn.pack_start(textrenderer, expand=True)
1116                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1117                 textrenderer = gtk.CellRendererText()
1118                 self._contactColumn.pack_start(textrenderer, expand=True)
1119                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1120                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1121                 self._contactColumn.set_sort_column_id(1)
1122                 self._contactColumn.set_visible(True)
1123
1124                 self._onContactsviewRowActivatedId = 0
1125                 self._onAddressbookComboChangedId = 0
1126                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1127                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1128
1129                 self._updateSink = gtk_toolbox.threaded_stage(
1130                         gtk_toolbox.comap(
1131                                 self._idly_populate_contactsview,
1132                                 gtk_toolbox.null_sink(),
1133                         )
1134                 )
1135
1136         def enable(self):
1137                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1138
1139                 self._contactsview.set_model(self._contactsmodel)
1140                 self._contactsview.append_column(self._contactColumn)
1141                 self._contactsviewselection = self._contactsview.get_selection()
1142                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1143
1144                 self._booksList.clear()
1145                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1146                         if factoryName and bookName:
1147                                 entryName = "%s: %s" % (factoryName, bookName)
1148                         elif factoryName:
1149                                 entryName = factoryName
1150                         elif bookName:
1151                                 entryName = bookName
1152                         else:
1153                                 entryName = "Bad name (%d)" % factoryId
1154                         row = (str(factoryId), bookId, entryName)
1155                         self._booksList.append(row)
1156
1157                 self._booksSelectionBox.set_model(self._booksList)
1158                 cell = gtk.CellRendererText()
1159                 self._booksSelectionBox.pack_start(cell, True)
1160                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1161
1162                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1163                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1164
1165                 if len(self._booksList) <= self._selectedComboIndex:
1166                         self._selectedComboIndex = 0
1167                 self._booksSelectionBox.set_active(self._selectedComboIndex)
1168
1169         def disable(self):
1170                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1171                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1172
1173                 self.clear()
1174
1175                 self._booksSelectionBox.clear()
1176                 self._booksSelectionBox.set_model(None)
1177                 self._contactsview.set_model(None)
1178                 self._contactsview.remove_column(self._contactColumn)
1179
1180         def number_selected(self, action, number, message):
1181                 """
1182                 @note Actual dial function is patched in later
1183                 """
1184                 raise NotImplementedError("Horrible unknown error has occurred")
1185
1186         def get_addressbooks(self):
1187                 """
1188                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1189                 """
1190                 for i, factory in enumerate(self._addressBookFactories):
1191                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1192                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1193
1194         def open_addressbook(self, bookFactoryId, bookId):
1195                 bookFactoryIndex = int(bookFactoryId)
1196                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1197
1198                 forceUpdate = True if addressBook is not self._addressBook else False
1199
1200                 self._addressBook = addressBook
1201                 self.update(force=forceUpdate)
1202
1203         def update(self, force = False):
1204                 if not force and self._isPopulated:
1205                         return False
1206                 self._updateSink.send(())
1207                 return True
1208
1209         def clear(self):
1210                 self._isPopulated = False
1211                 self._contactsmodel.clear()
1212                 for factory in self._addressBookFactories:
1213                         factory.clear_caches()
1214                 self._addressBook.clear_caches()
1215
1216         def append(self, book):
1217                 self._addressBookFactories.append(book)
1218
1219         def extend(self, books):
1220                 self._addressBookFactories.extend(books)
1221
1222         @staticmethod
1223         def name():
1224                 return "Contacts"
1225
1226         def load_settings(self, config, sectionName):
1227                 try:
1228                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1229                 except ConfigParser.NoOptionError:
1230                         self._selectedComboIndex = 0
1231
1232         def save_settings(self, config, sectionName):
1233                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1234
1235         def _idly_populate_contactsview(self):
1236                 addressBook = None
1237                 while addressBook is not self._addressBook:
1238                         addressBook = self._addressBook
1239                         with gtk_toolbox.gtk_lock():
1240                                 self._contactsview.set_model(None)
1241                                 self.clear()
1242
1243                         try:
1244                                 contacts = addressBook.get_contacts()
1245                         except StandardError, e:
1246                                 contacts = []
1247                                 self._isPopulated = False
1248                                 self._errorDisplay.push_exception_with_lock()
1249                         for contactId, contactName in contacts:
1250                                 contactType = (addressBook.contact_source_short_name(contactId), )
1251                                 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1252
1253                         with gtk_toolbox.gtk_lock():
1254                                 self._contactsview.set_model(self._contactsmodel)
1255
1256                 self._isPopulated = True
1257                 return False
1258
1259         def _on_addressbook_combo_changed(self, *args, **kwds):
1260                 itr = self._booksSelectionBox.get_active_iter()
1261                 if itr is None:
1262                         return
1263                 self._selectedComboIndex = self._booksSelectionBox.get_active()
1264                 selectedFactoryId = self._booksList.get_value(itr, 0)
1265                 selectedBookId = self._booksList.get_value(itr, 1)
1266                 self.open_addressbook(selectedFactoryId, selectedBookId)
1267
1268         def _on_contactsview_row_activated(self, treeview, path, view_column):
1269                 model, itr = self._contactsviewselection.get_selected()
1270                 if not itr:
1271                         return
1272
1273                 contactId = self._contactsmodel.get_value(itr, 3)
1274                 contactName = self._contactsmodel.get_value(itr, 1)
1275                 try:
1276                         contactDetails = self._addressBook.get_contact_details(contactId)
1277                 except StandardError, e:
1278                         contactDetails = []
1279                         self._errorDisplay.push_exception()
1280                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1281
1282                 if len(contactPhoneNumbers) == 0:
1283                         return
1284
1285                 action, phoneNumber, message = self._phoneTypeSelector.run(
1286                         contactPhoneNumbers,
1287                         message = contactName,
1288                         parent = self._window,
1289                 )
1290                 if action == PhoneTypeSelector.ACTION_CANCEL:
1291                         return
1292                 assert phoneNumber, "A lack of phone number exists"
1293
1294                 self.number_selected(action, phoneNumber, message)
1295                 self._contactsviewselection.unselect_all()