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