Switching from file-based book settings to json based, making it much easier to add...
[nqaap] / src / opt / Nqa-Audiobook-player / Gui.py
1 from __future__ import with_statement
2
3 import os
4 import ConfigParser
5 import logging
6
7 import gobject
8 import gtk
9
10 import constants
11 import hildonize
12 import gtk_toolbox
13 import Browser
14 import CallMonitor
15 import settings
16
17 if hildonize.IS_FREMANTLE_SUPPORTED:
18     # I don't normally do this but I want to error as loudly as possibly when an issue arises
19     import hildon
20
21
22 _moduleLogger = logging.getLogger(__name__)
23
24
25 class Gui(object):
26
27     def __init__(self):
28         _moduleLogger.info("Starting GUI")
29         self._clipboard = gtk.clipboard_get()
30         self._callMonitor = CallMonitor.CallMonitor()
31         self.__settingsWindow = None
32         self.__settingsManager = None
33         self._bookSelection = []
34         self._bookSelectionIndex = -1
35         self._chapterSelection = []
36         self._chapterSelectionIndex = -1
37         self._sleepSelection = ["0", "1", "10", "20", "30", "60"]
38         self._sleepSelectionIndex = 0
39
40         self.__window_in_fullscreen = False #The window isn't in full screen mode initially.
41         self.__isPortrait = False
42
43         self.controller = None
44         self.sleep_timer = None
45
46         # set up gui
47         self.setup()
48         self._callMonitor.connect("call_start", self.__on_call_started)
49         self._callMonitor.start()
50
51     def setup(self):
52         self._app = hildonize.get_app_class()()
53         self.win = gtk.Window()
54         self.win = hildonize.hildonize_window(self._app, self.win)
55         self.win.set_title(constants.__pretty_app_name__)
56
57         # Cover image
58         self.cover = gtk.Image()
59
60         # Controls:
61
62         # Label that hold the title of the book,and maybe the chapter
63         self.title = gtk.Label()
64         self.title.set_justify(gtk.JUSTIFY_CENTER)
65         self._set_display_title("Select a book to start listening")
66
67         self.chapter = gtk.Label()
68         self.chapter.set_justify(gtk.JUSTIFY_CENTER)
69
70         # Seekbar 
71         if hildonize.IS_FREMANTLE_SUPPORTED:
72             self.seek = hildon.Seekbar()
73             self.seek.set_range(0.0, 100)
74             self.seek.set_draw_value(False)
75             self.seek.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
76             self.seek.connect('change-value', self.seek_changed) # event
77             # self.seek.connect('value-changed',self.seek_changed) # event
78         else:
79             adjustment = gtk.Adjustment(0, 0, 101, 1, 5, 1)
80             self.seek = gtk.HScale(adjustment)
81             self.seek.set_draw_value(False)
82             self.seek.connect('change-value', self.seek_changed) # event
83
84         # Pause button
85         if hildonize.IS_FREMANTLE_SUPPORTED:
86             self.backButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
87             image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PREVIOUS, gtk.HILDON_SIZE_FINGER_HEIGHT)
88             self.backButton.set_image(image)
89
90             self.button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
91
92             self.forwardButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
93             image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_NEXT, gtk.HILDON_SIZE_FINGER_HEIGHT)
94             self.forwardButton.set_image(image)
95         else:
96             self.backButton = gtk.Button(stock=gtk.STOCK_MEDIA_PREVIOUS)
97             self.button = gtk.Button()
98             self.forwardButton = gtk.Button(stock=gtk.STOCK_MEDIA_NEXT)
99         self.set_button_text("Play", "Start playing the audiobook")
100         self.backButton.connect('clicked', self._on_previous_chapter)
101         self.button.connect('clicked', self.play_pressed) # event
102         self.forwardButton.connect('clicked', self._on_next_chapter)
103
104         self._toolbar = gtk.HBox()
105         self._toolbar.pack_start(self.backButton, False, False, 0)
106         self._toolbar.pack_start(self.button, True, True, 0)
107         self._toolbar.pack_start(self.forwardButton, False, False, 0)
108
109         # Box to hold the controls:
110         self._controlLayout = gtk.VBox()
111         self._controlLayout.pack_start(gtk.Label(), True, True, 0)
112         self._controlLayout.pack_start(self.title, False, True, 0)
113         self._controlLayout.pack_start(self.chapter, False, True, 0)
114         self._controlLayout.pack_start(gtk.Label(), True, True, 0)
115         self._controlLayout.pack_start(self.seek, False, True, 0)
116         self._controlLayout.pack_start(self._toolbar, False, True, 0)
117
118         #Box that divides the layout in two: cover on the lefta
119         #and controls on the right
120         self._viewLayout = gtk.HBox()
121         self._viewLayout.pack_start(self.cover, True, True, 0)
122         self._viewLayout.add(self._controlLayout)
123
124         self._menuBar = gtk.MenuBar()
125         self._menuBar.show()
126
127         self._mainLayout = gtk.VBox()
128         self._mainLayout.pack_start(self._menuBar, False, False, 0)
129         self._mainLayout.pack_start(self._viewLayout)
130
131         # Add hbox to the window
132         self.win.add(self._mainLayout)
133
134         #Menu:
135         # Create menu
136         self._populate_menu()
137
138         self.win.connect("delete_event", self.quit) # Add shutdown event
139         self.win.connect("key-press-event", self.on_key_press)
140         self.win.connect("window-state-event", self._on_window_state_change)
141
142         self.win.show_all()
143
144         # Run update timer
145         self.setup_timers()
146
147     def _populate_menu(self):
148         self._menuBar = hildonize.hildonize_menu(
149             self.win,
150             self._menuBar,
151         )
152         if hildonize.IS_FREMANTLE_SUPPORTED:
153             # Create a picker button 
154             self.book_button = hildon.Button(gtk.HILDON_SIZE_AUTO,
155                                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
156             self.book_button.set_title("Audiobook") # Set a title to the button 
157             self.book_button.connect("clicked", self._on_select_audiobook)
158
159             # Create a picker button 
160             self.chapter_button = hildon.Button(gtk.HILDON_SIZE_AUTO,
161                                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
162             self.chapter_button.set_title("Chapter") # Set a title to the button 
163             self.chapter_button.connect("clicked", self._on_select_chapter)
164
165             # Create a picker button 
166             self.sleeptime_button = hildon.Button(gtk.HILDON_SIZE_AUTO,
167                                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
168             self.sleeptime_button.set_title("Sleeptimer") # Set a title to the button 
169             self.sleeptime_button.connect("clicked", self._on_select_sleep)
170
171             settings_button = hildon.Button(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_VERTICAL)
172             settings_button.set_label("Settings")
173             settings_button.connect("clicked", self._on_settings)
174
175             about_button = hildon.Button(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_VERTICAL)
176             about_button.set_label("About")
177             about_button.connect("clicked", self._on_about_activate)
178
179             help_button = hildon.Button(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_VERTICAL)
180             help_button.set_label("Help")
181             help_button.connect("clicked", self.get_help)
182
183             self._menuBar.append(self.book_button)        # Add the button to menu
184             self._menuBar.append(self.chapter_button)        # Add the button to menu
185             self._menuBar.append(self.sleeptime_button)        # Add the button to menu
186             self._menuBar.append(settings_button)
187             self._menuBar.append(help_button)
188             self._menuBar.append(about_button)
189             self._menuBar.show_all()
190         else:
191             self._audiobookMenuItem = gtk.MenuItem("Audiobook: ")
192             self._audiobookMenuItem.connect("activate", self._on_select_audiobook)
193
194             self._chapterMenuItem = gtk.MenuItem("Chapter: ")
195             self._chapterMenuItem.connect("activate", self._on_select_chapter)
196
197             self._sleepMenuItem = gtk.MenuItem("Sleeptimer: 0")
198             self._sleepMenuItem.connect("activate", self._on_select_sleep)
199
200             settingsMenuItem = gtk.MenuItem("Settings")
201             settingsMenuItem.connect("activate", self._on_settings)
202
203             aboutMenuItem = gtk.MenuItem("About")
204             aboutMenuItem.connect("activate", self._on_about_activate)
205
206             helpMenuItem = gtk.MenuItem("Help")
207             helpMenuItem.connect("activate", self.get_help)
208
209             booksMenu = gtk.Menu()
210             booksMenu.append(self._audiobookMenuItem)
211             booksMenu.append(self._chapterMenuItem)
212             booksMenu.append(self._sleepMenuItem)
213             booksMenu.append(settingsMenuItem)
214             booksMenu.append(helpMenuItem)
215             booksMenu.append(aboutMenuItem)
216
217             booksMenuItem = gtk.MenuItem("Books")
218             booksMenuItem.show()
219             booksMenuItem.set_submenu(booksMenu)
220             self._menuBar.append(booksMenuItem)
221             self._menuBar.show_all()
222
223     def setup_timers(self):
224         self.seek_timer = timeout_add_seconds(3, self.update_seek)
225
226     def save_settings(self):
227         config = ConfigParser.SafeConfigParser()
228         self._save_settings(config)
229         with open(constants._user_settings_, "wb") as configFile:
230             config.write(configFile)
231         self.controller.save()
232
233     def _save_settings(self, config):
234         config.add_section(constants.__pretty_app_name__)
235         config.set(constants.__pretty_app_name__, "portrait", str(self.__isPortrait))
236         config.set(constants.__pretty_app_name__, "fullscreen", str(self.__window_in_fullscreen))
237         config.set(constants.__pretty_app_name__, "audiopath", self.controller.get_books_path())
238
239     def load_settings(self):
240         config = ConfigParser.SafeConfigParser()
241         config.read(constants._user_settings_)
242         self._load_settings(config)
243
244     def _load_settings(self, config):
245         isPortrait = False
246         window_in_fullscreen = False
247         booksPath = constants._default_book_path_
248         try:
249             isPortrait = config.getboolean(constants.__pretty_app_name__, "portrait")
250             window_in_fullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
251             booksPath = config.get(constants.__pretty_app_name__, "audiopath")
252         except ConfigParser.NoSectionError, e:
253             _moduleLogger.info(
254                 "Settings file %s is missing section %s" % (
255                     constants._user_settings_,
256                     e.section,
257                 )
258             )
259
260         if isPortrait ^ self.__isPortrait:
261             if isPortrait:
262                 orientation = gtk.ORIENTATION_VERTICAL
263             else:
264                 orientation = gtk.ORIENTATION_HORIZONTAL
265             self.set_orientation(orientation)
266
267         self.__window_in_fullscreen = window_in_fullscreen
268         if self.__window_in_fullscreen:
269             self.win.fullscreen()
270         else:
271             self.win.unfullscreen()
272
273         self.controller.load(booksPath)
274
275     @staticmethod
276     def __format_name(path):
277         if os.path.isfile(path):
278             return os.path.basename(path).rsplit(".", 1)[0]
279         else:
280             return os.path.basename(path)
281
282     @gtk_toolbox.log_exception(_moduleLogger)
283     def _on_select_audiobook(self, *args):
284         if not self._bookSelection:
285             return
286         index = hildonize.touch_selector(
287             self.win,
288             "Audiobook",
289             (self.__format_name(bookPath) for bookPath in self._bookSelection),
290             self._bookSelectionIndex if 0 <= self._bookSelectionIndex else 0,
291         )
292         self._bookSelectionIndex = index
293         bookName = self._bookSelection[index]
294         self.controller.set_book(bookName)
295         self.set_button_text("Play", "Start playing the audiobook") # reset button
296
297     @gtk_toolbox.log_exception(_moduleLogger)
298     def _on_select_chapter(self, *args):
299         if not self._chapterSelection:
300             return
301         index = hildonize.touch_selector(
302             self.win,
303             "Chapter",
304             (self.__format_name(chapterPath) for chapterPath in self._chapterSelection),
305             self._chapterSelectionIndex if 0 <= self._chapterSelectionIndex else 0,
306         )
307         self._chapterSelectionIndex = index
308         chapterName = self._chapterSelection[index]
309         self.controller.set_chapter(chapterName)
310         self.set_button_text("Play", "Start playing the audiobook") # reset button
311
312     @gtk_toolbox.log_exception(_moduleLogger)
313     def _on_select_sleep(self, *args):
314         if self.sleep_timer is not None:
315             gobject.source_remove(self.sleep_timer)
316
317         try:
318             index = hildonize.touch_selector(
319                 self.win,
320                 "Sleeptimer",
321                 self._sleepSelection,
322                 self._sleepSelectionIndex if 0 <= self._sleepSelectionIndex else 0,
323             )
324         except RuntimeError:
325             _moduleLogger.exception("Handling as if user cancelled")
326             hildonize.show_information_banner(self.win, "Sleep timer canceled")
327             index = 0
328
329         self._sleepSelectionIndex = index
330         sleepName = self._sleepSelection[index]
331
332         time_out = int(sleepName)
333         if 0 < time_out:
334             timeout_add_seconds(time_out * 60, self.sleep)
335
336         if hildonize.IS_FREMANTLE_SUPPORTED:
337             self.sleeptime_button.set_text("Sleeptimer", sleepName)
338         else:
339             self._sleepMenuItem.get_child().set_text("Sleeptimer: %s" % (sleepName, ))
340
341     @gtk_toolbox.log_exception(_moduleLogger)
342     def __on_call_started(self, callMonitor):
343         self.pause()
344
345     @gtk_toolbox.log_exception(_moduleLogger)
346     def _on_settings(self, *args):
347         if self.__settingsWindow is None:
348             vbox = gtk.VBox()
349             self.__settingsManager = settings.SettingsDialog(vbox)
350
351             self.__settingsWindow = gtk.Window()
352             self.__settingsWindow.add(vbox)
353             self.__settingsWindow = hildonize.hildonize_window(self._app, self.__settingsWindow)
354             self.__settingsManager.window = self.__settingsWindow
355
356             self.__settingsWindow.set_title("Settings")
357             self.__settingsWindow.set_transient_for(self.win)
358             self.__settingsWindow.set_default_size(*self.win.get_size())
359             self.__settingsWindow.connect("delete-event", self._on_settings_delete)
360         self.__settingsManager.set_portrait_state(self.__isPortrait)
361         self.__settingsManager.set_audiobook_path(self.controller.get_books_path())
362         self.__settingsWindow.set_modal(True)
363         self.__settingsWindow.show_all()
364
365     @gtk_toolbox.log_exception(_moduleLogger)
366     def _on_settings_delete(self, *args):
367         self.__settingsWindow.emit_stop_by_name("delete-event")
368         self.__settingsWindow.hide()
369         self.__settingsWindow.set_modal(False)
370
371         isPortrait = self.__settingsManager.is_portrait()
372         if isPortrait ^ self.__isPortrait:
373             if isPortrait:
374                 orientation = gtk.ORIENTATION_VERTICAL
375             else:
376                 orientation = gtk.ORIENTATION_HORIZONTAL
377             self.set_orientation(orientation)
378         if self.__settingsManager.get_audiobook_path() != self.controller.get_books_path():
379             self.controller.reload(self.__settingsManager.get_audiobook_path())
380
381         return True
382
383     @gtk_toolbox.log_exception(_moduleLogger)
384     def update_seek(self):
385         #print self.controller.get_percentage()
386         if self.controller.is_playing():
387             gtk.gdk.threads_enter()
388             self.seek.set_value(self.controller.get_percentage() * 100)
389             gtk.gdk.threads_leave()
390         #self.controller.get_percentage() 
391         return True                     # run again
392
393     @gtk_toolbox.log_exception(_moduleLogger)
394     def sleep(self):
395         _moduleLogger.info("sleep time timeout")
396         hildonize.show_information_banner(self.win, "Sleep timer")
397         self.controller.stop()
398         self.set_button_text("Resume", "Resume playing the audiobook")
399         return False                    # do not repeat
400
401     @gtk_toolbox.log_exception(_moduleLogger)
402     def get_help(self, button):
403         Browser.open("file:///opt/Nqa-Audiobook-player/Help/nqaap.html")
404
405     @gtk_toolbox.log_exception(_moduleLogger)
406     def seek_changed(self, seek, scroll , value):
407         # print "sok", scroll
408         self.controller.seek_percent(seek.get_value() / 100.0)
409
410     @gtk_toolbox.log_exception(_moduleLogger)
411     def _on_next_chapter(self, *args):
412         self.controller.next_chapter()
413
414     @gtk_toolbox.log_exception(_moduleLogger)
415     def _on_previous_chapter(self, *args):
416         self.controller.previous_chapter()
417
418     @gtk_toolbox.log_exception(_moduleLogger)
419     def play_pressed(self, button):
420         if self.controller.is_playing():
421             self.pause()
422         else:
423             self.play()
424
425     @gtk_toolbox.log_exception(_moduleLogger)
426     def on_key_press(self, widget, event, *args):
427         RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
428         isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK)
429         if (
430             event.keyval == gtk.keysyms.F6 or
431             event.keyval in RETURN_TYPES and isCtrl
432         ):
433             # The "Full screen" hardware key has been pressed 
434             if self.__window_in_fullscreen:
435                 self.win.unfullscreen ()
436             else:
437                 self.win.fullscreen ()
438             return True
439         elif event.keyval == gtk.keysyms.o and isCtrl:
440             self._toggle_rotate()
441             return True
442         elif (
443             event.keyval in (gtk.keysyms.w, gtk.keysyms.q) and
444             event.get_state() & gtk.gdk.CONTROL_MASK
445         ):
446             self.quit()
447         elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
448             with open(constants._user_logpath_, "r") as f:
449                 logLines = f.xreadlines()
450                 log = "".join(logLines)
451                 self._clipboard.set_text(str(log))
452             return True
453         elif event.keyval in RETURN_TYPES:
454             if self.controller.is_playing():
455                 self.pause()
456             else:
457                 self.play()
458             return True
459         elif event.keyval == gtk.keysyms.Left:
460             self.controller.previous_chapter()
461             return True
462         elif event.keyval == gtk.keysyms.Right:
463             self.controller.next_chapter()
464             return True
465
466     @gtk_toolbox.log_exception(_moduleLogger)
467     def _on_window_state_change(self, widget, event, *args):
468         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
469             self.__window_in_fullscreen = True
470         else:
471             self.__window_in_fullscreen = False
472
473     @gtk_toolbox.log_exception(_moduleLogger)
474     def quit(self, *args):             # what are the arguments?
475         _moduleLogger.info("Shutting down")
476         try:
477             self.save_settings()
478             self.controller.stop()          # to save the state
479         finally:
480             gtk.main_quit()
481
482     @gtk_toolbox.log_exception(_moduleLogger)
483     def _on_about_activate(self, *args):
484         dlg = gtk.AboutDialog()
485         dlg.set_name(constants.__pretty_app_name__)
486         dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
487         dlg.set_copyright("Copyright 2010")
488         dlg.set_comments("")
489         dlg.set_website("http://nqaap.garage.maemo.org/")
490         dlg.set_authors(["Pengman <pengmeister@gmail.com>", "Ed Page <eopage@byu.net>"])
491         dlg.run()
492         dlg.destroy()
493
494     # Actions:  
495
496     def play(self):
497         self.set_button_text("Stop", "Stop playing the audiobook")
498         self.controller.play()
499
500     def pause(self):
501         self.set_button_text("Resume", "Resume playing the audiobook")
502         self.controller.stop()
503
504     def set_orientation(self, orientation):
505         if orientation == gtk.ORIENTATION_VERTICAL:
506             if self.__isPortrait:
507                 return
508             hildonize.window_to_portrait(self.win)
509             self.__isPortrait = True
510
511             self._viewLayout.remove(self._controlLayout)
512             self._mainLayout.add(self._controlLayout)
513         elif orientation == gtk.ORIENTATION_HORIZONTAL:
514             if not self.__isPortrait:
515                 return
516             hildonize.window_to_landscape(self.win)
517             self.__isPortrait = False
518
519             self._mainLayout.remove(self._controlLayout)
520             self._viewLayout.add(self._controlLayout)
521         else:
522             raise NotImplementedError(orientation)
523
524     def get_orientation(self):
525         return gtk.ORIENTATION_VERTICAL if self.__isPortrait else gtk.ORIENTATION_HORIZONTAL
526
527     def _toggle_rotate(self):
528         if self.__isPortrait:
529             self.set_orientation(gtk.ORIENTATION_HORIZONTAL)
530         else:
531             self.set_orientation(gtk.ORIENTATION_VERTICAL)
532
533     def change_chapter(self, chapterName):
534         if chapterName is None:
535             _moduleLogger.debug("chapter selection canceled.")
536             return True                   # this should end the function and indicate it has been handled
537
538         _moduleLogger.debug("chapter changed (by controller) to: %s" % chapterName)
539
540     def set_button_text(self, title, text):
541         if hildonize.IS_FREMANTLE_SUPPORTED:
542             self.button.set_text(title, text)
543         else:
544             self.button.set_label("%s" % (title, ))
545
546     def set_books(self, books):
547         _moduleLogger.debug("new books")
548         del self._bookSelection[:]
549         self._bookSelection.extend(books)
550         if len(books) == 0 and self.controller is not None:
551             hildonize.show_information_banner(self.win, "No audiobooks found. \nPlease place your audiobooks in the directory %s" % self.controller.get_books_path())
552
553     def set_book(self, bookPath, cover):
554         bookName = self.__format_name(bookPath)
555
556         self.set_button_text("Play", "Start playing the audiobook") # reset button
557         self._set_display_title(bookName)
558         if hildonize.IS_FREMANTLE_SUPPORTED:
559             self.book_button.set_text("Audiobook", bookName)
560         else:
561             self._audiobookMenuItem.get_child().set_text("Audiobook: %s" % (bookName, ))
562         if cover != "":
563             self.cover.set_from_file(cover)
564
565     def set_chapter(self, chapterIndex):
566         '''
567         Called from controller whenever a new chapter is started
568
569         chapter parameter is supposed to be the index for the chapter, not the name
570         '''
571         self.auto_chapter_selected = True
572         self._set_display_chapter(str(chapterIndex + 1))
573         if hildonize.IS_FREMANTLE_SUPPORTED:
574             self.chapter_button.set_text("Chapter", str(chapterIndex))
575         else:
576             self._chapterMenuItem.get_child().set_text("Chapter: %s" % (chapterIndex+1, ))
577
578     def set_chapters(self, chapters):
579         _moduleLogger.debug("setting chapters" )
580         del self._chapterSelection[:]
581         self._chapterSelection.extend(chapters)
582
583     def set_sleep_timer(self, mins):
584         pass
585
586     # Utils
587     def set_selected_value(self, button, value):
588         i = button.get_selector().get_model(0).index[value] # get index of value from list
589         button.set_active(i)                                # set active index to that index
590
591     def _set_display_title(self, title):
592         self.title.set_markup("<b><big>%s</big></b>" % title)
593
594     def _set_display_chapter(self, chapter):
595         self.chapter.set_markup("<b><big>Chapter %s</big></b>" % chapter)
596
597
598 def _old_timeout_add_seconds(timeout, callback):
599     return gobject.timeout_add(timeout * 1000, callback)
600
601
602 def _timeout_add_seconds(timeout, callback):
603     return gobject.timeout_add_seconds(timeout, callback)
604
605
606 try:
607     gobject.timeout_add_seconds
608     timeout_add_seconds = _timeout_add_seconds
609 except AttributeError:
610     timeout_add_seconds = _old_timeout_add_seconds
611
612
613 if __name__ == "__main__":
614     g = Gui(None)