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