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