Random tweak, see changelist for details
[gc-dialer] / src / gv_views.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's GoogleVoice 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 @todo Alternate UI for dialogs (stackables)
22 """
23
24 from __future__ import with_statement
25
26 import ConfigParser
27 import logging
28 import itertools
29
30 import gobject
31 import pango
32 import gtk
33
34 import gtk_toolbox
35 import hildonize
36 import gv_backend
37 import null_backend
38
39
40 _moduleLogger = logging.getLogger("gv_views")
41
42
43 def make_ugly(prettynumber):
44         """
45         function to take a phone number and strip out all non-numeric
46         characters
47
48         >>> make_ugly("+012-(345)-678-90")
49         '01234567890'
50         """
51         import re
52         uglynumber = re.sub('\D', '', prettynumber)
53         return uglynumber
54
55
56 def make_pretty(phonenumber):
57         """
58         Function to take a phone number and return the pretty version
59         pretty numbers:
60                 if phonenumber begins with 0:
61                         ...-(...)-...-....
62                 if phonenumber begins with 1: ( for gizmo callback numbers )
63                         1 (...)-...-....
64                 if phonenumber is 13 digits:
65                         (...)-...-....
66                 if phonenumber is 10 digits:
67                         ...-....
68         >>> make_pretty("12")
69         '12'
70         >>> make_pretty("1234567")
71         '123-4567'
72         >>> make_pretty("2345678901")
73         '(234)-567-8901'
74         >>> make_pretty("12345678901")
75         '1 (234)-567-8901'
76         >>> make_pretty("01234567890")
77         '+012-(345)-678-90'
78         """
79         if phonenumber is None or phonenumber is "":
80                 return ""
81
82         phonenumber = make_ugly(phonenumber)
83
84         if len(phonenumber) < 3:
85                 return phonenumber
86
87         if phonenumber[0] == "0":
88                 prettynumber = ""
89                 prettynumber += "+%s" % phonenumber[0:3]
90                 if 3 < len(phonenumber):
91                         prettynumber += "-(%s)" % phonenumber[3:6]
92                         if 6 < len(phonenumber):
93                                 prettynumber += "-%s" % phonenumber[6:9]
94                                 if 9 < len(phonenumber):
95                                         prettynumber += "-%s" % phonenumber[9:]
96                 return prettynumber
97         elif len(phonenumber) <= 7:
98                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
99         elif len(phonenumber) > 8 and phonenumber[0] == "1":
100                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
101         elif len(phonenumber) > 7:
102                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
103         return prettynumber
104
105
106 def abbrev_relative_date(date):
107         """
108         >>> abbrev_relative_date("42 hours ago")
109         '42 h'
110         >>> abbrev_relative_date("2 days ago")
111         '2 d'
112         >>> abbrev_relative_date("4 weeks ago")
113         '4 w'
114         """
115         parts = date.split(" ")
116         return "%s %s" % (parts[0], parts[1][0])
117
118
119 class MergedAddressBook(object):
120         """
121         Merger of all addressbooks
122         """
123
124         def __init__(self, addressbookFactories, sorter = None):
125                 self.__addressbookFactories = addressbookFactories
126                 self.__addressbooks = None
127                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
128
129         def clear_caches(self):
130                 self.__addressbooks = None
131                 for factory in self.__addressbookFactories:
132                         factory.clear_caches()
133
134         def get_addressbooks(self):
135                 """
136                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
137                 """
138                 yield self, "", ""
139
140         def open_addressbook(self, bookId):
141                 return self
142
143         def contact_source_short_name(self, contactId):
144                 if self.__addressbooks is None:
145                         return ""
146                 bookIndex, originalId = contactId.split("-", 1)
147                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
148
149         @staticmethod
150         def factory_name():
151                 return "All Contacts"
152
153         def get_contacts(self):
154                 """
155                 @returns Iterable of (contact id, contact name)
156                 """
157                 if self.__addressbooks is None:
158                         self.__addressbooks = list(
159                                 factory.open_addressbook(id)
160                                 for factory in self.__addressbookFactories
161                                 for (f, id, name) in factory.get_addressbooks()
162                         )
163                 contacts = (
164                         ("-".join([str(bookIndex), contactId]), contactName)
165                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
166                                         for (contactId, contactName) in addressbook.get_contacts()
167                 )
168                 sortedContacts = self.__sort_contacts(contacts)
169                 return sortedContacts
170
171         def get_contact_details(self, contactId):
172                 """
173                 @returns Iterable of (Phone Type, Phone Number)
174                 """
175                 if self.__addressbooks is None:
176                         return []
177                 bookIndex, originalId = contactId.split("-", 1)
178                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
179
180         @staticmethod
181         def null_sorter(contacts):
182                 """
183                 Good for speed/low memory
184                 """
185                 return contacts
186
187         @staticmethod
188         def basic_firtname_sorter(contacts):
189                 """
190                 Expects names in "First Last" format
191                 """
192                 contactsWithKey = [
193                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
194                                 for (contactId, contactName) in contacts
195                 ]
196                 contactsWithKey.sort()
197                 return (contactData for (lastName, contactData) in contactsWithKey)
198
199         @staticmethod
200         def basic_lastname_sorter(contacts):
201                 """
202                 Expects names in "First Last" format
203                 """
204                 contactsWithKey = [
205                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
206                                 for (contactId, contactName) in contacts
207                 ]
208                 contactsWithKey.sort()
209                 return (contactData for (lastName, contactData) in contactsWithKey)
210
211         @staticmethod
212         def reversed_firtname_sorter(contacts):
213                 """
214                 Expects names in "Last, First" format
215                 """
216                 contactsWithKey = [
217                         (contactName.split(", ", 1)[-1], (contactId, contactName))
218                                 for (contactId, contactName) in contacts
219                 ]
220                 contactsWithKey.sort()
221                 return (contactData for (lastName, contactData) in contactsWithKey)
222
223         @staticmethod
224         def reversed_lastname_sorter(contacts):
225                 """
226                 Expects names in "Last, First" format
227                 """
228                 contactsWithKey = [
229                         (contactName.split(", ", 1)[0], (contactId, contactName))
230                                 for (contactId, contactName) in contacts
231                 ]
232                 contactsWithKey.sort()
233                 return (contactData for (lastName, contactData) in contactsWithKey)
234
235         @staticmethod
236         def guess_firstname(name):
237                 if ", " in name:
238                         return name.split(", ", 1)[-1]
239                 else:
240                         return name.rsplit(" ", 1)[0]
241
242         @staticmethod
243         def guess_lastname(name):
244                 if ", " in name:
245                         return name.split(", ", 1)[0]
246                 else:
247                         return name.rsplit(" ", 1)[-1]
248
249         @classmethod
250         def advanced_firstname_sorter(cls, contacts):
251                 contactsWithKey = [
252                         (cls.guess_firstname(contactName), (contactId, contactName))
253                                 for (contactId, contactName) in contacts
254                 ]
255                 contactsWithKey.sort()
256                 return (contactData for (lastName, contactData) in contactsWithKey)
257
258         @classmethod
259         def advanced_lastname_sorter(cls, contacts):
260                 contactsWithKey = [
261                         (cls.guess_lastname(contactName), (contactId, contactName))
262                                 for (contactId, contactName) in contacts
263                 ]
264                 contactsWithKey.sort()
265                 return (contactData for (lastName, contactData) in contactsWithKey)
266
267
268 class PhoneTypeSelector(object):
269
270         ACTION_CANCEL = "cancel"
271         ACTION_SELECT = "select"
272         ACTION_DIAL = "dial"
273         ACTION_SEND_SMS = "sms"
274
275         def __init__(self, widgetTree, gcBackend):
276                 self._clipboard = gtk.clipboard_get()
277                 self._gcBackend = gcBackend
278                 self._widgetTree = widgetTree
279
280                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
281                 self._smsDialog = SmsEntryDialog(self._widgetTree)
282
283                 self._smsButton = self._widgetTree.get_widget("sms_button")
284                 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
285
286                 self._dialButton = self._widgetTree.get_widget("dial_button")
287                 self._dialButton.connect("clicked", self._on_phonetype_dial)
288
289                 self._selectButton = self._widgetTree.get_widget("select_button")
290                 self._selectButton.connect("clicked", self._on_phonetype_select)
291
292                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
293                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
294
295                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
296                 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
297                 self._scrollWindow = self._messagesView.get_parent()
298
299                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
300                 self._typeviewselection = None
301                 self._typeview = self._widgetTree.get_widget("phonetypes")
302                 self._typeview.connect("row-activated", self._on_phonetype_select)
303
304                 self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
305
306                 self._action = self.ACTION_CANCEL
307
308         def run(self, contactDetails, messages = (), parent = None):
309                 self._action = self.ACTION_CANCEL
310
311                 # Add the column to the phone selection tree view
312                 self._typemodel.clear()
313                 self._typeview.set_model(self._typemodel)
314
315                 textrenderer = gtk.CellRendererText()
316                 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
317                 self._typeview.append_column(numberColumn)
318
319                 textrenderer = gtk.CellRendererText()
320                 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
321                 self._typeview.append_column(typeColumn)
322
323                 for phoneType, phoneNumber in contactDetails:
324                         display = " - ".join((phoneNumber, phoneType))
325                         display = phoneType
326                         row = (phoneNumber, display)
327                         self._typemodel.append(row)
328
329                 self._typeviewselection = self._typeview.get_selection()
330                 self._typeviewselection.set_mode(gtk.SELECTION_NONE)
331                 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
332
333                 # Add the column to the messages tree view
334                 self._messagemodel.clear()
335                 self._messagesView.set_model(self._messagemodel)
336
337                 textrenderer = gtk.CellRendererText()
338                 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
339                 textrenderer.set_property("wrap-width", 450)
340                 messageColumn = gtk.TreeViewColumn("")
341                 messageColumn.pack_start(textrenderer, expand=True)
342                 messageColumn.add_attribute(textrenderer, "markup", 0)
343                 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
344                 self._messagesView.append_column(messageColumn)
345                 self._messagesView.set_headers_visible(False)
346
347                 if messages:
348                         for message in messages:
349                                 row = (message, )
350                                 self._messagemodel.append(row)
351                         self._messagesView.show()
352                         self._scrollWindow.show()
353                         messagesSelection = self._messagesView.get_selection()
354                         messagesSelection.select_path((len(messages)-1, ))
355                 else:
356                         self._messagesView.hide()
357                         self._scrollWindow.hide()
358
359                 if parent is not None:
360                         self._dialog.set_transient_for(parent)
361
362                 try:
363                         self._dialog.show()
364                         if messages:
365                                 self._messagesView.scroll_to_cell((len(messages)-1, ))
366
367                         userResponse = self._dialog.run()
368                 finally:
369                         self._dialog.hide()
370
371                 if userResponse == gtk.RESPONSE_OK:
372                         phoneNumber = self._get_number()
373                         phoneNumber = make_ugly(phoneNumber)
374                 else:
375                         phoneNumber = ""
376                 if not phoneNumber:
377                         self._action = self.ACTION_CANCEL
378
379                 if self._action == self.ACTION_SEND_SMS:
380                         smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
381                         if not smsMessage:
382                                 phoneNumber = ""
383                                 self._action = self.ACTION_CANCEL
384                 else:
385                         smsMessage = ""
386
387                 self._messagesView.remove_column(messageColumn)
388                 self._messagesView.set_model(None)
389
390                 self._typeviewselection.unselect_all()
391                 self._typeview.remove_column(numberColumn)
392                 self._typeview.remove_column(typeColumn)
393                 self._typeview.set_model(None)
394
395                 return self._action, phoneNumber, smsMessage
396
397         def _get_number(self):
398                 model, itr = self._typeviewselection.get_selected()
399                 if not itr:
400                         return ""
401
402                 phoneNumber = self._typemodel.get_value(itr, 0)
403                 return phoneNumber
404
405         def _on_phonetype_dial(self, *args):
406                 self._dialog.response(gtk.RESPONSE_OK)
407                 self._action = self.ACTION_DIAL
408
409         def _on_phonetype_send_sms(self, *args):
410                 self._dialog.response(gtk.RESPONSE_OK)
411                 self._action = self.ACTION_SEND_SMS
412
413         def _on_phonetype_select(self, *args):
414                 self._dialog.response(gtk.RESPONSE_OK)
415                 self._action = self.ACTION_SELECT
416
417         def _on_phonetype_cancel(self, *args):
418                 self._dialog.response(gtk.RESPONSE_CANCEL)
419                 self._action = self.ACTION_CANCEL
420
421         def _on_key_press(self, widget, event):
422                 try:
423                         if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
424                                 message = "\n".join(
425                                         messagePart[0]
426                                         for messagePart in self._messagemodel
427                                 )
428                                 # For some reason this kills clipboard stuff
429                                 #self._clipboard.set_text(message)
430                 except Exception, e:
431                         _moduleLogger.exception(str(e))
432
433
434 class SmsEntryDialog(object):
435         """
436         @todo Add multi-SMS messages like GoogleVoice
437         """
438
439         MAX_CHAR = 160
440
441         def __init__(self, widgetTree):
442                 self._clipboard = gtk.clipboard_get()
443                 self._widgetTree = widgetTree
444                 self._dialog = self._widgetTree.get_widget("smsDialog")
445
446                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
447                 self._smsButton.connect("clicked", self._on_send)
448
449                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
450                 self._cancelButton.connect("clicked", self._on_cancel)
451
452                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
453
454                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
455                 self._messagesView = self._widgetTree.get_widget("smsMessages")
456                 self._scrollWindow = self._messagesView.get_parent()
457
458                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
459                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
460
461                 self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
462
463         def run(self, number, messages = (), parent = None):
464                 # Add the column to the messages tree view
465                 self._messagemodel.clear()
466                 self._messagesView.set_model(self._messagemodel)
467
468                 textrenderer = gtk.CellRendererText()
469                 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
470                 textrenderer.set_property("wrap-width", 450)
471                 messageColumn = gtk.TreeViewColumn("")
472                 messageColumn.pack_start(textrenderer, expand=True)
473                 messageColumn.add_attribute(textrenderer, "markup", 0)
474                 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
475                 self._messagesView.append_column(messageColumn)
476                 self._messagesView.set_headers_visible(False)
477
478                 if messages:
479                         for message in messages:
480                                 row = (message, )
481                                 self._messagemodel.append(row)
482                         self._messagesView.show()
483                         self._scrollWindow.show()
484                         messagesSelection = self._messagesView.get_selection()
485                         messagesSelection.select_path((len(messages)-1, ))
486                 else:
487                         self._messagesView.hide()
488                         self._scrollWindow.hide()
489
490                 self._smsEntry.get_buffer().set_text("")
491                 self._update_letter_count()
492
493                 if parent is not None:
494                         self._dialog.set_transient_for(parent)
495
496                 try:
497                         self._dialog.show()
498                         if messages:
499                                 self._messagesView.scroll_to_cell((len(messages)-1, ))
500                         self._smsEntry.grab_focus()
501
502                         userResponse = self._dialog.run()
503                 finally:
504                         self._dialog.hide()
505
506                 if userResponse == gtk.RESPONSE_OK:
507                         entryBuffer = self._smsEntry.get_buffer()
508                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
509                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
510                 else:
511                         enteredMessage = ""
512
513                 self._messagesView.remove_column(messageColumn)
514                 self._messagesView.set_model(None)
515
516                 return enteredMessage.strip()
517
518         def _update_letter_count(self, *args):
519                 entryLength = self._smsEntry.get_buffer().get_char_count()
520                 charsLeft = self.MAX_CHAR - entryLength
521                 self._letterCountLabel.set_text(str(charsLeft))
522                 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
523                         self._smsButton.set_sensitive(False)
524                 else:
525                         self._smsButton.set_sensitive(True)
526
527         def _on_entry_changed(self, *args):
528                 self._update_letter_count()
529
530         def _on_send(self, *args):
531                 self._dialog.response(gtk.RESPONSE_OK)
532
533         def _on_cancel(self, *args):
534                 self._dialog.response(gtk.RESPONSE_CANCEL)
535
536         def _on_key_press(self, widget, event):
537                 try:
538                         if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
539                                 message = "\n".join(
540                                         messagePart[0]
541                                         for messagePart in self._messagemodel
542                                 )
543                                 # For some reason this kills clipboard stuff
544                                 #self._clipboard.set_text(message)
545                 except Exception, e:
546                         _moduleLogger.exception(str(e))
547
548
549 class Dialpad(object):
550
551         def __init__(self, widgetTree, errorDisplay):
552                 self._clipboard = gtk.clipboard_get()
553                 self._errorDisplay = errorDisplay
554                 self._smsDialog = SmsEntryDialog(widgetTree)
555
556                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
557                 self._smsButton = widgetTree.get_widget("sms")
558                 self._dialButton = widgetTree.get_widget("dial")
559                 self._backButton = widgetTree.get_widget("back")
560                 self._phonenumber = ""
561                 self._prettynumber = ""
562
563                 callbackMapping = {
564                         "on_digit_clicked": self._on_digit_clicked,
565                 }
566                 widgetTree.signal_autoconnect(callbackMapping)
567                 self._dialButton.connect("clicked", self._on_dial_clicked)
568                 self._smsButton.connect("clicked", self._on_sms_clicked)
569
570                 self._originalLabel = self._backButton.get_label()
571                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
572                 self._backTapHandler.on_tap = self._on_backspace
573                 self._backTapHandler.on_hold = self._on_clearall
574                 self._backTapHandler.on_holding = self._set_clear_button
575                 self._backTapHandler.on_cancel = self._reset_back_button
576
577                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
578                 self._keyPressEventId = 0
579
580         def enable(self):
581                 self._dialButton.grab_focus()
582                 self._backTapHandler.enable()
583                 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
584
585         def disable(self):
586                 self._window.disconnect(self._keyPressEventId)
587                 self._keyPressEventId = 0
588                 self._reset_back_button()
589                 self._backTapHandler.disable()
590
591         def number_selected(self, action, number, message):
592                 """
593                 @note Actual dial function is patched in later
594                 """
595                 raise NotImplementedError("Horrible unknown error has occurred")
596
597         def get_number(self):
598                 return self._phonenumber
599
600         def set_number(self, number):
601                 """
602                 Set the number to dial
603                 """
604                 try:
605                         self._phonenumber = make_ugly(number)
606                         self._prettynumber = make_pretty(self._phonenumber)
607                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
608                 except TypeError, e:
609                         self._errorDisplay.push_exception()
610
611         def clear(self):
612                 self.set_number("")
613
614         @staticmethod
615         def name():
616                 return "Dialpad"
617
618         def load_settings(self, config, section):
619                 pass
620
621         def save_settings(self, config, section):
622                 """
623                 @note Thread Agnostic
624                 """
625                 pass
626
627         def _on_key_press(self, widget, event):
628                 try:
629                         if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
630                                 contents = self._clipboard.wait_for_text()
631                                 if contents is not None:
632                                         self.set_number(contents)
633                 except Exception, e:
634                         self._errorDisplay.push_exception()
635
636         def _on_sms_clicked(self, widget):
637                 try:
638                         action = PhoneTypeSelector.ACTION_SEND_SMS
639                         phoneNumber = self.get_number()
640
641                         message = self._smsDialog.run(phoneNumber, (), self._window)
642                         if not message:
643                                 phoneNumber = ""
644                                 action = PhoneTypeSelector.ACTION_CANCEL
645
646                         if action == PhoneTypeSelector.ACTION_CANCEL:
647                                 return
648                         self.number_selected(action, phoneNumber, message)
649                 except Exception, e:
650                         self._errorDisplay.push_exception()
651
652         def _on_dial_clicked(self, widget):
653                 try:
654                         action = PhoneTypeSelector.ACTION_DIAL
655                         phoneNumber = self.get_number()
656                         message = ""
657                         self.number_selected(action, phoneNumber, message)
658                 except Exception, e:
659                         self._errorDisplay.push_exception()
660
661         def _on_digit_clicked(self, widget):
662                 try:
663                         self.set_number(self._phonenumber + widget.get_name()[-1])
664                 except Exception, e:
665                         self._errorDisplay.push_exception()
666
667         def _on_backspace(self, taps):
668                 try:
669                         self.set_number(self._phonenumber[:-taps])
670                         self._reset_back_button()
671                 except Exception, e:
672                         self._errorDisplay.push_exception()
673
674         def _on_clearall(self, taps):
675                 try:
676                         self.clear()
677                         self._reset_back_button()
678                 except Exception, e:
679                         self._errorDisplay.push_exception()
680                 return False
681
682         def _set_clear_button(self):
683                 try:
684                         self._backButton.set_label("gtk-clear")
685                 except Exception, e:
686                         self._errorDisplay.push_exception()
687
688         def _reset_back_button(self):
689                 try:
690                         self._backButton.set_label(self._originalLabel)
691                 except Exception, e:
692                         self._errorDisplay.push_exception()
693
694
695 class AccountInfo(object):
696
697         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
698                 self._errorDisplay = errorDisplay
699                 self._backend = backend
700                 self._isPopulated = False
701                 self._alarmHandler = alarmHandler
702                 self._notifyOnMissed = False
703                 self._notifyOnVoicemail = False
704                 self._notifyOnSms = False
705
706                 self._callbackList = []
707                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
708                 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
709                 self._onCallbackSelectChangedId = 0
710
711                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
712                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
713                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
714                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
715                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
716                 self._onNotifyToggled = 0
717                 self._onMinutesChanged = 0
718                 self._onMissedToggled = 0
719                 self._onVoicemailToggled = 0
720                 self._onSmsToggled = 0
721                 self._applyAlarmTimeoutId = None
722
723                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
724                 self._defaultCallback = ""
725
726         def enable(self):
727                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
728
729                 self._accountViewNumberDisplay.set_use_markup(True)
730                 self.set_account_number("")
731
732                 del self._callbackList[:]
733                 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
734
735                 if self._alarmHandler is not None:
736                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
737                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
738                         self._missedCheckbox.set_active(self._notifyOnMissed)
739                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
740                         self._smsCheckbox.set_active(self._notifyOnSms)
741
742                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
743                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
744                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
745                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
746                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
747                 else:
748                         self._notifyCheckbox.set_sensitive(False)
749                         self._minutesEntryButton.set_sensitive(False)
750                         self._missedCheckbox.set_sensitive(False)
751                         self._voicemailCheckbox.set_sensitive(False)
752                         self._smsCheckbox.set_sensitive(False)
753
754                 self.update(force=True)
755
756         def disable(self):
757                 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
758                 self._onCallbackSelectChangedId = 0
759
760                 if self._alarmHandler is not None:
761                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
762                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
763                         self._missedCheckbox.disconnect(self._onNotifyToggled)
764                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
765                         self._smsCheckbox.disconnect(self._onNotifyToggled)
766                         self._onNotifyToggled = 0
767                         self._onMinutesChanged = 0
768                         self._onMissedToggled = 0
769                         self._onVoicemailToggled = 0
770                         self._onSmsToggled = 0
771                 else:
772                         self._notifyCheckbox.set_sensitive(True)
773                         self._minutesEntryButton.set_sensitive(True)
774                         self._missedCheckbox.set_sensitive(True)
775                         self._voicemailCheckbox.set_sensitive(True)
776                         self._smsCheckbox.set_sensitive(True)
777
778                 self.clear()
779                 del self._callbackList[:]
780
781         def get_selected_callback_number(self):
782                 currentLabel = self._callbackSelectButton.get_label()
783                 if currentLabel is not None:
784                         return make_ugly(currentLabel)
785                 else:
786                         return ""
787
788         def set_account_number(self, number):
789                 """
790                 Displays current account number
791                 """
792                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
793
794         def update(self, force = False):
795                 if not force and self._isPopulated:
796                         return False
797                 self._populate_callback_combo()
798                 self.set_account_number(self._backend.get_account_number())
799                 return True
800
801         def clear(self):
802                 self._callbackSelectButton.set_label("No Callback Number")
803                 self.set_account_number("")
804                 self._isPopulated = False
805
806         def save_everything(self):
807                 raise NotImplementedError
808
809         @staticmethod
810         def name():
811                 return "Account Info"
812
813         def load_settings(self, config, section):
814                 self._defaultCallback = config.get(section, "callback")
815                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
816                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
817                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
818
819         def save_settings(self, config, section):
820                 """
821                 @note Thread Agnostic
822                 """
823                 callback = self.get_selected_callback_number()
824                 config.set(section, "callback", callback)
825                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
826                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
827                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
828
829         def _populate_callback_combo(self):
830                 self._isPopulated = True
831                 del self._callbackList[:]
832                 try:
833                         callbackNumbers = self._backend.get_callback_numbers()
834                 except Exception, e:
835                         self._errorDisplay.push_exception()
836                         self._isPopulated = False
837                         return
838
839                 if len(callbackNumbers) == 0:
840                         callbackNumbers = {"": "No callback numbers available"}
841
842                 for number, description in callbackNumbers.iteritems():
843                         self._callbackList.append((make_pretty(number), description))
844
845                 self._set_callback_number(self._defaultCallback)
846
847         def _set_callback_number(self, number):
848                 try:
849                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
850                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
851                         elif number == self._backend.get_callback_number() and 0 < len(number):
852                                 _moduleLogger.warning(
853                                         "Callback number already is %s" % (
854                                                 self._backend.get_callback_number(),
855                                         ),
856                                 )
857                         else:
858                                 self._backend.set_callback_number(number)
859                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
860                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
861                                 )
862                                 prettyNumber = make_pretty(number)
863                                 if len(prettyNumber) == 0:
864                                         prettyNumber = "No Callback Number"
865                                 self._callbackSelectButton.set_label(prettyNumber)
866                                 _moduleLogger.info(
867                                         "Callback number set to %s" % (
868                                                 self._backend.get_callback_number(),
869                                         ),
870                                 )
871                 except Exception, e:
872                         self._errorDisplay.push_exception()
873
874         def _update_alarm_settings(self, recurrence):
875                 try:
876                         isEnabled = self._notifyCheckbox.get_active()
877                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
878                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
879                 finally:
880                         self.save_everything()
881                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
882                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
883
884         def _on_callbackentry_clicked(self, *args):
885                 try:
886                         actualSelection = make_pretty(self.get_selected_callback_number())
887
888                         userOptions = dict(
889                                 (number, "%s (%s)" % (number, description))
890                                 for (number, description) in self._callbackList
891                         )
892                         defaultSelection = userOptions.get(actualSelection, actualSelection)
893
894                         userSelection = hildonize.touch_selector_entry(
895                                 self._window,
896                                 "Callback Number",
897                                 list(userOptions.itervalues()),
898                                 defaultSelection,
899                         )
900                         reversedUserOptions = dict(
901                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
902                         )
903                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
904
905                         number = make_ugly(selectedNumber)
906                         self._set_callback_number(number)
907                 except RuntimeError, e:
908                         _moduleLogger.exception("%s" % str(e))
909                 except Exception, e:
910                         self._errorDisplay.push_exception()
911
912         def _on_notify_toggled(self, *args):
913                 try:
914                         if self._applyAlarmTimeoutId is not None:
915                                 gobject.source_remove(self._applyAlarmTimeoutId)
916                                 self._applyAlarmTimeoutId = None
917                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
918                 except Exception, e:
919                         self._errorDisplay.push_exception()
920
921         def _on_minutes_clicked(self, *args):
922                 recurrenceChoices = [
923                         (1, "1 minute"),
924                         (2, "2 minutes"),
925                         (3, "3 minutes"),
926                         (5, "5 minutes"),
927                         (8, "8 minutes"),
928                         (10, "10 minutes"),
929                         (15, "15 minutes"),
930                         (30, "30 minutes"),
931                         (45, "45 minutes"),
932                         (60, "1 hour"),
933                         (3*60, "3 hours"),
934                         (6*60, "6 hours"),
935                         (12*60, "12 hours"),
936                 ]
937                 try:
938                         actualSelection = self._alarmHandler.recurrence
939
940                         closestSelectionIndex = 0
941                         for i, possible in enumerate(recurrenceChoices):
942                                 if possible[0] <= actualSelection:
943                                         closestSelectionIndex = i
944                         recurrenceIndex = hildonize.touch_selector(
945                                 self._window,
946                                 "Minutes",
947                                 (("%s" % m[1]) for m in recurrenceChoices),
948                                 closestSelectionIndex,
949                         )
950                         recurrence = recurrenceChoices[recurrenceIndex][0]
951
952                         self._update_alarm_settings(recurrence)
953                 except RuntimeError, e:
954                         _moduleLogger.exception("%s" % str(e))
955                 except Exception, e:
956                         self._errorDisplay.push_exception()
957
958         def _on_apply_timeout(self, *args):
959                 try:
960                         self._applyAlarmTimeoutId = None
961
962                         self._update_alarm_settings(self._alarmHandler.recurrence)
963                 except Exception, e:
964                         self._errorDisplay.push_exception()
965                 return False
966
967         def _on_missed_toggled(self, *args):
968                 try:
969                         self._notifyOnMissed = self._missedCheckbox.get_active()
970                         self.save_everything()
971                 except Exception, e:
972                         self._errorDisplay.push_exception()
973
974         def _on_voicemail_toggled(self, *args):
975                 try:
976                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
977                         self.save_everything()
978                 except Exception, e:
979                         self._errorDisplay.push_exception()
980
981         def _on_sms_toggled(self, *args):
982                 try:
983                         self._notifyOnSms = self._smsCheckbox.get_active()
984                         self.save_everything()
985                 except Exception, e:
986                         self._errorDisplay.push_exception()
987
988
989 class RecentCallsView(object):
990
991         NUMBER_IDX = 0
992         DATE_IDX = 1
993         ACTION_IDX = 2
994         FROM_IDX = 3
995
996         def __init__(self, widgetTree, backend, errorDisplay):
997                 self._errorDisplay = errorDisplay
998                 self._backend = backend
999
1000                 self._isPopulated = False
1001                 self._recentmodel = gtk.ListStore(
1002                         gobject.TYPE_STRING, # number
1003                         gobject.TYPE_STRING, # date
1004                         gobject.TYPE_STRING, # action
1005                         gobject.TYPE_STRING, # from
1006                 )
1007                 self._recentview = widgetTree.get_widget("recentview")
1008                 self._recentviewselection = None
1009                 self._onRecentviewRowActivatedId = 0
1010
1011                 textrenderer = gtk.CellRendererText()
1012                 textrenderer.set_property("yalign", 0)
1013                 self._dateColumn = gtk.TreeViewColumn("Date")
1014                 self._dateColumn.pack_start(textrenderer, expand=True)
1015                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1016
1017                 textrenderer = gtk.CellRendererText()
1018                 textrenderer.set_property("yalign", 0)
1019                 self._actionColumn = gtk.TreeViewColumn("Action")
1020                 self._actionColumn.pack_start(textrenderer, expand=True)
1021                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1022
1023                 textrenderer = gtk.CellRendererText()
1024                 textrenderer.set_property("yalign", 0)
1025                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1026                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1027                 self._numberColumn = gtk.TreeViewColumn("Number")
1028                 self._numberColumn.pack_start(textrenderer, expand=True)
1029                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1030
1031                 textrenderer = gtk.CellRendererText()
1032                 textrenderer.set_property("yalign", 0)
1033                 hildonize.set_cell_thumb_selectable(textrenderer)
1034                 self._nameColumn = gtk.TreeViewColumn("From")
1035                 self._nameColumn.pack_start(textrenderer, expand=True)
1036                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1037                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1038
1039                 self._window = gtk_toolbox.find_parent_window(self._recentview)
1040                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1041
1042                 self._updateSink = gtk_toolbox.threaded_stage(
1043                         gtk_toolbox.comap(
1044                                 self._idly_populate_recentview,
1045                                 gtk_toolbox.null_sink(),
1046                         )
1047                 )
1048
1049         def enable(self):
1050                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1051                 self._recentview.set_model(self._recentmodel)
1052
1053                 self._recentview.append_column(self._dateColumn)
1054                 self._recentview.append_column(self._actionColumn)
1055                 self._recentview.append_column(self._numberColumn)
1056                 self._recentview.append_column(self._nameColumn)
1057                 self._recentviewselection = self._recentview.get_selection()
1058                 self._recentviewselection.set_mode(gtk.SELECTION_NONE)
1059
1060                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1061
1062         def disable(self):
1063                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1064
1065                 self.clear()
1066
1067                 self._recentview.remove_column(self._dateColumn)
1068                 self._recentview.remove_column(self._actionColumn)
1069                 self._recentview.remove_column(self._nameColumn)
1070                 self._recentview.remove_column(self._numberColumn)
1071                 self._recentview.set_model(None)
1072
1073         def number_selected(self, action, number, message):
1074                 """
1075                 @note Actual dial function is patched in later
1076                 """
1077                 raise NotImplementedError("Horrible unknown error has occurred")
1078
1079         def update(self, force = False):
1080                 if not force and self._isPopulated:
1081                         return False
1082                 self._updateSink.send(())
1083                 return True
1084
1085         def clear(self):
1086                 self._isPopulated = False
1087                 self._recentmodel.clear()
1088
1089         @staticmethod
1090         def name():
1091                 return "Recent Calls"
1092
1093         def load_settings(self, config, section):
1094                 pass
1095
1096         def save_settings(self, config, section):
1097                 """
1098                 @note Thread Agnostic
1099                 """
1100                 pass
1101
1102         def _idly_populate_recentview(self):
1103                 with gtk_toolbox.gtk_lock():
1104                         banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1105                 try:
1106                         self._recentmodel.clear()
1107                         self._isPopulated = True
1108
1109                         try:
1110                                 recentItems = self._backend.get_recent()
1111                         except Exception, e:
1112                                 self._errorDisplay.push_exception_with_lock()
1113                                 self._isPopulated = False
1114                                 recentItems = []
1115
1116                         recentItems = (
1117                                 gv_backend.decorate_recent(data)
1118                                 for data in gv_backend.sort_messages(recentItems)
1119                         )
1120
1121                         for personName, phoneNumber, date, action in recentItems:
1122                                 if not personName:
1123                                         personName = "Unknown"
1124                                 date = abbrev_relative_date(date)
1125                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1126                                 prettyNumber = make_pretty(prettyNumber)
1127                                 item = (prettyNumber, date, action.capitalize(), personName)
1128                                 with gtk_toolbox.gtk_lock():
1129                                         self._recentmodel.append(item)
1130                 except Exception, e:
1131                         self._errorDisplay.push_exception_with_lock()
1132                 finally:
1133                         with gtk_toolbox.gtk_lock():
1134                                 hildonize.show_busy_banner_end(banner)
1135
1136                 return False
1137
1138         def _on_recentview_row_activated(self, treeview, path, view_column):
1139                 try:
1140                         itr = self._recentmodel.get_iter(path)
1141                         if not itr:
1142                                 return
1143
1144                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1145                         number = make_ugly(number)
1146                         contactPhoneNumbers = [("Phone", number)]
1147                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
1148
1149                         action, phoneNumber, message = self._phoneTypeSelector.run(
1150                                 contactPhoneNumbers,
1151                                 messages = (description, ),
1152                                 parent = self._window,
1153                         )
1154                         if action == PhoneTypeSelector.ACTION_CANCEL:
1155                                 return
1156                         assert phoneNumber, "A lack of phone number exists"
1157
1158                         self.number_selected(action, phoneNumber, message)
1159                         self._recentviewselection.unselect_all()
1160                 except Exception, e:
1161                         self._errorDisplay.push_exception()
1162
1163
1164 class MessagesView(object):
1165
1166         NUMBER_IDX = 0
1167         DATE_IDX = 1
1168         HEADER_IDX = 2
1169         MESSAGE_IDX = 3
1170         MESSAGES_IDX = 4
1171
1172         def __init__(self, widgetTree, backend, errorDisplay):
1173                 self._errorDisplay = errorDisplay
1174                 self._backend = backend
1175
1176                 self._isPopulated = False
1177                 self._messagemodel = gtk.ListStore(
1178                         gobject.TYPE_STRING, # number
1179                         gobject.TYPE_STRING, # date
1180                         gobject.TYPE_STRING, # header
1181                         gobject.TYPE_STRING, # message
1182                         object, # messages
1183                 )
1184                 self._messageview = widgetTree.get_widget("messages_view")
1185                 self._messageviewselection = None
1186                 self._onMessageviewRowActivatedId = 0
1187
1188                 self._messageRenderer = gtk.CellRendererText()
1189                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1190                 self._messageRenderer.set_property("wrap-width", 500)
1191                 self._messageColumn = gtk.TreeViewColumn("Messages")
1192                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1193                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1194                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1195
1196                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1197                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1198
1199                 self._updateSink = gtk_toolbox.threaded_stage(
1200                         gtk_toolbox.comap(
1201                                 self._idly_populate_messageview,
1202                                 gtk_toolbox.null_sink(),
1203                         )
1204                 )
1205
1206         def enable(self):
1207                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1208                 self._messageview.set_model(self._messagemodel)
1209                 self._messageview.set_headers_visible(False)
1210
1211                 self._messageview.append_column(self._messageColumn)
1212                 self._messageviewselection = self._messageview.get_selection()
1213                 self._messageviewselection.set_mode(gtk.SELECTION_NONE)
1214
1215                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1216
1217         def disable(self):
1218                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1219
1220                 self.clear()
1221
1222                 self._messageview.remove_column(self._messageColumn)
1223                 self._messageview.set_model(None)
1224
1225         def number_selected(self, action, number, message):
1226                 """
1227                 @note Actual dial function is patched in later
1228                 """
1229                 raise NotImplementedError("Horrible unknown error has occurred")
1230
1231         def update(self, force = False):
1232                 if not force and self._isPopulated:
1233                         return False
1234                 self._updateSink.send(())
1235                 return True
1236
1237         def clear(self):
1238                 self._isPopulated = False
1239                 self._messagemodel.clear()
1240
1241         @staticmethod
1242         def name():
1243                 return "Messages"
1244
1245         def load_settings(self, config, section):
1246                 pass
1247
1248         def save_settings(self, config, section):
1249                 """
1250                 @note Thread Agnostic
1251                 """
1252                 pass
1253
1254         _MIN_MESSAGES_SHOWN = 4
1255
1256         def _idly_populate_messageview(self):
1257                 with gtk_toolbox.gtk_lock():
1258                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1259                 try:
1260                         self._messagemodel.clear()
1261                         self._isPopulated = True
1262
1263                         try:
1264                                 messageItems = self._backend.get_messages()
1265                         except Exception, e:
1266                                 self._errorDisplay.push_exception_with_lock()
1267                                 self._isPopulated = False
1268                                 messageItems = []
1269
1270                         messageItems = (
1271                                 gv_backend.decorate_message(message)
1272                                 for message in gv_backend.sort_messages(messageItems)
1273                         )
1274
1275                         for header, number, relativeDate, messages in messageItems:
1276                                 prettyNumber = number[2:] if number.startswith("+1") else number
1277                                 prettyNumber = make_pretty(prettyNumber)
1278
1279                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1280                                 expandedMessages = [firstMessage]
1281                                 expandedMessages.extend(messages)
1282                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1283                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1284                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1285                                         collapsedMessages = [firstMessage, secondMessage]
1286                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1287                                 else:
1288                                         collapsedMessages = expandedMessages
1289
1290                                 number = make_ugly(number)
1291
1292                                 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1293                                 with gtk_toolbox.gtk_lock():
1294                                         self._messagemodel.append(row)
1295                 except Exception, e:
1296                         self._errorDisplay.push_exception_with_lock()
1297                 finally:
1298                         with gtk_toolbox.gtk_lock():
1299                                 hildonize.show_busy_banner_end(banner)
1300
1301                 return False
1302
1303         def _on_messageview_row_activated(self, treeview, path, view_column):
1304                 try:
1305                         itr = self._messagemodel.get_iter(path)
1306                         if not itr:
1307                                 return
1308
1309                         contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1310                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1311
1312                         action, phoneNumber, message = self._phoneTypeSelector.run(
1313                                 contactPhoneNumbers,
1314                                 messages = description,
1315                                 parent = self._window,
1316                         )
1317                         if action == PhoneTypeSelector.ACTION_CANCEL:
1318                                 return
1319                         assert phoneNumber, "A lock of phone number exists"
1320
1321                         self.number_selected(action, phoneNumber, message)
1322                         self._messageviewselection.unselect_all()
1323                 except Exception, e:
1324                         self._errorDisplay.push_exception()
1325
1326
1327 class ContactsView(object):
1328
1329         def __init__(self, widgetTree, backend, errorDisplay):
1330                 self._errorDisplay = errorDisplay
1331                 self._backend = backend
1332
1333                 self._addressBook = None
1334                 self._selectedComboIndex = 0
1335                 self._addressBookFactories = [null_backend.NullAddressBook()]
1336
1337                 self._booksList = []
1338                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1339
1340                 self._isPopulated = False
1341                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1342                 self._contactsviewselection = None
1343                 self._contactsview = widgetTree.get_widget("contactsview")
1344
1345                 self._contactColumn = gtk.TreeViewColumn("Contact")
1346                 displayContactSource = False
1347                 if displayContactSource:
1348                         textrenderer = gtk.CellRendererText()
1349                         self._contactColumn.pack_start(textrenderer, expand=False)
1350                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1351                 textrenderer = gtk.CellRendererText()
1352                 hildonize.set_cell_thumb_selectable(textrenderer)
1353                 self._contactColumn.pack_start(textrenderer, expand=True)
1354                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1355                 textrenderer = gtk.CellRendererText()
1356                 self._contactColumn.pack_start(textrenderer, expand=True)
1357                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1358                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1359                 self._contactColumn.set_sort_column_id(1)
1360                 self._contactColumn.set_visible(True)
1361
1362                 self._onContactsviewRowActivatedId = 0
1363                 self._onAddressbookButtonChangedId = 0
1364                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1365                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1366
1367                 self._updateSink = gtk_toolbox.threaded_stage(
1368                         gtk_toolbox.comap(
1369                                 self._idly_populate_contactsview,
1370                                 gtk_toolbox.null_sink(),
1371                         )
1372                 )
1373
1374         def enable(self):
1375                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1376
1377                 self._contactsview.set_model(self._contactsmodel)
1378                 self._contactsview.append_column(self._contactColumn)
1379                 self._contactsviewselection = self._contactsview.get_selection()
1380                 self._contactsviewselection.set_mode(gtk.SELECTION_NONE)
1381
1382                 del self._booksList[:]
1383                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1384                         if factoryName and bookName:
1385                                 entryName = "%s: %s" % (factoryName, bookName)
1386                         elif factoryName:
1387                                 entryName = factoryName
1388                         elif bookName:
1389                                 entryName = bookName
1390                         else:
1391                                 entryName = "Bad name (%d)" % factoryId
1392                         row = (str(factoryId), bookId, entryName)
1393                         self._booksList.append(row)
1394
1395                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1396                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1397
1398                 if len(self._booksList) <= self._selectedComboIndex:
1399                         self._selectedComboIndex = 0
1400                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1401
1402                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1403                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1404                 self.open_addressbook(selectedFactoryId, selectedBookId)
1405
1406         def disable(self):
1407                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1408                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1409
1410                 self.clear()
1411
1412                 self._bookSelectionButton.set_label("")
1413                 self._contactsview.set_model(None)
1414                 self._contactsview.remove_column(self._contactColumn)
1415
1416         def number_selected(self, action, number, message):
1417                 """
1418                 @note Actual dial function is patched in later
1419                 """
1420                 raise NotImplementedError("Horrible unknown error has occurred")
1421
1422         def get_addressbooks(self):
1423                 """
1424                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1425                 """
1426                 for i, factory in enumerate(self._addressBookFactories):
1427                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1428                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1429
1430         def open_addressbook(self, bookFactoryId, bookId):
1431                 bookFactoryIndex = int(bookFactoryId)
1432                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1433
1434                 forceUpdate = True if addressBook is not self._addressBook else False
1435
1436                 self._addressBook = addressBook
1437                 self.update(force=forceUpdate)
1438
1439         def update(self, force = False):
1440                 if not force and self._isPopulated:
1441                         return False
1442                 self._updateSink.send(())
1443                 return True
1444
1445         def clear(self):
1446                 self._isPopulated = False
1447                 self._contactsmodel.clear()
1448                 for factory in self._addressBookFactories:
1449                         factory.clear_caches()
1450                 self._addressBook.clear_caches()
1451
1452         def append(self, book):
1453                 self._addressBookFactories.append(book)
1454
1455         def extend(self, books):
1456                 self._addressBookFactories.extend(books)
1457
1458         @staticmethod
1459         def name():
1460                 return "Contacts"
1461
1462         def load_settings(self, config, sectionName):
1463                 try:
1464                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1465                 except ConfigParser.NoOptionError:
1466                         self._selectedComboIndex = 0
1467
1468         def save_settings(self, config, sectionName):
1469                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1470
1471         def _idly_populate_contactsview(self):
1472                 with gtk_toolbox.gtk_lock():
1473                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1474                 try:
1475                         addressBook = None
1476                         while addressBook is not self._addressBook:
1477                                 addressBook = self._addressBook
1478                                 with gtk_toolbox.gtk_lock():
1479                                         self._contactsview.set_model(None)
1480                                         self.clear()
1481
1482                                 try:
1483                                         contacts = addressBook.get_contacts()
1484                                 except Exception, e:
1485                                         contacts = []
1486                                         self._isPopulated = False
1487                                         self._errorDisplay.push_exception_with_lock()
1488                                 for contactId, contactName in contacts:
1489                                         contactType = (addressBook.contact_source_short_name(contactId), )
1490                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1491
1492                                 with gtk_toolbox.gtk_lock():
1493                                         self._contactsview.set_model(self._contactsmodel)
1494
1495                         self._isPopulated = True
1496                 except Exception, e:
1497                         self._errorDisplay.push_exception_with_lock()
1498                 finally:
1499                         with gtk_toolbox.gtk_lock():
1500                                 hildonize.show_busy_banner_end(banner)
1501                 return False
1502
1503         def _on_addressbook_button_changed(self, *args, **kwds):
1504                 try:
1505                         try:
1506                                 newSelectedComboIndex = hildonize.touch_selector(
1507                                         self._window,
1508                                         "Addressbook",
1509                                         (("%s" % m[2]) for m in self._booksList),
1510                                         self._selectedComboIndex,
1511                                 )
1512                         except RuntimeError:
1513                                 return
1514
1515                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1516                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1517                         self.open_addressbook(selectedFactoryId, selectedBookId)
1518                         self._selectedComboIndex = newSelectedComboIndex
1519                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1520                 except Exception, e:
1521                         self._errorDisplay.push_exception()
1522
1523         def _on_contactsview_row_activated(self, treeview, path, view_column):
1524                 try:
1525                         itr = self._contactsmodel.get_iter(path)
1526                         if not itr:
1527                                 return
1528
1529                         contactId = self._contactsmodel.get_value(itr, 3)
1530                         contactName = self._contactsmodel.get_value(itr, 1)
1531                         try:
1532                                 contactDetails = self._addressBook.get_contact_details(contactId)
1533                         except Exception, e:
1534                                 contactDetails = []
1535                                 self._errorDisplay.push_exception()
1536                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1537
1538                         if len(contactPhoneNumbers) == 0:
1539                                 return
1540
1541                         action, phoneNumber, message = self._phoneTypeSelector.run(
1542                                 contactPhoneNumbers,
1543                                 messages = (contactName, ),
1544                                 parent = self._window,
1545                         )
1546                         if action == PhoneTypeSelector.ACTION_CANCEL:
1547                                 return
1548                         assert phoneNumber, "A lack of phone number exists"
1549
1550                         self.number_selected(action, phoneNumber, message)
1551                         self._contactsviewselection.unselect_all()
1552                 except Exception, e:
1553                         self._errorDisplay.push_exception()