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