A SMS bug fix and further work on displaying SMS messages
[gc-dialer] / src / gc_views.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 @todo Look into a messages view
22         @li https://www.google.com/voice/inbox/recent/voicemail/
23         @li https://www.google.com/voice/inbox/recent/sms/
24         Would need to either use both json and html or just html
25 """
26
27 from __future__ import with_statement
28
29 import threading
30 import time
31 import warnings
32
33 import gobject
34 import gtk
35
36 import gtk_toolbox
37
38
39 def make_ugly(prettynumber):
40         """
41         function to take a phone number and strip out all non-numeric
42         characters
43
44         >>> make_ugly("+012-(345)-678-90")
45         '01234567890'
46         """
47         import re
48         uglynumber = re.sub('\D', '', prettynumber)
49         return uglynumber
50
51
52 def make_pretty(phonenumber):
53         """
54         Function to take a phone number and return the pretty version
55         pretty numbers:
56                 if phonenumber begins with 0:
57                         ...-(...)-...-....
58                 if phonenumber begins with 1: ( for gizmo callback numbers )
59                         1 (...)-...-....
60                 if phonenumber is 13 digits:
61                         (...)-...-....
62                 if phonenumber is 10 digits:
63                         ...-....
64         >>> make_pretty("12")
65         '12'
66         >>> make_pretty("1234567")
67         '123-4567'
68         >>> make_pretty("2345678901")
69         '(234)-567-8901'
70         >>> make_pretty("12345678901")
71         '1 (234)-567-8901'
72         >>> make_pretty("01234567890")
73         '+012-(345)-678-90'
74         """
75         if phonenumber is None or phonenumber is "":
76                 return ""
77
78         phonenumber = make_ugly(phonenumber)
79
80         if len(phonenumber) < 3:
81                 return phonenumber
82
83         if phonenumber[0] == "0":
84                 prettynumber = ""
85                 prettynumber += "+%s" % phonenumber[0:3]
86                 if 3 < len(phonenumber):
87                         prettynumber += "-(%s)" % phonenumber[3:6]
88                         if 6 < len(phonenumber):
89                                 prettynumber += "-%s" % phonenumber[6:9]
90                                 if 9 < len(phonenumber):
91                                         prettynumber += "-%s" % phonenumber[9:]
92                 return prettynumber
93         elif len(phonenumber) <= 7:
94                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
95         elif len(phonenumber) > 8 and phonenumber[0] == "1":
96                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
97         elif len(phonenumber) > 7:
98                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
99         return prettynumber
100
101
102 def make_idler(func):
103         """
104         Decorator that makes a generator-function into a function that will continue execution on next call
105         """
106         a = []
107
108         def decorated_func(*args, **kwds):
109                 if not a:
110                         a.append(func(*args, **kwds))
111                 try:
112                         a[0].next()
113                         return True
114                 except StopIteration:
115                         del a[:]
116                         return False
117
118         decorated_func.__name__ = func.__name__
119         decorated_func.__doc__ = func.__doc__
120         decorated_func.__dict__.update(func.__dict__)
121
122         return decorated_func
123
124
125 class DummyAddressBook(object):
126         """
127         Minimal example of both an addressbook factory and an addressbook
128         """
129
130         def clear_caches(self):
131                 pass
132
133         def get_addressbooks(self):
134                 """
135                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
136                 """
137                 yield self, "", "None"
138
139         def open_addressbook(self, bookId):
140                 return self
141
142         @staticmethod
143         def contact_source_short_name(contactId):
144                 return ""
145
146         @staticmethod
147         def factory_name():
148                 return ""
149
150         @staticmethod
151         def get_contacts():
152                 """
153                 @returns Iterable of (contact id, contact name)
154                 """
155                 return []
156
157         @staticmethod
158         def get_contact_details(contactId):
159                 """
160                 @returns Iterable of (Phone Type, Phone Number)
161                 """
162                 return []
163
164
165 class MergedAddressBook(object):
166         """
167         Merger of all addressbooks
168         """
169
170         def __init__(self, addressbookFactories, sorter = None):
171                 self.__addressbookFactories = addressbookFactories
172                 self.__addressbooks = None
173                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
174
175         def clear_caches(self):
176                 self.__addressbooks = None
177                 for factory in self.__addressbookFactories:
178                         factory.clear_caches()
179
180         def get_addressbooks(self):
181                 """
182                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
183                 """
184                 yield self, "", ""
185
186         def open_addressbook(self, bookId):
187                 return self
188
189         def contact_source_short_name(self, contactId):
190                 if self.__addressbooks is None:
191                         return ""
192                 bookIndex, originalId = contactId.split("-", 1)
193                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
194
195         @staticmethod
196         def factory_name():
197                 return "All Contacts"
198
199         def get_contacts(self):
200                 """
201                 @returns Iterable of (contact id, contact name)
202                 """
203                 if self.__addressbooks is None:
204                         self.__addressbooks = list(
205                                 factory.open_addressbook(id)
206                                 for factory in self.__addressbookFactories
207                                 for (f, id, name) in factory.get_addressbooks()
208                         )
209                 contacts = (
210                         ("-".join([str(bookIndex), contactId]), contactName)
211                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
212                                         for (contactId, contactName) in addressbook.get_contacts()
213                 )
214                 sortedContacts = self.__sort_contacts(contacts)
215                 return sortedContacts
216
217         def get_contact_details(self, contactId):
218                 """
219                 @returns Iterable of (Phone Type, Phone Number)
220                 """
221                 if self.__addressbooks is None:
222                         return []
223                 bookIndex, originalId = contactId.split("-", 1)
224                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
225
226         @staticmethod
227         def null_sorter(contacts):
228                 """
229                 Good for speed/low memory
230                 """
231                 return contacts
232
233         @staticmethod
234         def basic_firtname_sorter(contacts):
235                 """
236                 Expects names in "First Last" format
237                 """
238                 contactsWithKey = [
239                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
240                                 for (contactId, contactName) in contacts
241                 ]
242                 contactsWithKey.sort()
243                 return (contactData for (lastName, contactData) in contactsWithKey)
244
245         @staticmethod
246         def basic_lastname_sorter(contacts):
247                 """
248                 Expects names in "First Last" format
249                 """
250                 contactsWithKey = [
251                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
252                                 for (contactId, contactName) in contacts
253                 ]
254                 contactsWithKey.sort()
255                 return (contactData for (lastName, contactData) in contactsWithKey)
256
257         @staticmethod
258         def reversed_firtname_sorter(contacts):
259                 """
260                 Expects names in "Last, First" format
261                 """
262                 contactsWithKey = [
263                         (contactName.split(", ", 1)[-1], (contactId, contactName))
264                                 for (contactId, contactName) in contacts
265                 ]
266                 contactsWithKey.sort()
267                 return (contactData for (lastName, contactData) in contactsWithKey)
268
269         @staticmethod
270         def reversed_lastname_sorter(contacts):
271                 """
272                 Expects names in "Last, First" format
273                 """
274                 contactsWithKey = [
275                         (contactName.split(", ", 1)[0], (contactId, contactName))
276                                 for (contactId, contactName) in contacts
277                 ]
278                 contactsWithKey.sort()
279                 return (contactData for (lastName, contactData) in contactsWithKey)
280
281         @staticmethod
282         def guess_firstname(name):
283                 if ", " in name:
284                         return name.split(", ", 1)[-1]
285                 else:
286                         return name.rsplit(" ", 1)[0]
287
288         @staticmethod
289         def guess_lastname(name):
290                 if ", " in name:
291                         return name.split(", ", 1)[0]
292                 else:
293                         return name.rsplit(" ", 1)[-1]
294
295         @classmethod
296         def advanced_firstname_sorter(cls, contacts):
297                 contactsWithKey = [
298                         (cls.guess_firstname(contactName), (contactId, contactName))
299                                 for (contactId, contactName) in contacts
300                 ]
301                 contactsWithKey.sort()
302                 return (contactData for (lastName, contactData) in contactsWithKey)
303
304         @classmethod
305         def advanced_lastname_sorter(cls, contacts):
306                 contactsWithKey = [
307                         (cls.guess_lastname(contactName), (contactId, contactName))
308                                 for (contactId, contactName) in contacts
309                 ]
310                 contactsWithKey.sort()
311                 return (contactData for (lastName, contactData) in contactsWithKey)
312
313
314 class PhoneTypeSelector(object):
315
316         ACTION_CANCEL = "cancel"
317         ACTION_SELECT = "select"
318         ACTION_DIAL = "dial"
319         ACTION_SEND_SMS = "sms"
320
321         def __init__(self, widgetTree, gcBackend):
322                 self._gcBackend = gcBackend
323                 self._widgetTree = widgetTree
324
325                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
326                 self._smsDialog = SmsEntryDialog(self._widgetTree, self._gcBackend)
327
328                 self._smsButton = self._widgetTree.get_widget("sms_button")
329                 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
330
331                 self._dialButton = self._widgetTree.get_widget("dial_button")
332                 self._dialButton.connect("clicked", self._on_phonetype_dial)
333
334                 self._selectButton = self._widgetTree.get_widget("select_button")
335                 self._selectButton.connect("clicked", self._on_phonetype_select)
336
337                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
338                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
339
340                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
341                 self._typeviewselection = None
342
343                 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
344                 typeview = self._widgetTree.get_widget("phonetypes")
345                 typeview.connect("row-activated", self._on_phonetype_select)
346                 typeview.set_model(self._typemodel)
347                 textrenderer = gtk.CellRendererText()
348
349                 # Add the column to the treeview
350                 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
351                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
352
353                 typeview.append_column(column)
354
355                 self._typeviewselection = typeview.get_selection()
356                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
357
358                 self._action = self.ACTION_CANCEL
359
360         def run(self, contactDetails, message = ""):
361                 self._typemodel.clear()
362
363                 for phoneType, phoneNumber in contactDetails:
364                         # @bug this isn't populating correctly for recent and messages but it is for contacts
365                         print repr(phoneNumber), repr(phoneType)
366                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
367
368                 # @todo Need to decide how how to handle the single phone number case
369                 if message:
370                         self._message.set_markup(message)
371                         self._message.show()
372                 else:
373                         self._message.set_markup("")
374                         self._message.hide()
375
376                 userResponse = self._dialog.run()
377
378                 if userResponse == gtk.RESPONSE_OK:
379                         phoneNumber = self._get_number()
380                 else:
381                         phoneNumber = ""
382                 if not phoneNumber:
383                         self._action = self.ACTION_CANCEL
384
385                 if self._action == self.ACTION_SEND_SMS:
386                         smsMessage = self._smsDialog.run(phoneNumber, message)
387                 else:
388                         smsMessage = ""
389                 if not smsMessage:
390                         phoneNumber = ""
391                         self._action = self.ACTION_CANCEL
392
393                 self._typeviewselection.unselect_all()
394                 self._dialog.hide()
395                 return self._action, phoneNumber, smsMessage
396
397         def _get_number(self):
398                 model, itr = self._typeviewselection.get_selected()
399                 if not itr:
400                         return ""
401
402                 phoneNumber = self._typemodel.get_value(itr, 0)
403                 return phoneNumber
404
405         def _on_phonetype_dial(self, *args):
406                 self._dialog.response(gtk.RESPONSE_OK)
407                 self._action = self.ACTION_DIAL
408
409         def _on_phonetype_send_sms(self, *args):
410                 self._dialog.response(gtk.RESPONSE_OK)
411                 self._action = self.ACTION_SEND_SMS
412
413         def _on_phonetype_select(self, *args):
414                 self._dialog.response(gtk.RESPONSE_OK)
415                 self._action = self.ACTION_SELECT
416
417         def _on_phonetype_cancel(self, *args):
418                 self._dialog.response(gtk.RESPONSE_CANCEL)
419                 self._action = self.ACTION_CANCEL
420
421
422 class SmsEntryDialog(object):
423
424         MAX_CHAR = 160
425
426         def __init__(self, widgetTree, gcBackend):
427                 self._gcBackend = gcBackend
428                 self._widgetTree = widgetTree
429                 self._dialog = self._widgetTree.get_widget("smsDialog")
430
431                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
432                 self._smsButton.connect("clicked", self._on_send)
433
434                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
435                 self._cancelButton.connect("clicked", self._on_cancel)
436
437                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
438                 self._message = self._widgetTree.get_widget("smsMessage")
439                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
440                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
441
442         def run(self, number, message = ""):
443                 if message:
444                         self._message.set_markup(message)
445                         self._message.show()
446                 else:
447                         self._message.set_markup("")
448                         self._message.hide()
449                 self._smsEntry.get_buffer().set_text("")
450                 self._update_letter_count()
451
452                 userResponse = self._dialog.run()
453                 if userResponse == gtk.RESPONSE_OK:
454                         entryBuffer = self._smsEntry.get_buffer()
455                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
456                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
457                 else:
458                         enteredMessage = ""
459
460                 self._dialog.hide()
461                 return enteredMessage
462
463         def _update_letter_count(self, *args):
464                 entryLength = self._smsEntry.get_buffer().get_char_count()
465                 self._letterCountLabel.set_text(str(self.MAX_CHAR - entryLength))
466
467         def _on_entry_changed(self, *args):
468                 self._update_letter_count()
469
470         def _on_send(self, *args):
471                 self._dialog.response(gtk.RESPONSE_OK)
472
473         def _on_cancel(self, *args):
474                 self._dialog.response(gtk.RESPONSE_CANCEL)
475
476
477 class Dialpad(object):
478
479         def __init__(self, widgetTree, errorDisplay):
480                 self._errorDisplay = errorDisplay
481                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
482                 self._dialButton = widgetTree.get_widget("dial")
483                 self._phonenumber = ""
484                 self._prettynumber = ""
485                 self._clearall_id = None
486
487                 callbackMapping = {
488                         "on_dial_clicked": self._on_dial_clicked,
489                         "on_digit_clicked": self._on_digit_clicked,
490                         "on_clear_number": self._on_clear_number,
491                         "on_back_clicked": self._on_backspace,
492                         "on_back_pressed": self._on_back_pressed,
493                         "on_back_released": self._on_back_released,
494                 }
495                 widgetTree.signal_autoconnect(callbackMapping)
496
497         def enable(self):
498                 self._dialButton.grab_focus()
499
500         def disable(self):
501                 pass
502
503         def dial(self, number):
504                 """
505                 @note Actual dial function is patched in later
506                 """
507                 raise NotImplementedError
508
509         def get_number(self):
510                 return self._phonenumber
511
512         def set_number(self, number):
513                 """
514                 Set the callback phonenumber
515                 """
516                 try:
517                         self._phonenumber = make_ugly(number)
518                         self._prettynumber = make_pretty(self._phonenumber)
519                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
520                 except TypeError, e:
521                         self._errorDisplay.push_exception(e)
522
523         def clear(self):
524                 self.set_number("")
525
526         def _on_dial_clicked(self, widget):
527                 self.dial(self.get_number())
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, widget):
536                 self.set_number(self._phonenumber[:-1])
537
538         def _on_clearall(self):
539                 self.clear()
540                 return False
541
542         def _on_back_pressed(self, widget):
543                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
544
545         def _on_back_released(self, widget):
546                 if self._clearall_id is not None:
547                         gobject.source_remove(self._clearall_id)
548                 self._clearall_id = None
549
550
551 class AccountInfo(object):
552
553         def __init__(self, widgetTree, backend, errorDisplay):
554                 self._errorDisplay = errorDisplay
555                 self._backend = backend
556
557                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
558                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
559                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
560                 self._onCallbackentryChangedId = 0
561
562         def enable(self):
563                 assert self._backend.is_authed()
564                 self._accountViewNumberDisplay.set_use_markup(True)
565                 self.set_account_number("")
566                 self._callbackList.clear()
567                 self.update()
568                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
569
570         def disable(self):
571                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
572                 self.clear()
573                 self._callbackList.clear()
574
575         def get_selected_callback_number(self):
576                 return make_ugly(self._callbackCombo.get_child().get_text())
577
578         def set_account_number(self, number):
579                 """
580                 Displays current account number
581                 """
582                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
583
584         def update(self):
585                 self.populate_callback_combo()
586                 self.set_account_number(self._backend.get_account_number())
587
588         def clear(self):
589                 self._callbackCombo.get_child().set_text("")
590                 self.set_account_number("")
591
592         def populate_callback_combo(self):
593                 self._callbackList.clear()
594                 try:
595                         callbackNumbers = self._backend.get_callback_numbers()
596                 except RuntimeError, e:
597                         self._errorDisplay.push_exception(e)
598                         return
599
600                 for number, description in callbackNumbers.iteritems():
601                         self._callbackList.append((make_pretty(number),))
602
603                 self._callbackCombo.set_model(self._callbackList)
604                 self._callbackCombo.set_text_column(0)
605                 try:
606                         callbackNumber = self._backend.get_callback_number()
607                 except RuntimeError, e:
608                         self._errorDisplay.push_exception(e)
609                         return
610                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
611
612         def _on_callbackentry_changed(self, *args):
613                 """
614                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
615                 """
616                 try:
617                         text = self.get_selected_callback_number()
618                         if not self._backend.is_valid_syntax(text):
619                                 self._errorDisplay.push_message("%s is not a valid callback number" % text)
620                         elif text == self._backend.get_callback_number():
621                                 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
622                         else:
623                                 self._backend.set_callback_number(text)
624                 except RuntimeError, e:
625                         self._errorDisplay.push_exception(e)
626
627
628 class RecentCallsView(object):
629
630         def __init__(self, widgetTree, backend, errorDisplay):
631                 self._errorDisplay = errorDisplay
632                 self._backend = backend
633
634                 self._recenttime = 0.0
635                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
636                 self._recentview = widgetTree.get_widget("recentview")
637                 self._recentviewselection = None
638                 self._onRecentviewRowActivatedId = 0
639
640                 textrenderer = gtk.CellRendererText()
641                 # @todo Make seperate columns for each item in recent item payload
642                 self._recentviewColumn = gtk.TreeViewColumn("Calls")
643                 self._recentviewColumn.pack_start(textrenderer, expand=True)
644                 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
645                 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
646
647                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
648
649         def enable(self):
650                 assert self._backend.is_authed()
651                 self._recentview.set_model(self._recentmodel)
652
653                 self._recentview.append_column(self._recentviewColumn)
654                 self._recentviewselection = self._recentview.get_selection()
655                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
656
657                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
658
659         def disable(self):
660                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
661                 self._recentview.remove_column(self._recentviewColumn)
662                 self._recentview.set_model(None)
663
664         def number_selected(self, action, number, message):
665                 """
666                 @note Actual dial function is patched in later
667                 """
668                 raise NotImplementedError
669
670         def update(self):
671                 if (time.time() - self._recenttime) < 300:
672                         return
673                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
674                 backgroundPopulate.setDaemon(True)
675                 backgroundPopulate.start()
676
677         def clear(self):
678                 self._recenttime = 0.0
679                 self._recentmodel.clear()
680
681         def _idly_populate_recentview(self):
682                 self._recenttime = time.time()
683                 self._recentmodel.clear()
684
685                 try:
686                         recentItems = self._backend.get_recent()
687                 except RuntimeError, e:
688                         self._errorDisplay.push_exception_with_lock(e)
689                         self._recenttime = 0.0
690                         recentItems = []
691
692                 for personsName, phoneNumber, date, action in recentItems:
693                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
694                         item = (phoneNumber, description)
695                         with gtk_toolbox.gtk_lock():
696                                 self._recentmodel.append(item)
697
698                 return False
699
700         def _on_recentview_row_activated(self, treeview, path, view_column):
701                 model, itr = self._recentviewselection.get_selected()
702                 if not itr:
703                         return
704
705                 number = self._recentmodel.get_value(itr, 0)
706                 number = make_ugly(number)
707                 contactPhoneNumbers = [("Phone", number)]
708                 description = self._recentmodel.get_value(itr, 1)
709                 print "Activated Recent Row:", repr(contactPhoneNumbers), repr(description)
710
711                 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
712                 if action == PhoneTypeSelector.ACTION_CANCEL:
713                         return
714                 assert phoneNumber
715
716                 self.number_selected(action, phoneNumber, message)
717                 self._recentviewselection.unselect_all()
718
719
720 class MessagesView(object):
721
722         def __init__(self, widgetTree, backend, errorDisplay):
723                 self._errorDisplay = errorDisplay
724                 self._backend = backend
725
726                 self._messagetime = 0.0
727                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
728                 self._messageview = widgetTree.get_widget("messages_view")
729                 self._messageviewselection = None
730                 self._onMessageviewRowActivatedId = 0
731
732                 textrenderer = gtk.CellRendererText()
733                 # @todo Make seperate columns for each item in message payload
734                 self._messageviewColumn = gtk.TreeViewColumn("Messages")
735                 self._messageviewColumn.pack_start(textrenderer, expand=True)
736                 self._messageviewColumn.add_attribute(textrenderer, "markup", 1)
737                 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
738
739                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
740
741         def enable(self):
742                 assert self._backend.is_authed()
743                 self._messageview.set_model(self._messagemodel)
744
745                 self._messageview.append_column(self._messageviewColumn)
746                 self._messageviewselection = self._messageview.get_selection()
747                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
748
749                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
750
751         def disable(self):
752                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
753                 self._messageview.remove_column(self._messageviewColumn)
754                 self._messageview.set_model(None)
755
756         def number_selected(self, action, number, message):
757                 """
758                 @note Actual dial function is patched in later
759                 """
760                 raise NotImplementedError
761
762         def update(self):
763                 if (time.time() - self._messagetime) < 300:
764                         return
765                 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
766                 backgroundPopulate.setDaemon(True)
767                 backgroundPopulate.start()
768
769         def clear(self):
770                 self._messagetime = 0.0
771                 self._messagemodel.clear()
772
773         def _idly_populate_messageview(self):
774                 self._messagetime = time.time()
775                 self._messagemodel.clear()
776
777                 try:
778                         messageItems = self._backend.get_messages()
779                 except RuntimeError, e:
780                         self._errorDisplay.push_exception_with_lock(e)
781                         self._messagetime = 0.0
782                         messageItems = []
783
784                 for header, number, relativeDate, message in messageItems:
785                         number = make_ugly(number)
786                         print "Discarding", header, relativeDate
787                         item = (number, message)
788                         with gtk_toolbox.gtk_lock():
789                                 self._messagemodel.append(item)
790
791                 return False
792
793         def _on_messageview_row_activated(self, treeview, path, view_column):
794                 model, itr = self._messageviewselection.get_selected()
795                 if not itr:
796                         return
797
798                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
799                 description = self._messagemodel.get_value(itr, 1)
800                 print repr(contactPhoneNumbers), repr(description)
801
802                 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
803                 if action == PhoneTypeSelector.ACTION_CANCEL:
804                         return
805                 assert phoneNumber
806
807                 self.number_selected(action, phoneNumber, message)
808                 self._messageviewselection.unselect_all()
809
810
811 class ContactsView(object):
812
813         def __init__(self, widgetTree, backend, errorDisplay):
814                 self._errorDisplay = errorDisplay
815                 self._backend = backend
816
817                 self._addressBook = None
818                 self._addressBookFactories = [DummyAddressBook()]
819
820                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
821                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
822
823                 self._contactstime = 0.0
824                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
825                 self._contactsviewselection = None
826                 self._contactsview = widgetTree.get_widget("contactsview")
827
828                 self._contactColumn = gtk.TreeViewColumn("Contact")
829                 displayContactSource = False
830                 if displayContactSource:
831                         textrenderer = gtk.CellRendererText()
832                         self._contactColumn.pack_start(textrenderer, expand=False)
833                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
834                 textrenderer = gtk.CellRendererText()
835                 self._contactColumn.pack_start(textrenderer, expand=True)
836                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
837                 textrenderer = gtk.CellRendererText()
838                 self._contactColumn.pack_start(textrenderer, expand=True)
839                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
840                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
841                 self._contactColumn.set_sort_column_id(1)
842                 self._contactColumn.set_visible(True)
843
844                 self._onContactsviewRowActivatedId = 0
845                 self._onAddressbookComboChangedId = 0
846                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
847
848         def enable(self):
849                 assert self._backend.is_authed()
850
851                 self._contactsview.set_model(self._contactsmodel)
852                 self._contactsview.append_column(self._contactColumn)
853                 self._contactsviewselection = self._contactsview.get_selection()
854                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
855
856                 self._booksList.clear()
857                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
858                         if factoryName and bookName:
859                                 entryName = "%s: %s" % (factoryName, bookName)
860                         elif factoryName:
861                                 entryName = factoryName
862                         elif bookName:
863                                 entryName = bookName
864                         else:
865                                 entryName = "Bad name (%d)" % factoryId
866                         row = (str(factoryId), bookId, entryName)
867                         self._booksList.append(row)
868
869                 self._booksSelectionBox.set_model(self._booksList)
870                 cell = gtk.CellRendererText()
871                 self._booksSelectionBox.pack_start(cell, True)
872                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
873                 self._booksSelectionBox.set_active(0)
874
875                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
876                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
877
878         def disable(self):
879                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
880                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
881
882                 self._booksSelectionBox.clear()
883                 self._booksSelectionBox.set_model(None)
884                 self._contactsview.set_model(None)
885                 self._contactsview.remove_column(self._contactColumn)
886
887         def number_selected(self, action, number, message):
888                 """
889                 @note Actual dial function is patched in later
890                 """
891                 raise NotImplementedError
892
893         def get_addressbooks(self):
894                 """
895                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
896                 """
897                 for i, factory in enumerate(self._addressBookFactories):
898                         for bookFactory, bookId, bookName in factory.get_addressbooks():
899                                 yield (i, bookId), (factory.factory_name(), bookName)
900
901         def open_addressbook(self, bookFactoryId, bookId):
902                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
903                 self._contactstime = 0
904                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
905                 backgroundPopulate.setDaemon(True)
906                 backgroundPopulate.start()
907
908         def update(self):
909                 if (time.time() - self._contactstime) < 300:
910                         return
911                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
912                 backgroundPopulate.setDaemon(True)
913                 backgroundPopulate.start()
914
915         def clear(self):
916                 self._contactstime = 0.0
917                 self._contactsmodel.clear()
918
919         def clear_caches(self):
920                 for factory in self._addressBookFactories:
921                         factory.clear_caches()
922                 self._addressBook.clear_caches()
923
924         def append(self, book):
925                 self._addressBookFactories.append(book)
926
927         def extend(self, books):
928                 self._addressBookFactories.extend(books)
929
930         def _idly_populate_contactsview(self):
931                 #@todo Add a lock so only one code path can be in here at a time
932                 self.clear()
933
934                 # completely disable updating the treeview while we populate the data
935                 self._contactsview.freeze_child_notify()
936                 self._contactsview.set_model(None)
937
938                 addressBook = self._addressBook
939                 try:
940                         contacts = addressBook.get_contacts()
941                 except RuntimeError, e:
942                         contacts = []
943                         self._contactstime = 0.0
944                         self._errorDisplay.push_exception_with_lock(e)
945                 for contactId, contactName in contacts:
946                         contactType = (addressBook.contact_source_short_name(contactId), )
947                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
948
949                 # restart the treeview data rendering
950                 self._contactsview.set_model(self._contactsmodel)
951                 self._contactsview.thaw_child_notify()
952                 return False
953
954         def _on_addressbook_combo_changed(self, *args, **kwds):
955                 itr = self._booksSelectionBox.get_active_iter()
956                 if itr is None:
957                         return
958                 factoryId = int(self._booksList.get_value(itr, 0))
959                 bookId = self._booksList.get_value(itr, 1)
960                 self.open_addressbook(factoryId, bookId)
961
962         def _on_contactsview_row_activated(self, treeview, path, view_column):
963                 model, itr = self._contactsviewselection.get_selected()
964                 if not itr:
965                         return
966
967                 contactId = self._contactsmodel.get_value(itr, 3)
968                 contactName = self._contactsmodel.get_value(itr, 1)
969                 try:
970                         contactDetails = self._addressBook.get_contact_details(contactId)
971                 except RuntimeError, e:
972                         contactDetails = []
973                         self._contactstime = 0.0
974                         self._errorDisplay.push_exception(e)
975                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
976
977                 if len(contactPhoneNumbers) == 0:
978                         return
979
980                 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
981                 if action == PhoneTypeSelector.ACTION_CANCEL:
982                         return
983                 assert phoneNumber
984
985                 self.number_selected(action, phoneNumber, message)
986                 self._contactsviewselection.unselect_all()