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