Adding an sms button to the dialpad
[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                         description = "%s (%s)" % (phoneNumber, personName)
751                         item = (phoneNumber, date, action.capitalize(), description)
752                         with gtk_toolbox.gtk_lock():
753                                 self._recentmodel.append(item)
754
755                 return False
756
757         def _on_recentview_row_activated(self, treeview, path, view_column):
758                 model, itr = self._recentviewselection.get_selected()
759                 if not itr:
760                         return
761
762                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
763                 number = make_ugly(number)
764                 contactPhoneNumbers = [("Phone", number)]
765                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
766
767                 action, phoneNumber, message = self._phoneTypeSelector.run(
768                         contactPhoneNumbers,
769                         message = description,
770                         parent = self._window,
771                 )
772                 if action == PhoneTypeSelector.ACTION_CANCEL:
773                         return
774                 assert phoneNumber
775
776                 self.number_selected(action, phoneNumber, message)
777                 self._recentviewselection.unselect_all()
778
779
780 class MessagesView(object):
781
782         NUMBER_IDX = 0
783         DATE_IDX = 1
784         HEADER_IDX = 2
785         MESSAGE_IDX = 3
786
787         def __init__(self, widgetTree, backend, errorDisplay):
788                 self._errorDisplay = errorDisplay
789                 self._backend = backend
790
791                 self._isPopulated = False
792                 self._messagemodel = gtk.ListStore(
793                         gobject.TYPE_STRING, # number
794                         gobject.TYPE_STRING, # date
795                         gobject.TYPE_STRING, # header
796                         gobject.TYPE_STRING, # message
797                 )
798                 self._messageview = widgetTree.get_widget("messages_view")
799                 self._messageviewselection = None
800                 self._onMessageviewRowActivatedId = 0
801
802                 textrenderer = gtk.CellRendererText()
803                 textrenderer.set_property("yalign", 0)
804                 self._dateColumn = gtk.TreeViewColumn("Date")
805                 self._dateColumn.pack_start(textrenderer, expand=True)
806                 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
807
808                 textrenderer = gtk.CellRendererText()
809                 textrenderer.set_property("yalign", 0)
810                 self._headerColumn = gtk.TreeViewColumn("From")
811                 self._headerColumn.pack_start(textrenderer, expand=True)
812                 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
813
814                 textrenderer = gtk.CellRendererText()
815                 textrenderer.set_property("yalign", 0)
816                 self._messageColumn = gtk.TreeViewColumn("Messages")
817                 self._messageColumn.pack_start(textrenderer, expand=True)
818                 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
819                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
820
821                 self._window = gtk_toolbox.find_parent_window(self._messageview)
822                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
823
824         def enable(self):
825                 assert self._backend.is_authed()
826                 self._messageview.set_model(self._messagemodel)
827
828                 self._messageview.append_column(self._dateColumn)
829                 self._messageview.append_column(self._headerColumn)
830                 self._messageview.append_column(self._messageColumn)
831                 self._messageviewselection = self._messageview.get_selection()
832                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
833
834                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
835
836         def disable(self):
837                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
838
839                 self.clear()
840
841                 self._messageview.remove_column(self._dateColumn)
842                 self._messageview.remove_column(self._headerColumn)
843                 self._messageview.remove_column(self._messageColumn)
844                 self._messageview.set_model(None)
845
846         def number_selected(self, action, number, message):
847                 """
848                 @note Actual dial function is patched in later
849                 """
850                 raise NotImplementedError
851
852         def update(self, force = False):
853                 if not force and self._isPopulated:
854                         return
855                 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
856                 backgroundPopulate.setDaemon(True)
857                 backgroundPopulate.start()
858
859         def clear(self):
860                 self._isPopulated = False
861                 self._messagemodel.clear()
862
863         @staticmethod
864         def name():
865                 return "Messages"
866
867         def load_settings(self, config, section):
868                 pass
869
870         def save_settings(self, config, section):
871                 """
872                 @note Thread Agnostic
873                 """
874                 pass
875
876         def _idly_populate_messageview(self):
877                 self._isPopulated = True
878                 self._messagemodel.clear()
879
880                 try:
881                         messageItems = self._backend.get_messages()
882                 except RuntimeError, e:
883                         self._errorDisplay.push_exception_with_lock(e)
884                         self._isPopulated = False
885                         messageItems = []
886
887                 for header, number, relativeDate, message in messageItems:
888                         number = make_ugly(number)
889                         row = (number, relativeDate, header, message)
890                         with gtk_toolbox.gtk_lock():
891                                 self._messagemodel.append(row)
892
893                 return False
894
895         def _on_messageview_row_activated(self, treeview, path, view_column):
896                 model, itr = self._messageviewselection.get_selected()
897                 if not itr:
898                         return
899
900                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
901                 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
902
903                 action, phoneNumber, message = self._phoneTypeSelector.run(
904                         contactPhoneNumbers,
905                         message = description,
906                         parent = self._window,
907                 )
908                 if action == PhoneTypeSelector.ACTION_CANCEL:
909                         return
910                 assert phoneNumber
911
912                 self.number_selected(action, phoneNumber, message)
913                 self._messageviewselection.unselect_all()
914
915
916 class ContactsView(object):
917
918         def __init__(self, widgetTree, backend, errorDisplay):
919                 self._errorDisplay = errorDisplay
920                 self._backend = backend
921
922                 self._addressBook = None
923                 self._addressBookFactories = [null_backend.NullAddressBook()]
924
925                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
926                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
927
928                 self._isPopulated = False
929                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
930                 self._contactsviewselection = None
931                 self._contactsview = widgetTree.get_widget("contactsview")
932
933                 self._contactColumn = gtk.TreeViewColumn("Contact")
934                 displayContactSource = False
935                 if displayContactSource:
936                         textrenderer = gtk.CellRendererText()
937                         self._contactColumn.pack_start(textrenderer, expand=False)
938                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
939                 textrenderer = gtk.CellRendererText()
940                 self._contactColumn.pack_start(textrenderer, expand=True)
941                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
942                 textrenderer = gtk.CellRendererText()
943                 self._contactColumn.pack_start(textrenderer, expand=True)
944                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
945                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
946                 self._contactColumn.set_sort_column_id(1)
947                 self._contactColumn.set_visible(True)
948
949                 self._onContactsviewRowActivatedId = 0
950                 self._onAddressbookComboChangedId = 0
951                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
952                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
953
954         def enable(self):
955                 assert self._backend.is_authed()
956
957                 self._contactsview.set_model(self._contactsmodel)
958                 self._contactsview.append_column(self._contactColumn)
959                 self._contactsviewselection = self._contactsview.get_selection()
960                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
961
962                 self._booksList.clear()
963                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
964                         if factoryName and bookName:
965                                 entryName = "%s: %s" % (factoryName, bookName)
966                         elif factoryName:
967                                 entryName = factoryName
968                         elif bookName:
969                                 entryName = bookName
970                         else:
971                                 entryName = "Bad name (%d)" % factoryId
972                         row = (str(factoryId), bookId, entryName)
973                         self._booksList.append(row)
974
975                 self._booksSelectionBox.set_model(self._booksList)
976                 cell = gtk.CellRendererText()
977                 self._booksSelectionBox.pack_start(cell, True)
978                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
979                 self._booksSelectionBox.set_active(0)
980
981                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
982                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
983
984         def disable(self):
985                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
986                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
987
988                 self.clear()
989
990                 self._booksSelectionBox.clear()
991                 self._booksSelectionBox.set_model(None)
992                 self._contactsview.set_model(None)
993                 self._contactsview.remove_column(self._contactColumn)
994
995         def number_selected(self, action, number, message):
996                 """
997                 @note Actual dial function is patched in later
998                 """
999                 raise NotImplementedError
1000
1001         def get_addressbooks(self):
1002                 """
1003                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1004                 """
1005                 for i, factory in enumerate(self._addressBookFactories):
1006                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1007                                 yield (i, bookId), (factory.factory_name(), bookName)
1008
1009         def open_addressbook(self, bookFactoryId, bookId):
1010                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1011                 self.update(force=True)
1012
1013         def update(self, force = False):
1014                 if not force and self._isPopulated:
1015                         return
1016                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1017                 backgroundPopulate.setDaemon(True)
1018                 backgroundPopulate.start()
1019
1020         def clear(self):
1021                 self._isPopulated = False
1022                 self._contactsmodel.clear()
1023
1024         def clear_caches(self):
1025                 for factory in self._addressBookFactories:
1026                         factory.clear_caches()
1027                 self._addressBook.clear_caches()
1028
1029         def append(self, book):
1030                 self._addressBookFactories.append(book)
1031
1032         def extend(self, books):
1033                 self._addressBookFactories.extend(books)
1034
1035         @staticmethod
1036         def name():
1037                 return "Contacts"
1038
1039         def load_settings(self, config, section):
1040                 pass
1041
1042         def save_settings(self, config, section):
1043                 """
1044                 @note Thread Agnostic
1045                 """
1046                 pass
1047
1048         def _idly_populate_contactsview(self):
1049                 self._isPopulated = True
1050                 self.clear()
1051
1052                 # completely disable updating the treeview while we populate the data
1053                 self._contactsview.freeze_child_notify()
1054                 self._contactsview.set_model(None)
1055
1056                 addressBook = self._addressBook
1057                 try:
1058                         contacts = addressBook.get_contacts()
1059                 except RuntimeError, e:
1060                         contacts = []
1061                         self._isPopulated = False
1062                         self._errorDisplay.push_exception_with_lock(e)
1063                 for contactId, contactName in contacts:
1064                         contactType = (addressBook.contact_source_short_name(contactId), )
1065                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1066
1067                 # restart the treeview data rendering
1068                 self._contactsview.set_model(self._contactsmodel)
1069                 self._contactsview.thaw_child_notify()
1070                 return False
1071
1072         def _on_addressbook_combo_changed(self, *args, **kwds):
1073                 itr = self._booksSelectionBox.get_active_iter()
1074                 if itr is None:
1075                         return
1076                 factoryId = int(self._booksList.get_value(itr, 0))
1077                 bookId = self._booksList.get_value(itr, 1)
1078                 self.open_addressbook(factoryId, bookId)
1079
1080         def _on_contactsview_row_activated(self, treeview, path, view_column):
1081                 model, itr = self._contactsviewselection.get_selected()
1082                 if not itr:
1083                         return
1084
1085                 contactId = self._contactsmodel.get_value(itr, 3)
1086                 contactName = self._contactsmodel.get_value(itr, 1)
1087                 try:
1088                         contactDetails = self._addressBook.get_contact_details(contactId)
1089                 except RuntimeError, e:
1090                         contactDetails = []
1091                         self._errorDisplay.push_exception(e)
1092                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1093
1094                 if len(contactPhoneNumbers) == 0:
1095                         return
1096
1097                 action, phoneNumber, message = self._phoneTypeSelector.run(
1098                         contactPhoneNumbers,
1099                         message = contactName,
1100                         parent = self._window,
1101                 )
1102                 if action == PhoneTypeSelector.ACTION_CANCEL:
1103                         return
1104                 assert phoneNumber
1105
1106                 self.number_selected(action, phoneNumber, message)
1107                 self._contactsviewselection.unselect_all()