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