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