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