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