0b841adc2168f59a7ff8c0764ab68b5d06c91c11
[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                 self._typeview = self._widgetTree.get_widget("phonetypes")
340                 self._typeview.connect("row-activated", self._on_phonetype_select)
341
342                 self._action = self.ACTION_CANCEL
343
344         def run(self, contactDetails, message = ""):
345                 self._action = self.ACTION_CANCEL
346                 self._typemodel.clear()
347                 self._typeview.set_model(self._typemodel)
348
349                 # Add the column to the treeview
350                 textrenderer = gtk.CellRendererText()
351                 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
352                 self._typeview.append_column(numberColumn)
353
354                 textrenderer = gtk.CellRendererText()
355                 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
356                 self._typeview.append_column(typeColumn)
357
358                 self._typeviewselection = self._typeview.get_selection()
359                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
360
361                 for phoneType, phoneNumber in contactDetails:
362                         display = " - ".join((phoneNumber, phoneType))
363                         display = phoneType
364                         row = (phoneNumber, display)
365                         self._typemodel.append(row)
366
367                 # @todo Need to decide how how to handle the single phone number case
368                 if message:
369                         self._message.set_markup(message)
370                         self._message.show()
371                 else:
372                         self._message.set_markup("")
373                         self._message.hide()
374
375                 userResponse = self._dialog.run()
376
377                 if userResponse == gtk.RESPONSE_OK:
378                         phoneNumber = self._get_number()
379                 else:
380                         phoneNumber = ""
381                 if not phoneNumber:
382                         self._action = self.ACTION_CANCEL
383
384                 if self._action == self.ACTION_SEND_SMS:
385                         smsMessage = self._smsDialog.run(phoneNumber, message)
386                 else:
387                         smsMessage = ""
388                 if not smsMessage:
389                         phoneNumber = ""
390                         self._action = self.ACTION_CANCEL
391
392                 self._typeviewselection.unselect_all()
393                 self._typeview.remove_column(numberColumn)
394                 self._typeview.remove_column(typeColumn)
395                 self._typeview.set_model(None)
396                 self._dialog.hide()
397                 return self._action, phoneNumber, smsMessage
398
399         def _get_number(self):
400                 model, itr = self._typeviewselection.get_selected()
401                 if not itr:
402                         return ""
403
404                 phoneNumber = self._typemodel.get_value(itr, 0)
405                 return phoneNumber
406
407         def _on_phonetype_dial(self, *args):
408                 self._dialog.response(gtk.RESPONSE_OK)
409                 self._action = self.ACTION_DIAL
410
411         def _on_phonetype_send_sms(self, *args):
412                 self._dialog.response(gtk.RESPONSE_OK)
413                 self._action = self.ACTION_SEND_SMS
414
415         def _on_phonetype_select(self, *args):
416                 self._dialog.response(gtk.RESPONSE_OK)
417                 self._action = self.ACTION_SELECT
418
419         def _on_phonetype_cancel(self, *args):
420                 self._dialog.response(gtk.RESPONSE_CANCEL)
421                 self._action = self.ACTION_CANCEL
422
423
424 class SmsEntryDialog(object):
425
426         MAX_CHAR = 160
427
428         def __init__(self, widgetTree, gcBackend):
429                 self._gcBackend = gcBackend
430                 self._widgetTree = widgetTree
431                 self._dialog = self._widgetTree.get_widget("smsDialog")
432
433                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
434                 self._smsButton.connect("clicked", self._on_send)
435
436                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
437                 self._cancelButton.connect("clicked", self._on_cancel)
438
439                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
440                 self._message = self._widgetTree.get_widget("smsMessage")
441                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
442                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
443
444         def run(self, number, message = ""):
445                 if message:
446                         self._message.set_markup(message)
447                         self._message.show()
448                 else:
449                         self._message.set_markup("")
450                         self._message.hide()
451                 self._smsEntry.get_buffer().set_text("")
452                 self._update_letter_count()
453
454                 userResponse = self._dialog.run()
455                 if userResponse == gtk.RESPONSE_OK:
456                         entryBuffer = self._smsEntry.get_buffer()
457                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
458                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
459                 else:
460                         enteredMessage = ""
461
462                 self._dialog.hide()
463                 return enteredMessage
464
465         def _update_letter_count(self, *args):
466                 entryLength = self._smsEntry.get_buffer().get_char_count()
467                 charsLeft = self.MAX_CHAR - entryLength
468                 self._letterCountLabel.set_text(str(charsLeft))
469                 if charsLeft < 0:
470                         self._smsButton.set_sensitive(False)
471                 else:
472                         self._smsButton.set_sensitive(True)
473
474         def _on_entry_changed(self, *args):
475                 self._update_letter_count()
476
477         def _on_send(self, *args):
478                 self._dialog.response(gtk.RESPONSE_OK)
479
480         def _on_cancel(self, *args):
481                 self._dialog.response(gtk.RESPONSE_CANCEL)
482
483
484 class Dialpad(object):
485
486         def __init__(self, widgetTree, errorDisplay):
487                 self._errorDisplay = errorDisplay
488                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
489                 self._dialButton = widgetTree.get_widget("dial")
490                 self._phonenumber = ""
491                 self._prettynumber = ""
492                 self._clearall_id = None
493
494                 callbackMapping = {
495                         "on_dial_clicked": self._on_dial_clicked,
496                         "on_digit_clicked": self._on_digit_clicked,
497                         "on_clear_number": self._on_clear_number,
498                         "on_back_clicked": self._on_backspace,
499                         "on_back_pressed": self._on_back_pressed,
500                         "on_back_released": self._on_back_released,
501                 }
502                 widgetTree.signal_autoconnect(callbackMapping)
503
504         def enable(self):
505                 self._dialButton.grab_focus()
506
507         def disable(self):
508                 pass
509
510         def dial(self, number):
511                 """
512                 @note Actual dial function is patched in later
513                 """
514                 raise NotImplementedError
515
516         def get_number(self):
517                 return self._phonenumber
518
519         def set_number(self, number):
520                 """
521                 Set the callback phonenumber
522                 """
523                 try:
524                         self._phonenumber = make_ugly(number)
525                         self._prettynumber = make_pretty(self._phonenumber)
526                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
527                 except TypeError, e:
528                         self._errorDisplay.push_exception(e)
529
530         def clear(self):
531                 self.set_number("")
532
533         def _on_dial_clicked(self, widget):
534                 self.dial(self.get_number())
535
536         def _on_clear_number(self, *args):
537                 self.clear()
538
539         def _on_digit_clicked(self, widget):
540                 self.set_number(self._phonenumber + widget.get_name()[-1])
541
542         def _on_backspace(self, widget):
543                 self.set_number(self._phonenumber[:-1])
544
545         def _on_clearall(self):
546                 self.clear()
547                 return False
548
549         def _on_back_pressed(self, widget):
550                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
551
552         def _on_back_released(self, widget):
553                 if self._clearall_id is not None:
554                         gobject.source_remove(self._clearall_id)
555                 self._clearall_id = None
556
557
558 class AccountInfo(object):
559
560         def __init__(self, widgetTree, backend, errorDisplay):
561                 self._errorDisplay = errorDisplay
562                 self._backend = backend
563
564                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
565                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
566                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
567                 self._onCallbackentryChangedId = 0
568
569         def enable(self):
570                 assert self._backend.is_authed()
571                 self._accountViewNumberDisplay.set_use_markup(True)
572                 self.set_account_number("")
573                 self._callbackList.clear()
574                 self.update()
575                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
576
577         def disable(self):
578                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
579                 self.clear()
580                 self._callbackList.clear()
581
582         def get_selected_callback_number(self):
583                 return make_ugly(self._callbackCombo.get_child().get_text())
584
585         def set_account_number(self, number):
586                 """
587                 Displays current account number
588                 """
589                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
590
591         def update(self):
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
599         def populate_callback_combo(self):
600                 self._callbackList.clear()
601                 try:
602                         callbackNumbers = self._backend.get_callback_numbers()
603                 except RuntimeError, e:
604                         self._errorDisplay.push_exception(e)
605                         return
606
607                 for number, description in callbackNumbers.iteritems():
608                         self._callbackList.append((make_pretty(number),))
609
610                 self._callbackCombo.set_model(self._callbackList)
611                 self._callbackCombo.set_text_column(0)
612                 try:
613                         callbackNumber = self._backend.get_callback_number()
614                 except RuntimeError, e:
615                         self._errorDisplay.push_exception(e)
616                         return
617                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
618
619         def _on_callbackentry_changed(self, *args):
620                 """
621                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
622                 """
623                 try:
624                         text = self.get_selected_callback_number()
625                         if not self._backend.is_valid_syntax(text):
626                                 self._errorDisplay.push_message("%s is not a valid callback number" % text)
627                         elif text == self._backend.get_callback_number():
628                                 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
629                         else:
630                                 self._backend.set_callback_number(text)
631                 except RuntimeError, e:
632                         self._errorDisplay.push_exception(e)
633
634
635 class RecentCallsView(object):
636
637         def __init__(self, widgetTree, backend, errorDisplay):
638                 self._errorDisplay = errorDisplay
639                 self._backend = backend
640
641                 self._recenttime = 0.0
642                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
643                 self._recentview = widgetTree.get_widget("recentview")
644                 self._recentviewselection = None
645                 self._onRecentviewRowActivatedId = 0
646
647                 # @todo Make seperate columns for each item in recent item payload
648                 textrenderer = gtk.CellRendererText()
649                 self._recentviewColumn = gtk.TreeViewColumn("Calls")
650                 self._recentviewColumn.pack_start(textrenderer, expand=True)
651                 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
652                 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
653
654                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
655
656         def enable(self):
657                 assert self._backend.is_authed()
658                 self._recentview.set_model(self._recentmodel)
659
660                 self._recentview.append_column(self._recentviewColumn)
661                 self._recentviewselection = self._recentview.get_selection()
662                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
663
664                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
665
666         def disable(self):
667                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
668                 self._recentview.remove_column(self._recentviewColumn)
669                 self._recentview.set_model(None)
670
671         def number_selected(self, action, number, message):
672                 """
673                 @note Actual dial function is patched in later
674                 """
675                 raise NotImplementedError
676
677         def update(self):
678                 if (time.time() - self._recenttime) < 300:
679                         return
680                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
681                 backgroundPopulate.setDaemon(True)
682                 backgroundPopulate.start()
683
684         def clear(self):
685                 self._recenttime = 0.0
686                 self._recentmodel.clear()
687
688         def _idly_populate_recentview(self):
689                 self._recenttime = time.time()
690                 self._recentmodel.clear()
691
692                 try:
693                         recentItems = self._backend.get_recent()
694                 except RuntimeError, e:
695                         self._errorDisplay.push_exception_with_lock(e)
696                         self._recenttime = 0.0
697                         recentItems = []
698
699                 for personsName, phoneNumber, date, action in recentItems:
700                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
701                         item = (phoneNumber, description)
702                         with gtk_toolbox.gtk_lock():
703                                 self._recentmodel.append(item)
704
705                 return False
706
707         def _on_recentview_row_activated(self, treeview, path, view_column):
708                 model, itr = self._recentviewselection.get_selected()
709                 if not itr:
710                         return
711
712                 number = self._recentmodel.get_value(itr, 0)
713                 number = make_ugly(number)
714                 contactPhoneNumbers = [("Phone", number)]
715                 description = self._recentmodel.get_value(itr, 1)
716
717                 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
718                 if action == PhoneTypeSelector.ACTION_CANCEL:
719                         return
720                 assert phoneNumber
721
722                 self.number_selected(action, phoneNumber, message)
723                 self._recentviewselection.unselect_all()
724
725
726 class MessagesView(object):
727
728         def __init__(self, widgetTree, backend, errorDisplay):
729                 self._errorDisplay = errorDisplay
730                 self._backend = backend
731
732                 self._messagetime = 0.0
733                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
734                 self._messageview = widgetTree.get_widget("messages_view")
735                 self._messageviewselection = None
736                 self._onMessageviewRowActivatedId = 0
737
738                 textrenderer = gtk.CellRendererText()
739                 # @todo Make seperate columns for each item in message payload
740                 self._messageviewColumn = gtk.TreeViewColumn("Messages")
741                 self._messageviewColumn.pack_start(textrenderer, expand=True)
742                 self._messageviewColumn.add_attribute(textrenderer, "markup", 1)
743                 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
744
745                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
746
747         def enable(self):
748                 assert self._backend.is_authed()
749                 self._messageview.set_model(self._messagemodel)
750
751                 self._messageview.append_column(self._messageviewColumn)
752                 self._messageviewselection = self._messageview.get_selection()
753                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
754
755                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
756
757         def disable(self):
758                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
759                 self._messageview.remove_column(self._messageviewColumn)
760                 self._messageview.set_model(None)
761
762         def number_selected(self, action, number, message):
763                 """
764                 @note Actual dial function is patched in later
765                 """
766                 raise NotImplementedError
767
768         def update(self):
769                 if (time.time() - self._messagetime) < 300:
770                         return
771                 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
772                 backgroundPopulate.setDaemon(True)
773                 backgroundPopulate.start()
774
775         def clear(self):
776                 self._messagetime = 0.0
777                 self._messagemodel.clear()
778
779         def _idly_populate_messageview(self):
780                 self._messagetime = time.time()
781                 self._messagemodel.clear()
782
783                 try:
784                         messageItems = self._backend.get_messages()
785                 except RuntimeError, e:
786                         self._errorDisplay.push_exception_with_lock(e)
787                         self._messagetime = 0.0
788                         messageItems = []
789
790                 for header, number, relativeDate, message in messageItems:
791                         number = make_ugly(number)
792                         row = (number, message)
793                         with gtk_toolbox.gtk_lock():
794                                 self._messagemodel.append(row)
795
796                 return False
797
798         def _on_messageview_row_activated(self, treeview, path, view_column):
799                 model, itr = self._messageviewselection.get_selected()
800                 if not itr:
801                         return
802
803                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
804                 description = self._messagemodel.get_value(itr, 1)
805
806                 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
807                 if action == PhoneTypeSelector.ACTION_CANCEL:
808                         return
809                 assert phoneNumber
810
811                 self.number_selected(action, phoneNumber, message)
812                 self._messageviewselection.unselect_all()
813
814
815 class ContactsView(object):
816
817         def __init__(self, widgetTree, backend, errorDisplay):
818                 self._errorDisplay = errorDisplay
819                 self._backend = backend
820
821                 self._addressBook = None
822                 self._addressBookFactories = [DummyAddressBook()]
823
824                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
825                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
826
827                 self._contactstime = 0.0
828                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
829                 self._contactsviewselection = None
830                 self._contactsview = widgetTree.get_widget("contactsview")
831
832                 self._contactColumn = gtk.TreeViewColumn("Contact")
833                 displayContactSource = False
834                 if displayContactSource:
835                         textrenderer = gtk.CellRendererText()
836                         self._contactColumn.pack_start(textrenderer, expand=False)
837                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
838                 textrenderer = gtk.CellRendererText()
839                 self._contactColumn.pack_start(textrenderer, expand=True)
840                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
841                 textrenderer = gtk.CellRendererText()
842                 self._contactColumn.pack_start(textrenderer, expand=True)
843                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
844                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
845                 self._contactColumn.set_sort_column_id(1)
846                 self._contactColumn.set_visible(True)
847
848                 self._onContactsviewRowActivatedId = 0
849                 self._onAddressbookComboChangedId = 0
850                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
851
852         def enable(self):
853                 assert self._backend.is_authed()
854
855                 self._contactsview.set_model(self._contactsmodel)
856                 self._contactsview.append_column(self._contactColumn)
857                 self._contactsviewselection = self._contactsview.get_selection()
858                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
859
860                 self._booksList.clear()
861                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
862                         if factoryName and bookName:
863                                 entryName = "%s: %s" % (factoryName, bookName)
864                         elif factoryName:
865                                 entryName = factoryName
866                         elif bookName:
867                                 entryName = bookName
868                         else:
869                                 entryName = "Bad name (%d)" % factoryId
870                         row = (str(factoryId), bookId, entryName)
871                         self._booksList.append(row)
872
873                 self._booksSelectionBox.set_model(self._booksList)
874                 cell = gtk.CellRendererText()
875                 self._booksSelectionBox.pack_start(cell, True)
876                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
877                 self._booksSelectionBox.set_active(0)
878
879                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
880                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
881
882         def disable(self):
883                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
884                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
885
886                 self._booksSelectionBox.clear()
887                 self._booksSelectionBox.set_model(None)
888                 self._contactsview.set_model(None)
889                 self._contactsview.remove_column(self._contactColumn)
890
891         def number_selected(self, action, number, message):
892                 """
893                 @note Actual dial function is patched in later
894                 """
895                 raise NotImplementedError
896
897         def get_addressbooks(self):
898                 """
899                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
900                 """
901                 for i, factory in enumerate(self._addressBookFactories):
902                         for bookFactory, bookId, bookName in factory.get_addressbooks():
903                                 yield (i, bookId), (factory.factory_name(), bookName)
904
905         def open_addressbook(self, bookFactoryId, bookId):
906                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
907                 self._contactstime = 0
908                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
909                 backgroundPopulate.setDaemon(True)
910                 backgroundPopulate.start()
911
912         def update(self):
913                 if (time.time() - self._contactstime) < 300:
914                         return
915                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
916                 backgroundPopulate.setDaemon(True)
917                 backgroundPopulate.start()
918
919         def clear(self):
920                 self._contactstime = 0.0
921                 self._contactsmodel.clear()
922
923         def clear_caches(self):
924                 for factory in self._addressBookFactories:
925                         factory.clear_caches()
926                 self._addressBook.clear_caches()
927
928         def append(self, book):
929                 self._addressBookFactories.append(book)
930
931         def extend(self, books):
932                 self._addressBookFactories.extend(books)
933
934         def _idly_populate_contactsview(self):
935                 #@todo Add a lock so only one code path can be in here at a time
936                 self.clear()
937
938                 # completely disable updating the treeview while we populate the data
939                 self._contactsview.freeze_child_notify()
940                 self._contactsview.set_model(None)
941
942                 addressBook = self._addressBook
943                 try:
944                         contacts = addressBook.get_contacts()
945                 except RuntimeError, e:
946                         contacts = []
947                         self._contactstime = 0.0
948                         self._errorDisplay.push_exception_with_lock(e)
949                 for contactId, contactName in contacts:
950                         contactType = (addressBook.contact_source_short_name(contactId), )
951                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
952
953                 # restart the treeview data rendering
954                 self._contactsview.set_model(self._contactsmodel)
955                 self._contactsview.thaw_child_notify()
956                 return False
957
958         def _on_addressbook_combo_changed(self, *args, **kwds):
959                 itr = self._booksSelectionBox.get_active_iter()
960                 if itr is None:
961                         return
962                 factoryId = int(self._booksList.get_value(itr, 0))
963                 bookId = self._booksList.get_value(itr, 1)
964                 self.open_addressbook(factoryId, bookId)
965
966         def _on_contactsview_row_activated(self, treeview, path, view_column):
967                 model, itr = self._contactsviewselection.get_selected()
968                 if not itr:
969                         return
970
971                 contactId = self._contactsmodel.get_value(itr, 3)
972                 contactName = self._contactsmodel.get_value(itr, 1)
973                 try:
974                         contactDetails = self._addressBook.get_contact_details(contactId)
975                 except RuntimeError, e:
976                         contactDetails = []
977                         self._contactstime = 0.0
978                         self._errorDisplay.push_exception(e)
979                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
980
981                 if len(contactPhoneNumbers) == 0:
982                         return
983
984                 action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
985                 if action == PhoneTypeSelector.ACTION_CANCEL:
986                         return
987                 assert phoneNumber
988
989                 self.number_selected(action, phoneNumber, message)
990                 self._contactsviewselection.unselect_all()