Version bump to 1.0, description fixes, and adding of names to the recent tab for...
[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         MAX_CHAR = 160
371
372         def __init__(self, widgetTree):
373                 self._widgetTree = widgetTree
374                 self._dialog = self._widgetTree.get_widget("smsDialog")
375
376                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
377                 self._smsButton.connect("clicked", self._on_send)
378
379                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
380                 self._cancelButton.connect("clicked", self._on_cancel)
381
382                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
383                 self._message = self._widgetTree.get_widget("smsMessage")
384                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
385                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
386
387         def run(self, number, message = "", parent = None):
388                 if message:
389                         self._message.set_markup(message)
390                         self._message.show()
391                 else:
392                         self._message.set_markup("")
393                         self._message.hide()
394                 self._smsEntry.get_buffer().set_text("")
395                 self._update_letter_count()
396
397                 if parent is not None:
398                         self._dialog.set_transient_for(parent)
399
400                 try:
401                         userResponse = self._dialog.run()
402                 finally:
403                         self._dialog.hide()
404
405                 if userResponse == gtk.RESPONSE_OK:
406                         entryBuffer = self._smsEntry.get_buffer()
407                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
408                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
409                 else:
410                         enteredMessage = ""
411
412                 return enteredMessage.strip()
413
414         def _update_letter_count(self, *args):
415                 entryLength = self._smsEntry.get_buffer().get_char_count()
416                 charsLeft = self.MAX_CHAR - entryLength
417                 self._letterCountLabel.set_text(str(charsLeft))
418                 if charsLeft < 0:
419                         self._smsButton.set_sensitive(False)
420                 else:
421                         self._smsButton.set_sensitive(True)
422
423         def _on_entry_changed(self, *args):
424                 self._update_letter_count()
425
426         def _on_send(self, *args):
427                 self._dialog.response(gtk.RESPONSE_OK)
428
429         def _on_cancel(self, *args):
430                 self._dialog.response(gtk.RESPONSE_CANCEL)
431
432
433 class Dialpad(object):
434
435         def __init__(self, widgetTree, errorDisplay):
436                 self._errorDisplay = errorDisplay
437                 self._smsDialog = SmsEntryDialog(widgetTree)
438
439                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
440                 self._dialButton = widgetTree.get_widget("dial")
441                 self._phonenumber = ""
442                 self._prettynumber = ""
443                 self._clearall_id = None
444
445                 callbackMapping = {
446                         "on_dial_clicked": self._on_dial_clicked,
447                         "on_sms_clicked": self._on_sms_clicked,
448                         "on_digit_clicked": self._on_digit_clicked,
449                         "on_clear_number": self._on_clear_number,
450                         "on_back_clicked": self._on_backspace,
451                         "on_back_pressed": self._on_back_pressed,
452                         "on_back_released": self._on_back_released,
453                 }
454                 widgetTree.signal_autoconnect(callbackMapping)
455
456                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
457
458         def enable(self):
459                 self._dialButton.grab_focus()
460
461         def disable(self):
462                 pass
463
464         def number_selected(self, action, number, message):
465                 """
466                 @note Actual dial function is patched in later
467                 """
468                 raise NotImplementedError
469
470         def get_number(self):
471                 return self._phonenumber
472
473         def set_number(self, number):
474                 """
475                 Set the callback phonenumber
476                 """
477                 try:
478                         self._phonenumber = make_ugly(number)
479                         self._prettynumber = make_pretty(self._phonenumber)
480                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
481                 except TypeError, e:
482                         self._errorDisplay.push_exception(e)
483
484         def clear(self):
485                 self.set_number("")
486
487         @staticmethod
488         def name():
489                 return "Dialpad"
490
491         def load_settings(self, config, section):
492                 pass
493
494         def save_settings(self, config, section):
495                 """
496                 @note Thread Agnostic
497                 """
498                 pass
499
500         def _on_sms_clicked(self, widget):
501                 action = PhoneTypeSelector.ACTION_SEND_SMS
502                 phoneNumber = self.get_number()
503
504                 message = self._smsDialog.run(phoneNumber, "", self._window)
505                 if not message:
506                         phoneNumber = ""
507                         action = PhoneTypeSelector.ACTION_CANCEL
508
509                 if action == PhoneTypeSelector.ACTION_CANCEL:
510                         return
511                 self.number_selected(action, phoneNumber, message)
512
513         def _on_dial_clicked(self, widget):
514                 action = PhoneTypeSelector.ACTION_DIAL
515                 phoneNumber = self.get_number()
516                 message = ""
517                 self.number_selected(action, phoneNumber, message)
518
519         def _on_clear_number(self, *args):
520                 self.clear()
521
522         def _on_digit_clicked(self, widget):
523                 self.set_number(self._phonenumber + widget.get_name()[-1])
524
525         def _on_backspace(self, widget):
526                 self.set_number(self._phonenumber[:-1])
527
528         def _on_clearall(self):
529                 self.clear()
530                 return False
531
532         def _on_back_pressed(self, widget):
533                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
534
535         def _on_back_released(self, widget):
536                 if self._clearall_id is not None:
537                         gobject.source_remove(self._clearall_id)
538                 self._clearall_id = None
539
540
541 class AccountInfo(object):
542
543         def __init__(self, widgetTree, backend, errorDisplay):
544                 self._errorDisplay = errorDisplay
545                 self._backend = backend
546                 self._isPopulated = False
547
548                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
549                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
550                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
551                 self._onCallbackentryChangedId = 0
552
553                 self._defaultCallback = ""
554
555         def enable(self):
556                 assert self._backend.is_authed()
557                 self._accountViewNumberDisplay.set_use_markup(True)
558                 self.set_account_number("")
559                 self._callbackList.clear()
560                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
561                 self.update(force=True)
562
563         def disable(self):
564                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
565
566                 self.clear()
567
568                 self._callbackList.clear()
569
570         def get_selected_callback_number(self):
571                 return make_ugly(self._callbackCombo.get_child().get_text())
572
573         def set_account_number(self, number):
574                 """
575                 Displays current account number
576                 """
577                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
578
579         def update(self, force = False):
580                 if not force and self._isPopulated:
581                         return
582                 self._populate_callback_combo()
583                 self.set_account_number(self._backend.get_account_number())
584
585         def clear(self):
586                 self._callbackCombo.get_child().set_text("")
587                 self.set_account_number("")
588                 self._isPopulated = False
589
590         @staticmethod
591         def name():
592                 return "Account Info"
593
594         def load_settings(self, config, section):
595                 self._defaultCallback = config.get(section, "callback")
596
597         def save_settings(self, config, section):
598                 """
599                 @note Thread Agnostic
600                 """
601                 callback = self.get_selected_callback_number()
602                 config.set(section, "callback", callback)
603
604         def _populate_callback_combo(self):
605                 self._isPopulated = True
606                 self._callbackList.clear()
607                 try:
608                         callbackNumbers = self._backend.get_callback_numbers()
609                 except RuntimeError, e:
610                         self._errorDisplay.push_exception(e)
611                         self._isPopulated = False
612                         return
613
614                 for number, description in callbackNumbers.iteritems():
615                         self._callbackList.append((make_pretty(number),))
616
617                 self._callbackCombo.set_model(self._callbackList)
618                 self._callbackCombo.set_text_column(0)
619                 #callbackNumber = self._backend.get_callback_number()
620                 callbackNumber = self._defaultCallback
621                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
622
623         def _set_callback_number(self, number):
624                 try:
625                         if not self._backend.is_valid_syntax(number):
626                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
627                         elif number == 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(number)
631                                 warnings.warn("Callback number set to %s" % self._backend.get_callback_number(), UserWarning, 2)
632                 except RuntimeError, e:
633                         self._errorDisplay.push_exception(e)
634
635         def _on_callbackentry_changed(self, *args):
636                 text = self.get_selected_callback_number()
637                 self._set_callback_number(text)
638
639
640 class RecentCallsView(object):
641
642         NUMBER_IDX = 0
643         DATE_IDX = 1
644         ACTION_IDX = 2
645         FROM_IDX = 3
646
647         def __init__(self, widgetTree, backend, errorDisplay):
648                 self._errorDisplay = errorDisplay
649                 self._backend = backend
650
651                 self._isPopulated = False
652                 self._recentmodel = gtk.ListStore(
653                         gobject.TYPE_STRING, # number
654                         gobject.TYPE_STRING, # date
655                         gobject.TYPE_STRING, # action
656                         gobject.TYPE_STRING, # from
657                 )
658                 self._recentview = widgetTree.get_widget("recentview")
659                 self._recentviewselection = None
660                 self._onRecentviewRowActivatedId = 0
661
662                 textrenderer = gtk.CellRendererText()
663                 textrenderer.set_property("yalign", 0)
664                 self._dateColumn = gtk.TreeViewColumn("Date")
665                 self._dateColumn.pack_start(textrenderer, expand=True)
666                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
667
668                 textrenderer = gtk.CellRendererText()
669                 textrenderer.set_property("yalign", 0)
670                 self._actionColumn = gtk.TreeViewColumn("Action")
671                 self._actionColumn.pack_start(textrenderer, expand=True)
672                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
673
674                 textrenderer = gtk.CellRendererText()
675                 textrenderer.set_property("yalign", 0)
676                 self._fromColumn = gtk.TreeViewColumn("From")
677                 self._fromColumn.pack_start(textrenderer, expand=True)
678                 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
679                 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
680
681                 self._window = gtk_toolbox.find_parent_window(self._recentview)
682                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
683
684         def enable(self):
685                 assert self._backend.is_authed()
686                 self._recentview.set_model(self._recentmodel)
687
688                 self._recentview.append_column(self._dateColumn)
689                 self._recentview.append_column(self._actionColumn)
690                 self._recentview.append_column(self._fromColumn)
691                 self._recentviewselection = self._recentview.get_selection()
692                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
693
694                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
695
696         def disable(self):
697                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
698
699                 self.clear()
700
701                 self._recentview.remove_column(self._dateColumn)
702                 self._recentview.remove_column(self._actionColumn)
703                 self._recentview.remove_column(self._fromColumn)
704                 self._recentview.set_model(None)
705
706         def number_selected(self, action, number, message):
707                 """
708                 @note Actual dial function is patched in later
709                 """
710                 raise NotImplementedError
711
712         def update(self, force = False):
713                 if not force and self._isPopulated:
714                         return
715                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
716                 backgroundPopulate.setDaemon(True)
717                 backgroundPopulate.start()
718
719         def clear(self):
720                 self._isPopulated = False
721                 self._recentmodel.clear()
722
723         @staticmethod
724         def name():
725                 return "Recent Calls"
726
727         def load_settings(self, config, section):
728                 pass
729
730         def save_settings(self, config, section):
731                 """
732                 @note Thread Agnostic
733                 """
734                 pass
735
736         def _idly_populate_recentview(self):
737                 self._isPopulated = True
738                 self._recentmodel.clear()
739
740                 try:
741                         recentItems = self._backend.get_recent()
742                 except RuntimeError, e:
743                         self._errorDisplay.push_exception_with_lock(e)
744                         self._isPopulated = False
745                         recentItems = []
746
747                 for personName, phoneNumber, date, action in recentItems:
748                         if not personName:
749                                 personName = "Unknown"
750                         prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
751                         prettyNumber = make_pretty(prettyNumber)
752                         description = "%s - %s" % (personName, prettyNumber)
753                         item = (phoneNumber, date, action.capitalize(), description)
754                         with gtk_toolbox.gtk_lock():
755                                 self._recentmodel.append(item)
756
757                 return False
758
759         def _on_recentview_row_activated(self, treeview, path, view_column):
760                 model, itr = self._recentviewselection.get_selected()
761                 if not itr:
762                         return
763
764                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
765                 number = make_ugly(number)
766                 contactPhoneNumbers = [("Phone", number)]
767                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
768
769                 action, phoneNumber, message = self._phoneTypeSelector.run(
770                         contactPhoneNumbers,
771                         message = description,
772                         parent = self._window,
773                 )
774                 if action == PhoneTypeSelector.ACTION_CANCEL:
775                         return
776                 assert phoneNumber
777
778                 self.number_selected(action, phoneNumber, message)
779                 self._recentviewselection.unselect_all()
780
781
782 class MessagesView(object):
783
784         NUMBER_IDX = 0
785         DATE_IDX = 1
786         HEADER_IDX = 2
787         MESSAGE_IDX = 3
788
789         def __init__(self, widgetTree, backend, errorDisplay):
790                 self._errorDisplay = errorDisplay
791                 self._backend = backend
792
793                 self._isPopulated = False
794                 self._messagemodel = gtk.ListStore(
795                         gobject.TYPE_STRING, # number
796                         gobject.TYPE_STRING, # date
797                         gobject.TYPE_STRING, # header
798                         gobject.TYPE_STRING, # message
799                 )
800                 self._messageview = widgetTree.get_widget("messages_view")
801                 self._messageviewselection = None
802                 self._onMessageviewRowActivatedId = 0
803
804                 textrenderer = gtk.CellRendererText()
805                 textrenderer.set_property("yalign", 0)
806                 self._dateColumn = gtk.TreeViewColumn("Date")
807                 self._dateColumn.pack_start(textrenderer, expand=True)
808                 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
809
810                 textrenderer = gtk.CellRendererText()
811                 textrenderer.set_property("yalign", 0)
812                 self._headerColumn = gtk.TreeViewColumn("From")
813                 self._headerColumn.pack_start(textrenderer, expand=True)
814                 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
815
816                 textrenderer = gtk.CellRendererText()
817                 textrenderer.set_property("yalign", 0)
818                 self._messageColumn = gtk.TreeViewColumn("Messages")
819                 self._messageColumn.pack_start(textrenderer, expand=True)
820                 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
821                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
822
823                 self._window = gtk_toolbox.find_parent_window(self._messageview)
824                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
825
826         def enable(self):
827                 assert self._backend.is_authed()
828                 self._messageview.set_model(self._messagemodel)
829
830                 self._messageview.append_column(self._dateColumn)
831                 self._messageview.append_column(self._headerColumn)
832                 self._messageview.append_column(self._messageColumn)
833                 self._messageviewselection = self._messageview.get_selection()
834                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
835
836                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
837
838         def disable(self):
839                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
840
841                 self.clear()
842
843                 self._messageview.remove_column(self._dateColumn)
844                 self._messageview.remove_column(self._headerColumn)
845                 self._messageview.remove_column(self._messageColumn)
846                 self._messageview.set_model(None)
847
848         def number_selected(self, action, number, message):
849                 """
850                 @note Actual dial function is patched in later
851                 """
852                 raise NotImplementedError
853
854         def update(self, force = False):
855                 if not force and self._isPopulated:
856                         return
857                 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
858                 backgroundPopulate.setDaemon(True)
859                 backgroundPopulate.start()
860
861         def clear(self):
862                 self._isPopulated = False
863                 self._messagemodel.clear()
864
865         @staticmethod
866         def name():
867                 return "Messages"
868
869         def load_settings(self, config, section):
870                 pass
871
872         def save_settings(self, config, section):
873                 """
874                 @note Thread Agnostic
875                 """
876                 pass
877
878         def _idly_populate_messageview(self):
879                 self._isPopulated = True
880                 self._messagemodel.clear()
881
882                 try:
883                         messageItems = self._backend.get_messages()
884                 except RuntimeError, e:
885                         self._errorDisplay.push_exception_with_lock(e)
886                         self._isPopulated = False
887                         messageItems = []
888
889                 for header, number, relativeDate, message in messageItems:
890                         number = make_ugly(number)
891                         row = (number, relativeDate, header, message)
892                         with gtk_toolbox.gtk_lock():
893                                 self._messagemodel.append(row)
894
895                 return False
896
897         def _on_messageview_row_activated(self, treeview, path, view_column):
898                 model, itr = self._messageviewselection.get_selected()
899                 if not itr:
900                         return
901
902                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
903                 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
904
905                 action, phoneNumber, message = self._phoneTypeSelector.run(
906                         contactPhoneNumbers,
907                         message = description,
908                         parent = self._window,
909                 )
910                 if action == PhoneTypeSelector.ACTION_CANCEL:
911                         return
912                 assert phoneNumber
913
914                 self.number_selected(action, phoneNumber, message)
915                 self._messageviewselection.unselect_all()
916
917
918 class ContactsView(object):
919
920         def __init__(self, widgetTree, backend, errorDisplay):
921                 self._errorDisplay = errorDisplay
922                 self._backend = backend
923
924                 self._addressBook = None
925                 self._addressBookFactories = [null_backend.NullAddressBook()]
926
927                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
928                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
929
930                 self._isPopulated = False
931                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
932                 self._contactsviewselection = None
933                 self._contactsview = widgetTree.get_widget("contactsview")
934
935                 self._contactColumn = gtk.TreeViewColumn("Contact")
936                 displayContactSource = False
937                 if displayContactSource:
938                         textrenderer = gtk.CellRendererText()
939                         self._contactColumn.pack_start(textrenderer, expand=False)
940                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
941                 textrenderer = gtk.CellRendererText()
942                 self._contactColumn.pack_start(textrenderer, expand=True)
943                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
944                 textrenderer = gtk.CellRendererText()
945                 self._contactColumn.pack_start(textrenderer, expand=True)
946                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
947                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
948                 self._contactColumn.set_sort_column_id(1)
949                 self._contactColumn.set_visible(True)
950
951                 self._onContactsviewRowActivatedId = 0
952                 self._onAddressbookComboChangedId = 0
953                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
954                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
955
956         def enable(self):
957                 assert self._backend.is_authed()
958
959                 self._contactsview.set_model(self._contactsmodel)
960                 self._contactsview.append_column(self._contactColumn)
961                 self._contactsviewselection = self._contactsview.get_selection()
962                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
963
964                 self._booksList.clear()
965                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
966                         if factoryName and bookName:
967                                 entryName = "%s: %s" % (factoryName, bookName)
968                         elif factoryName:
969                                 entryName = factoryName
970                         elif bookName:
971                                 entryName = bookName
972                         else:
973                                 entryName = "Bad name (%d)" % factoryId
974                         row = (str(factoryId), bookId, entryName)
975                         self._booksList.append(row)
976
977                 self._booksSelectionBox.set_model(self._booksList)
978                 cell = gtk.CellRendererText()
979                 self._booksSelectionBox.pack_start(cell, True)
980                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
981                 self._booksSelectionBox.set_active(0)
982
983                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
984                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
985
986         def disable(self):
987                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
988                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
989
990                 self.clear()
991
992                 self._booksSelectionBox.clear()
993                 self._booksSelectionBox.set_model(None)
994                 self._contactsview.set_model(None)
995                 self._contactsview.remove_column(self._contactColumn)
996
997         def number_selected(self, action, number, message):
998                 """
999                 @note Actual dial function is patched in later
1000                 """
1001                 raise NotImplementedError
1002
1003         def get_addressbooks(self):
1004                 """
1005                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1006                 """
1007                 for i, factory in enumerate(self._addressBookFactories):
1008                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1009                                 yield (i, bookId), (factory.factory_name(), bookName)
1010
1011         def open_addressbook(self, bookFactoryId, bookId):
1012                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1013                 self.update(force=True)
1014
1015         def update(self, force = False):
1016                 if not force and self._isPopulated:
1017                         return
1018                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1019                 backgroundPopulate.setDaemon(True)
1020                 backgroundPopulate.start()
1021
1022         def clear(self):
1023                 self._isPopulated = False
1024                 self._contactsmodel.clear()
1025
1026         def clear_caches(self):
1027                 for factory in self._addressBookFactories:
1028                         factory.clear_caches()
1029                 self._addressBook.clear_caches()
1030
1031         def append(self, book):
1032                 self._addressBookFactories.append(book)
1033
1034         def extend(self, books):
1035                 self._addressBookFactories.extend(books)
1036
1037         @staticmethod
1038         def name():
1039                 return "Contacts"
1040
1041         def load_settings(self, config, section):
1042                 pass
1043
1044         def save_settings(self, config, section):
1045                 """
1046                 @note Thread Agnostic
1047                 """
1048                 pass
1049
1050         def _idly_populate_contactsview(self):
1051                 self._isPopulated = True
1052                 self.clear()
1053
1054                 # completely disable updating the treeview while we populate the data
1055                 self._contactsview.freeze_child_notify()
1056                 self._contactsview.set_model(None)
1057
1058                 addressBook = self._addressBook
1059                 try:
1060                         contacts = addressBook.get_contacts()
1061                 except RuntimeError, e:
1062                         contacts = []
1063                         self._isPopulated = False
1064                         self._errorDisplay.push_exception_with_lock(e)
1065                 for contactId, contactName in contacts:
1066                         contactType = (addressBook.contact_source_short_name(contactId), )
1067                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1068
1069                 # restart the treeview data rendering
1070                 self._contactsview.set_model(self._contactsmodel)
1071                 self._contactsview.thaw_child_notify()
1072                 return False
1073
1074         def _on_addressbook_combo_changed(self, *args, **kwds):
1075                 itr = self._booksSelectionBox.get_active_iter()
1076                 if itr is None:
1077                         return
1078                 factoryId = int(self._booksList.get_value(itr, 0))
1079                 bookId = self._booksList.get_value(itr, 1)
1080                 self.open_addressbook(factoryId, bookId)
1081
1082         def _on_contactsview_row_activated(self, treeview, path, view_column):
1083                 model, itr = self._contactsviewselection.get_selected()
1084                 if not itr:
1085                         return
1086
1087                 contactId = self._contactsmodel.get_value(itr, 3)
1088                 contactName = self._contactsmodel.get_value(itr, 1)
1089                 try:
1090                         contactDetails = self._addressBook.get_contact_details(contactId)
1091                 except RuntimeError, e:
1092                         contactDetails = []
1093                         self._errorDisplay.push_exception(e)
1094                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1095
1096                 if len(contactPhoneNumbers) == 0:
1097                         return
1098
1099                 action, phoneNumber, message = self._phoneTypeSelector.run(
1100                         contactPhoneNumbers,
1101                         message = contactName,
1102                         parent = self._window,
1103                 )
1104                 if action == PhoneTypeSelector.ACTION_CANCEL:
1105                         return
1106                 assert phoneNumber
1107
1108                 self.number_selected(action, phoneNumber, message)
1109                 self._contactsviewselection.unselect_all()