Very minor cleanups
[gc-dialer] / src / dialcentral / gc_dialer.py
1 #!/usr/bin/python2.5
2
3 # DialCentral - Front end for Google's Grand Central service.
4 # Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
5
6 # This library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
10
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # Lesser General Public License for more details.
15
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20
21 """
22 DialCentral: A phone dialer using GrandCentral
23 """
24
25 import sys
26 import gc
27 import os
28 import threading
29 import time
30 import warnings
31
32 import gobject
33 import gtk
34 import gtk.glade
35
36 try:
37         import hildon
38 except ImportError:
39         hildon = None
40
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         if len(phonenumber) < 3:
83                 return phonenumber
84
85         if phonenumber[0] == "0":
86                 prettynumber = ""
87                 prettynumber += "+%s" % phonenumber[0:3]
88                 if 3 < len(phonenumber):
89                         prettynumber += "-(%s)" % phonenumber[3:6]
90                         if 6 < len(phonenumber):
91                                 prettynumber += "-%s" % phonenumber[6:9]
92                                 if 9 < len(phonenumber):
93                                         prettynumber += "-%s" % phonenumber[9:]
94                 return prettynumber
95         elif len(phonenumber) <= 7:
96                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
97         elif len(phonenumber) > 8 and phonenumber[0] == "1":
98                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
99         elif len(phonenumber) > 7:
100                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
101         return prettynumber
102
103
104 def make_idler(func):
105         """
106         Decorator that makes a generator-function into a function that will continue execution on next call
107         """
108         a = []
109
110         def decorated_func(*args, **kwds):
111                 if not a:
112                         a.append(func(*args, **kwds))
113                 try:
114                         a[0].next()
115                         return True
116                 except StopIteration:
117                         del a[:]
118                         return False
119
120         decorated_func.__name__ = func.__name__
121         decorated_func.__doc__ = func.__doc__
122         decorated_func.__dict__.update(func.__dict__)
123
124         return decorated_func
125
126
127 class DummyAddressBook(object):
128         """
129         Minimal example of both an addressbook factory and an addressbook
130         """
131
132         def get_addressbooks(self):
133                 """
134                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
135                 """
136                 yield self, "", "None"
137
138         def open_addressbook(self, bookId):
139                 return self
140
141         @staticmethod
142         def contact_source_short_name(contactId):
143                 return ""
144
145         @staticmethod
146         def factory_name():
147                 return ""
148
149         @staticmethod
150         def get_contacts():
151                 """
152                 @returns Iterable of (contact id, contact name)
153                 """
154                 return []
155
156         @staticmethod
157         def get_contact_details(contactId):
158                 """
159                 @returns Iterable of (Phone Type, Phone Number)
160                 """
161                 return []
162
163
164 class MergedAddressBook(object):
165         """
166         Merger of all addressbooks
167         """
168
169         def __init__(self, addressbooks, sorter = None):
170                 self.__addressbooks = addressbooks
171                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
172
173         def get_addressbooks(self):
174                 """
175                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
176                 """
177                 yield self, "", ""
178
179         def open_addressbook(self, bookId):
180                 return self
181
182         def contact_source_short_name(self, contactId):
183                 bookIndex, originalId = contactId.split("-", 1)
184                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
185
186         @staticmethod
187         def factory_name():
188                 return "All Contacts"
189
190         def get_contacts(self):
191                 """
192                 @returns Iterable of (contact id, contact name)
193                 """
194                 contacts = (
195                         ("-".join([str(bookIndex), contactId]), contactName)
196                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
197                                         for (contactId, contactName) in addressbook.get_contacts()
198                 )
199                 sortedContacts = self.__sort_contacts(contacts)
200                 return sortedContacts
201
202         def get_contact_details(self, contactId):
203                 """
204                 @returns Iterable of (Phone Type, Phone Number)
205                 """
206                 bookIndex, originalId = contactId.split("-", 1)
207                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
208
209         @staticmethod
210         def null_sorter(contacts):
211                 return contacts
212
213         @staticmethod
214         def basic_lastname_sorter(contacts):
215                 contactsWithKey = [
216                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
217                                 for (contactId, contactName) in contacts
218                 ]
219                 contactsWithKey.sort()
220                 return (contactData for (lastName, contactData) in contactsWithKey)
221
222
223 class PhoneTypeSelector(object):
224
225         def __init__(self, widgetTree, gcBackend):
226                 self._gcBackend = gcBackend
227                 self._widgetTree = widgetTree
228                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
229
230                 self._selectButton = self._widgetTree.get_widget("select_button")
231                 self._selectButton.connect("clicked", self._on_phonetype_select)
232
233                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
234                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
235
236                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
237                 self._typeviewselection = None
238
239                 typeview = self._widgetTree.get_widget("phonetypes")
240                 typeview.connect("row-activated", self._on_phonetype_select)
241                 typeview.set_model(self._typemodel)
242                 textrenderer = gtk.CellRendererText()
243
244                 # Add the column to the treeview
245                 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
246                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
247
248                 typeview.append_column(column)
249
250                 self._typeviewselection = typeview.get_selection()
251                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
252
253         def run(self, contactDetails):
254                 self._typemodel.clear()
255
256                 for phoneType, phoneNumber in contactDetails:
257                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
258
259                 userResponse = self._dialog.run()
260
261                 if userResponse == gtk.RESPONSE_OK:
262                         model, itr = self._typeviewselection.get_selected()
263                         if itr:
264                                 phoneNumber = self._typemodel.get_value(itr, 0)
265                 else:
266                         phoneNumber = ""
267
268                 self._typeviewselection.unselect_all()
269                 self._dialog.hide()
270                 return phoneNumber
271
272         def _on_phonetype_select(self, *args):
273                 self._dialog.response(gtk.RESPONSE_OK)
274
275         def _on_phonetype_cancel(self, *args):
276                 self._dialog.response(gtk.RESPONSE_CANCEL)
277
278
279 class Dialpad(object):
280
281         __pretty_app_name__ = "DialCentral"
282         __app_name__ = "dialcentral"
283         __version__ = "0.8.0"
284         __app_magic__ = 0xdeadbeef
285
286         _glade_files = [
287                 '/usr/lib/dialcentral/gc_dialer.glade',
288                 os.path.join(os.path.dirname(__file__), "gc_dialer.glade"),
289                 os.path.join(os.path.dirname(__file__), "../lib/gc_dialer.glade"),
290         ]
291
292         def __init__(self):
293                 self._phonenumber = ""
294                 self._prettynumber = ""
295                 self._areacode = "518"
296
297                 self._clipboard = gtk.clipboard_get()
298
299                 self._deviceIsOnline = True
300                 self._callbackList = None
301
302                 self._recenttime = 0.0
303                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
304                 self._recentviewselection = None
305
306                 self._contactstime = 0.0
307                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
308                 self._contactsviewselection = None
309
310                 self._clearall_id = None
311
312                 for path in Dialpad._glade_files:
313                         if os.path.isfile(path):
314                                 self._widgetTree = gtk.glade.XML(path)
315                                 break
316                 else:
317                         self.display_error_message("Cannot find gc_dialer.glade")
318                         gtk.main_quit()
319                         return
320
321                 #Get the buffer associated with the number display
322                 self._numberdisplay = self._widgetTree.get_widget("numberdisplay")
323                 self.set_number("")
324                 self._notebook = self._widgetTree.get_widget("notebook")
325
326                 self._window = self._widgetTree.get_widget("Dialpad")
327
328                 global hildon
329                 self._app = None
330                 self._isFullScreen = False
331                 if hildon is not None and self._window is gtk.Window:
332                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
333                         hildon = None
334                 elif hildon is not None:
335                         self._app = hildon.Program()
336                         self._window = hildon.Window()
337                         self._widgetTree.get_widget("vbox1").reparent(self._window)
338                         self._app.add_window(self._window)
339                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
340                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
341                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
342                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
343                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
344
345                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
346                         menu = gtk.Menu()
347                         for child in gtkMenu.get_children():
348                                 child.reparent(menu)
349                         self._window.set_menu(menu)
350                         gtkMenu.destroy()
351
352                         self._window.connect("key-press-event", self._on_key_press)
353                         self._window.connect("window-state-event", self._on_window_state_change)
354                 else:
355                         warnings.warn("No Hildon", UserWarning, 2)
356
357                 if hildon is not None:
358                         self._window.set_title("Keypad")
359                 else:
360                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
361
362                 callbackMapping = {
363                         # Process signals from buttons
364                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
365                         "on_loginclose_clicked": self._on_loginclose_clicked,
366
367                         "on_dialpad_quit": self._on_close,
368                         "on_paste": self._on_paste,
369                         "on_clear_number": self._on_clear_number,
370
371                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
372                         "on_notebook_switch_page": self._on_notebook_switch_page,
373                         "on_recentview_row_activated": self._on_recentview_row_activated,
374                         "on_contactsview_row_activated" : self._on_contactsview_row_activated,
375
376                         "on_digit_clicked": self._on_digit_clicked,
377                         "on_back_clicked": self._on_backspace,
378                         "on_dial_clicked": self._on_dial_clicked,
379                         "on_back_pressed": self._on_back_pressed,
380                         "on_back_released": self._on_back_released,
381                         "on_addressbook_combo_changed": self._on_addressbook_combo_changed,
382                         "on_about_activate": self._on_about_activate,
383                 }
384                 self._widgetTree.signal_autoconnect(callbackMapping)
385                 self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
386
387                 if self._window:
388                         self._window.connect("destroy", gtk.main_quit)
389                         self._window.show_all()
390
391                 self.set_account_number("")
392                 self._widgetTree.get_widget("dial").grab_default()
393                 self._widgetTree.get_widget("dial").grab_focus()
394
395                 threading.Thread(target=self._idle_setup).start()
396
397
398         def _idle_setup(self):
399                 """
400                 If something can be done after the UI loads, push it here so it's not blocking the UI
401                 """
402
403                 import gc_backend
404                 import evo_backend
405                 import gmail_backend
406                 import maemo_backend
407
408                 self._gcBackend = gc_backend.GCDialer()
409
410                 try:
411                         import osso
412                 except ImportError:
413                         osso = None
414
415                 try:
416                         import conic
417                 except ImportError:
418                         conic = None
419
420
421                 self._osso = None
422                 if osso is not None:
423                         self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
424                         device = osso.DeviceState(self._osso)
425                         device.set_device_state_callback(self._on_device_state_change, 0)
426                 else:
427                         warnings.warn("No OSSO", UserWarning, 2)
428
429                 self._connection = None
430                 if conic is not None:
431                         self._connection = conic.Connection()
432                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
433                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
434                 else:
435                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
436
437
438                 addressBooks = [
439                         self._gcBackend,
440                         evo_backend.EvolutionAddressBook(),
441                         DummyAddressBook(),
442                 ]
443                 mergedBook = MergedAddressBook(addressBooks, MergedAddressBook.basic_lastname_sorter)
444                 self._addressBookFactories = list(addressBooks)
445                 self._addressBookFactories.insert(0, mergedBook)
446                 self._addressBook = None
447                 self.open_addressbook(*self.get_addressbooks().next()[0][0:2])
448
449                 gtk.gdk.threads_enter()
450                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
451                 gtk.gdk.threads_leave()
452
453                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
454                         if factoryName and bookName:
455                                 entryName = "%s: %s" % (factoryName, bookName)
456                         elif factoryName:
457                                 entryName = factoryName
458                         elif bookName:
459                                 entryName = bookName
460                         else:
461                                 entryName = "Bad name (%d)" % factoryId
462                         row = (str(factoryId), bookId, entryName)
463                         gtk.gdk.threads_enter()
464                         self._booksList.append(row)
465                         gtk.gdk.threads_leave()
466
467                 gtk.gdk.threads_enter()
468                 combobox = self._widgetTree.get_widget("addressbook_combo")
469                 combobox.set_model(self._booksList)
470                 cell = gtk.CellRendererText()
471                 combobox.pack_start(cell, True)
472                 combobox.add_attribute(cell, 'text', 2)
473                 combobox.set_active(0)
474                 gtk.gdk.threads_leave()
475
476                 self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend)
477
478                 gtk.gdk.threads_enter()
479                 self._init_recent_view()
480                 self._init_contacts_view()
481                 gtk.gdk.threads_leave()
482
483                 #This is where the blocking can start
484                 if self._gcBackend.is_authed():
485                         gtk.gdk.threads_enter()
486                         self.set_account_number(self._gcBackend.get_account_number())
487                         self.populate_callback_combo()
488                         gtk.gdk.threads_leave()
489                 else:
490                         self.attempt_login(2)
491
492                 return False
493
494         def _init_recent_view(self):
495                 recentview = self._widgetTree.get_widget("recentview")
496                 recentview.set_model(self._recentmodel)
497                 textrenderer = gtk.CellRendererText()
498
499                 # Add the column to the treeview
500                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
501                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
502
503                 recentview.append_column(column)
504
505                 self._recentviewselection = recentview.get_selection()
506                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
507
508                 return False
509
510         def _init_contacts_view(self):
511                 contactsview = self._widgetTree.get_widget("contactsview")
512                 contactsview.set_model(self._contactsmodel)
513
514                 # Add the column to the treeview
515                 column = gtk.TreeViewColumn("Contact")
516
517                 #displayContactSource = False
518                 displayContactSource = True
519                 if displayContactSource:
520                         textrenderer = gtk.CellRendererText()
521                         column.pack_start(textrenderer, expand=False)
522                         column.add_attribute(textrenderer, 'text', 0)
523
524                 textrenderer = gtk.CellRendererText()
525                 column.pack_start(textrenderer, expand=True)
526                 column.add_attribute(textrenderer, 'text', 1)
527
528                 textrenderer = gtk.CellRendererText()
529                 column.pack_start(textrenderer, expand=True)
530                 column.add_attribute(textrenderer, 'text', 4)
531
532                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
533                 column.set_sort_column_id(1)
534                 column.set_visible(True)
535                 contactsview.append_column(column)
536
537                 #textrenderer = gtk.CellRendererText()
538                 #column = gtk.TreeViewColumn("Location", textrenderer, text=2)
539                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
540                 #column.set_sort_column_id(2)
541                 #column.set_visible(True)
542                 #contactsview.append_column(column)
543
544                 #textrenderer = gtk.CellRendererText()
545                 #column = gtk.TreeViewColumn("Phone", textrenderer, text=3)
546                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
547                 #column.set_sort_column_id(3)
548                 #column.set_visible(True)
549                 #contactsview.append_column(column)
550
551                 self._contactsviewselection = contactsview.get_selection()
552                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
553
554                 return False
555
556         def populate_callback_combo(self):
557                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
558                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
559                         self._callbackList.append((make_pretty(number),))
560
561                 combobox = self._widgetTree.get_widget("callbackcombo")
562                 combobox.set_model(self._callbackList)
563                 combobox.set_text_column(0)
564
565                 combobox.get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
566
567         def _idly_populate_recentview(self):
568                 self._recenttime = time.time()
569                 self._recentmodel.clear()
570
571                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
572                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
573                         item = (phoneNumber, description)
574                         gtk.gdk.threads_enter()
575                         self._recentmodel.append(item)
576                         gtk.gdk.threads_leave()
577
578                 return False
579
580         def _idly_populate_contactsview(self):
581                 #@todo Add a lock so only one code path can be in here at a time
582                 self._contactstime = time.time()
583                 self._contactsmodel.clear()
584
585                 # completely disable updating the treeview while we populate the data
586                 contactsview = self._widgetTree.get_widget("contactsview")
587                 contactsview.freeze_child_notify()
588                 contactsview.set_model(None)
589
590                 addressBook = self._addressBook
591                 for contactId, contactName in addressBook.get_contacts():
592                         contactType = (addressBook.contact_source_short_name(contactId),)
593                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("",))
594
595                 # restart the treeview data rendering
596                 contactsview.set_model(self._contactsmodel)
597                 contactsview.thaw_child_notify()
598                 return False
599
600         def attempt_login(self, numOfAttempts = 1):
601                 """
602                 @note Not meant to be called directly, but run as a seperate thread.
603                 """
604                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
605
606                 if not self._deviceIsOnline:
607                         warnings.warn("Attempted to login while device was offline", UserWarning, 2)
608                         return False
609
610                 if self._gcBackend.is_authed():
611                         return True
612
613                 for x in xrange(numOfAttempts):
614                         gtk.gdk.threads_enter()
615
616                         dialog = self._widgetTree.get_widget("login_dialog")
617                         dialog.set_transient_for(self._window)
618                         dialog.set_default_response(0)
619                         dialog.run()
620
621                         username = self._widgetTree.get_widget("usernameentry").get_text()
622                         password = self._widgetTree.get_widget("passwordentry").get_text()
623                         self._widgetTree.get_widget("passwordentry").set_text("")
624                         dialog.hide()
625                         gtk.gdk.threads_leave()
626                         loggedIn = self._gcBackend.login(username, password)
627                         if loggedIn:
628                                 gtk.gdk.threads_enter()
629                                 if self._gcBackend.get_callback_number() is None:
630                                         self._gcBackend.set_sane_callback()
631                                 self.populate_callback_combo()
632                                 self.set_account_number(self._gcBackend.get_account_number())
633                                 gtk.gdk.threads_leave()
634                                 return True
635
636                 return False
637
638         def display_error_message(self, msg):
639                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
640
641                 def close(dialog, response, editor):
642                         editor.about_dialog = None
643                         dialog.destroy()
644                 error_dialog.connect("response", close, self)
645                 error_dialog.run()
646
647         def get_addressbooks(self):
648                 """
649                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
650                 """
651                 for i, factory in enumerate(self._addressBookFactories):
652                         for bookFactory, bookId, bookName in factory.get_addressbooks():
653                                 yield (i, bookId), (factory.factory_name(), bookName)
654
655         def open_addressbook(self, bookFactoryId, bookId):
656                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
657                 self._contactstime = 0
658                 threading.Thread(target=self._idly_populate_contactsview).start()
659
660         def set_number(self, number):
661                 """
662                 Set the callback phonenumber
663                 """
664                 self._phonenumber = make_ugly(number)
665                 self._prettynumber = make_pretty(self._phonenumber)
666                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
667
668         def set_account_number(self, number):
669                 """
670                 Displays current account number
671                 """
672                 self._widgetTree.get_widget("gcnumber_display").set_label("<span size='23000' weight='bold'>%s</span>" % (number))
673
674         @staticmethod
675         def _on_close(*args, **kwds):
676                 gtk.main_quit()
677
678         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
679                 """
680                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
681                 For system_inactivity, we have no background tasks to pause
682
683                 @note Hildon specific
684                 """
685                 if memory_low:
686                         self._gcBackend.clear_caches()
687                         gc.collect()
688
689         def _on_connection_change(self, connection, event, magicIdentifier):
690                 """
691                 @note Hildon specific
692                 """
693                 import conic
694
695                 status = event.get_status()
696                 error = event.get_error()
697                 iap_id = event.get_iap_id()
698                 bearer = event.get_bearer_type()
699
700                 if status == conic.STATUS_CONNECTED:
701                         self._window.set_sensitive(True)
702                         self._deviceIsOnline = True
703                         threading.Thread(target=self.attempt_login, args=[2]).start()
704                 elif status == conic.STATUS_DISCONNECTED:
705                         self._window.set_sensitive(False)
706                         self._deviceIsOnline = False
707
708         def _on_window_state_change(self, widget, event, *args):
709                 """
710                 @note Hildon specific
711                 """
712                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
713                         self._isFullScreen = True
714                 else:
715                         self._isFullScreen = False
716
717         def _on_key_press(self, widget, event, *args):
718                 """
719                 @note Hildon specific
720                 """
721                 if event.keyval == gtk.keysyms.F6:
722                         if self._isFullScreen:
723                                 self._window.unfullscreen()
724                         else:
725                                 self._window.fullscreen()
726
727         def _on_loginbutton_clicked(self, *args):
728                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
729
730         def _on_loginclose_clicked(self, *args):
731                 self._on_close()
732                 sys.exit(0)
733
734         def _on_clearcookies_clicked(self, *args):
735                 self._gcBackend.logout()
736                 self._recenttime = 0.0
737                 self._contactstime = 0.0
738                 self._recentmodel.clear()
739                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
740                 self.set_account_number("")
741
742                 # re-run the inital grandcentral setup
743                 threading.Thread(target=self.attempt_login, args=[2]).start()
744                 #gobject.idle_add(self._idly_populate_callback_combo)
745
746         def _on_callbackentry_changed(self, *args):
747                 """
748                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
749                 """
750                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
751                 if not self._gcBackend.is_valid_syntax(text):
752                         warnings.warn("%s is not a valid callback number" % text, UserWarning, 2)
753                 elif text == self._gcBackend.get_callback_number():
754                         warnings.warn("Callback number already is %s" % self._gcBackend.get_callback_number(), UserWarning, 2)
755                 else:
756                         self._gcBackend.set_callback_number(text)
757
758         def _on_recentview_row_activated(self, treeview, path, view_column):
759                 model, itr = self._recentviewselection.get_selected()
760                 if not itr:
761                         return
762
763                 self.set_number(self._recentmodel.get_value(itr, 0))
764                 self._notebook.set_current_page(0)
765                 self._recentviewselection.unselect_all()
766
767         def _on_addressbook_combo_changed(self, *args, **kwds):
768                 combobox = self._widgetTree.get_widget("addressbook_combo")
769                 itr = combobox.get_active_iter()
770
771                 factoryId = int(self._booksList.get_value(itr, 0))
772                 bookId = self._booksList.get_value(itr, 1)
773                 self.open_addressbook(factoryId, bookId)
774
775         def _on_contactsview_row_activated(self, treeview, path, view_column):
776                 model, itr = self._contactsviewselection.get_selected()
777                 if not itr:
778                         return
779
780                 contactId = self._contactsmodel.get_value(itr, 3)
781                 contactDetails = self._addressBook.get_contact_details(contactId)
782                 contactDetails = [phoneNumber for phoneNumber in contactDetails]
783
784                 if len(contactDetails) == 0:
785                         phoneNumber = ""
786                 elif len(contactDetails) == 1:
787                         phoneNumber = contactDetails[0][1]
788                 else:
789                         phoneNumber = self._phoneTypeSelector.run(contactDetails)
790
791                 if 0 < len(phoneNumber):
792                         self.set_number(phoneNumber)
793                         self._notebook.set_current_page(0)
794
795                 self._contactsviewselection.unselect_all()
796
797         def _on_notebook_switch_page(self, notebook, page, page_num):
798                 if page_num == 1:
799                         if 300 < (time.time() - self._contactstime):
800                                 threading.Thread(target=self._idly_populate_contactsview).start()
801                 elif page_num == 3:
802                         if 300 < (time.time() - self._recenttime):
803                                 threading.Thread(target=self._idly_populate_recentview).start()
804                 #elif page_num == 2:
805                 #       self._callbackNeedsSetup::
806                 #               gobject.idle_add(self._idly_populate_callback_combo)
807
808                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
809                 if hildon is not None:
810                         self._window.set_title(tabTitle)
811                 else:
812                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
813
814         def _on_dial_clicked(self, widget):
815                 """
816                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
817                 """
818                 loggedIn = self._gcBackend.is_authed()
819                 if not loggedIn:
820                         return
821                         #loggedIn = self.attempt_login(2)
822
823                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
824                         self.display_error_message("Backend link with grandcentral is not working, please try again")
825                         warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2)
826                         return
827
828                 try:
829                         callSuccess = self._gcBackend.dial(self._phonenumber)
830                 except ValueError, e:
831                         self._gcBackend._msg = e.message
832                         callSuccess = False
833
834                 if not callSuccess:
835                         self.display_error_message(self._gcBackend._msg)
836                 else:
837                         self.set_number("")
838
839                 self._recentmodel.clear()
840                 self._recenttime = 0.0
841
842         def _on_paste(self, *args):
843                 contents = self._clipboard.wait_for_text()
844                 phoneNumber = make_ugly(contents)
845                 self.set_number(phoneNumber)
846
847         def _on_clear_number(self, *args):
848                 self.set_number("")
849
850         def _on_digit_clicked(self, widget):
851                 self.set_number(self._phonenumber + widget.get_name()[-1])
852
853         def _on_backspace(self, widget):
854                 self.set_number(self._phonenumber[:-1])
855
856         def _on_clearall(self):
857                 self.set_number("")
858                 return False
859
860         def _on_back_pressed(self, widget):
861                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
862
863         def _on_back_released(self, widget):
864                 if self._clearall_id is not None:
865                         gobject.source_remove(self._clearall_id)
866                 self._clearall_id = None
867
868         def _on_about_activate(self, *args):
869                 dlg = gtk.AboutDialog()
870                 dlg.set_name(self.__pretty_app_name__)
871                 dlg.set_version(self.__version__)
872                 dlg.set_copyright("Copyright 2008 - LGPL")
873                 dlg.set_comments("Dialer is designed to interface with your Google Grandcentral account.  This application is not affiliated with Google or Grandcentral in any way")
874                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
875                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
876                 dlg.run()
877                 dlg.destroy()
878
879
880 def run_doctest():
881         import doctest
882
883         failureCount, testCount = doctest.testmod()
884         if not failureCount:
885                 print "Tests Successful"
886                 sys.exit(0)
887         else:
888                 sys.exit(1)
889
890
891 def run_dialpad():
892         gtk.gdk.threads_init()
893         if hildon is not None:
894                 gtk.set_application_name(Dialpad.__pretty_app_name__)
895         title = 'Dialpad'
896         handle = Dialpad()
897         gtk.main()
898
899
900 class DummyOptions(object):
901
902         def __init__(self):
903                 self.test = False
904
905
906 if __name__ == "__main__":
907         if len(sys.argv) > 1:
908                 try:
909                         import optparse
910                 except ImportError:
911                         optparse = None
912
913                 if optparse is not None:
914                         parser = optparse.OptionParser()
915                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
916                         (commandOptions, commandArgs) = parser.parse_args()
917         else:
918                 commandOptions = DummyOptions()
919                 commandArgs = []
920
921         if commandOptions.test:
922                 run_doctest()
923         else:
924                 run_dialpad()