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