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