Cleaning up in prep for release
[watersofshiloah] / src / windows / _base.py
1 from __future__ import with_statement
2
3 import ConfigParser
4 import logging
5
6 import gobject
7 import gtk
8
9 import constants
10 import hildonize
11 import util.misc as misc_utils
12 import util.go_utils as go_utils
13
14 import stream_index
15 import banners
16 import presenter
17
18
19 _moduleLogger = logging.getLogger(__name__)
20
21
22 class BasicWindow(gobject.GObject, go_utils.AutoSignal):
23
24         __gsignals__ = {
25                 'quit' : (
26                         gobject.SIGNAL_RUN_LAST,
27                         gobject.TYPE_NONE,
28                         (),
29                 ),
30                 'home' : (
31                         gobject.SIGNAL_RUN_LAST,
32                         gobject.TYPE_NONE,
33                         (),
34                 ),
35                 'jump-to' : (
36                         gobject.SIGNAL_RUN_LAST,
37                         gobject.TYPE_NONE,
38                         (gobject.TYPE_PYOBJECT, ),
39                 ),
40                 'rotate' : (
41                         gobject.SIGNAL_RUN_LAST,
42                         gobject.TYPE_NONE,
43                         (gobject.TYPE_BOOLEAN, ),
44                 ),
45                 'fullscreen' : (
46                         gobject.SIGNAL_RUN_LAST,
47                         gobject.TYPE_NONE,
48                         (gobject.TYPE_BOOLEAN, ),
49                 ),
50         }
51
52         def __init__(self, app, player, store):
53                 gobject.GObject.__init__(self)
54                 self._isDestroyed = False
55
56                 self._app = app
57                 self._player = player
58                 self._store = store
59
60                 self._clipboard = gtk.clipboard_get()
61                 self._windowInFullscreen = False
62
63                 self._errorBanner = banners.StackingBanner()
64
65                 self._layout = gtk.VBox()
66
67                 self._window = gtk.Window()
68                 self._window.add(self._layout)
69                 self._window = hildonize.hildonize_window(self._app, self._window)
70                 go_utils.AutoSignal.__init__(self, self.window)
71
72                 self._window.set_icon(self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["icon"]))
73                 self._window.connect("key-press-event", self._on_key_press)
74                 self._window.connect("window-state-event", self._on_window_state_change)
75                 self._window.connect("destroy", self._on_destroy)
76
77                 if hildonize.GTK_MENU_USED:
78                         aboutMenuItem = gtk.MenuItem("About")
79                         aboutMenuItem.connect("activate", self._on_about)
80
81                         helpMenu = gtk.Menu()
82                         helpMenu.append(aboutMenuItem)
83
84                         helpMenuItem = gtk.MenuItem("Help")
85                         helpMenuItem.set_submenu(helpMenu)
86
87                         menuBar = gtk.MenuBar()
88                         menuBar.append(helpMenuItem)
89
90                         self._layout.pack_start(menuBar, False, False)
91                 else:
92                         aboutMenuItem = gtk.Button("About")
93                         aboutMenuItem.connect("clicked", self._on_about)
94
95                         appMenu = hildonize.hildon.AppMenu()
96                         appMenu.append(aboutMenuItem)
97                         appMenu.show_all()
98                         self._window.set_app_menu(appMenu)
99
100                 self._layout.pack_start(self._errorBanner.toplevel, False, True)
101
102         @property
103         def window(self):
104                 return self._window
105
106         def show(self):
107                 hildonize.window_to_portrait(self._window)
108                 self._window.show_all()
109
110         def save_settings(self, config, sectionName):
111                 config.add_section(sectionName)
112                 config.set(sectionName, "fullscreen", str(self._windowInFullscreen))
113
114         def load_settings(self, config, sectionName):
115                 try:
116                         windowInFullscreen = config.getboolean(sectionName, "fullscreen")
117                 except ConfigParser.NoSectionError, e:
118                         _moduleLogger.info(
119                                 "Settings file %s is missing section %s" % (
120                                         constants._user_settings_,
121                                         e.section,
122                                 )
123                         )
124                         windowInFullscreen = self._windowInFullscreen
125
126                 if windowInFullscreen:
127                         self._window.fullscreen()
128                 else:
129                         self._window.unfullscreen()
130
131         def jump_to(self, node):
132                 raise NotImplementedError("On %s" % self)
133
134         @misc_utils.log_exception(_moduleLogger)
135         def _on_about(self, *args):
136                 show_about()
137
138         @misc_utils.log_exception(_moduleLogger)
139         def _on_destroy(self, *args):
140                 self._isDestroyed = True
141
142         @misc_utils.log_exception(_moduleLogger)
143         def _on_window_state_change(self, widget, event, *args):
144                 oldIsFull = self._windowInFullscreen
145                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
146                         self._windowInFullscreen = True
147                 else:
148                         self._windowInFullscreen = False
149                 if oldIsFull != self._windowInFullscreen:
150                         _moduleLogger.info("%r Emit fullscreen %s" % (self, self._windowInFullscreen))
151                         self.emit("fullscreen", self._windowInFullscreen)
152
153         @misc_utils.log_exception(_moduleLogger)
154         def _on_key_press(self, widget, event, *args):
155                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
156                 isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK)
157                 if (
158                         event.keyval == gtk.keysyms.F6 or
159                         event.keyval in RETURN_TYPES and isCtrl
160                 ):
161                         # The "Full screen" hardware key has been pressed
162                         if self._windowInFullscreen:
163                                 self._window.unfullscreen ()
164                         else:
165                                 self._window.fullscreen ()
166                         return True
167                 elif (
168                         event.keyval in (gtk.keysyms.w, ) and
169                         event.get_state() & gtk.gdk.CONTROL_MASK
170                 ):
171                         self._window.destroy()
172                 elif (
173                         event.keyval in (gtk.keysyms.q, ) and
174                         event.get_state() & gtk.gdk.CONTROL_MASK
175                 ):
176                         self.emit("quit")
177                         self._window.destroy()
178                 elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
179                         with open(constants._user_logpath_, "r") as f:
180                                 logLines = f.xreadlines()
181                                 log = "".join(logLines)
182                                 self._clipboard.set_text(str(log))
183                         return True
184
185         @misc_utils.log_exception(_moduleLogger)
186         def _on_home(self, *args):
187                 self.emit("home")
188                 self._window.destroy()
189
190         @misc_utils.log_exception(_moduleLogger)
191         def _on_child_fullscreen(self, source, isFull):
192                 if isFull:
193                         _moduleLogger.info("Full screen %r to mirror child %r" % (self, source))
194                         self._window.fullscreen()
195                 else:
196                         _moduleLogger.info("Unfull screen %r to mirror child %r" % (self, source))
197                         self._window.unfullscreen()
198
199         @misc_utils.log_exception(_moduleLogger)
200         def _on_jump(self, source, node):
201                 raise NotImplementedError("On %s" % self)
202
203         @misc_utils.log_exception(_moduleLogger)
204         def _on_quit(self, *args):
205                 self.emit("quit")
206                 self._window.destroy()
207
208
209 class ListWindow(BasicWindow):
210
211         def __init__(self, app, player, store, node):
212                 BasicWindow.__init__(self, app, player, store)
213                 self._node = node
214
215                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
216
217                 self._loadingBanner = banners.GenericBanner()
218
219                 modelTypes, columns = zip(*self._get_columns())
220
221                 self._model = gtk.ListStore(*modelTypes)
222
223                 self._treeView = gtk.TreeView()
224                 self._treeView.connect("row-activated", self._on_row_activated)
225                 self._treeView.set_property("fixed-height-mode", True)
226                 self._treeView.set_headers_visible(False)
227                 self._treeView.set_model(self._model)
228                 for column in columns:
229                         if column is not None:
230                                 self._treeView.append_column(column)
231
232                 self._viewport = gtk.Viewport()
233                 self._viewport.add(self._treeView)
234
235                 self._treeScroller = gtk.ScrolledWindow()
236                 self._treeScroller.add(self._viewport)
237                 self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
238                 self._treeScroller = hildonize.hildonize_scrollwindow(self._treeScroller)
239
240                 self._separator = gtk.HSeparator()
241                 self._presenter = presenter.NavControl(self._player, self._store)
242                 self.connect_auto(self._presenter, "home", self._on_home)
243                 self.connect_auto(self._presenter, "jump-to", self._on_jump)
244
245                 self._contentLayout = gtk.VBox(False)
246                 self._contentLayout.pack_start(self._treeScroller, True, True)
247                 self._contentLayout.pack_start(self._separator, False, True)
248                 self._contentLayout.pack_start(self._presenter.toplevel, False, True)
249
250                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
251                 self._layout.pack_start(self._contentLayout, True, True)
252
253         def show(self):
254                 BasicWindow.show(self)
255
256                 self._errorBanner.toplevel.hide()
257                 self._loadingBanner.toplevel.hide()
258
259                 self._refresh()
260                 self._presenter.refresh()
261
262         @classmethod
263         def _get_columns(cls):
264                 raise NotImplementedError("")
265
266         def _get_current_row(self):
267                 if self._player.node is None:
268                         return -1
269                 ancestors, current, descendants = stream_index.common_paths(self._player.node, self._node)
270                 if not descendants:
271                         return -1
272                 activeChild = descendants[0]
273                 for i, row in enumerate(self._model):
274                         if activeChild is row[0]:
275                                 return i
276                 else:
277                         return -1
278
279         def jump_to(self, node):
280                 ancestors, current, descendants = stream_index.common_paths(node, self._node)
281                 if current is None:
282                         raise RuntimeError("Cannot jump to node %s" % node)
283                 if not descendants:
284                         _moduleLogger.info("Current node is the target")
285                         return
286                 child = descendants[0]
287                 window = self._window_from_node(child)
288                 window.jump_to(node)
289
290         def _window_from_node(self, node):
291                 raise NotImplementedError("")
292
293         @misc_utils.log_exception(_moduleLogger)
294         def _on_row_activated(self, view, path, column):
295                 itr = self._model.get_iter(path)
296                 node = self._model.get_value(itr, 0)
297                 self._window_from_node(node)
298
299         @misc_utils.log_exception(_moduleLogger)
300         def _on_player_title_change(self, player, node):
301                 assert not self._isDestroyed
302                 self._select_row()
303
304         @misc_utils.log_exception(_moduleLogger)
305         def _on_jump(self, source, node):
306                 ancestors, current, descendants = stream_index.common_paths(node, self._node)
307                 if current is None:
308                         _moduleLogger.info("%s is not the target, moving up" % self._node)
309                         self.emit("jump-to", node)
310                         self._window.destroy()
311                         return
312                 if not descendants:
313                         _moduleLogger.info("Current node is the target")
314                         return
315                 child = descendants[0]
316                 window = self._window_from_node(child)
317                 window.jump_to(node)
318
319         @misc_utils.log_exception(_moduleLogger)
320         def _on_delay_scroll(self, *args):
321                 self._scroll_to_row()
322
323         def _show_loading(self):
324                 animationPath = self._store.STORE_LOOKUP["loading"]
325                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
326                 self._loadingBanner.show(animation, "Loading...")
327
328         def _hide_loading(self):
329                 self._loadingBanner.hide()
330
331         def _refresh(self):
332                 self._show_loading()
333                 self._model.clear()
334
335         def _select_row(self):
336                 rowIndex = self._get_current_row()
337                 if rowIndex < 0:
338                         return
339                 path = (rowIndex, )
340                 self._treeView.get_selection().select_path(path)
341
342         def _scroll_to_row(self):
343                 rowIndex = self._get_current_row()
344                 if rowIndex < 0:
345                         return
346
347                 path = (rowIndex, )
348                 self._treeView.scroll_to_cell(path)
349
350                 treeViewHeight = self._treeView.get_allocation().height
351                 viewportHeight = self._viewport.get_allocation().height
352
353                 viewsPerPort = treeViewHeight / float(viewportHeight)
354                 maxRows = len(self._model)
355                 percentThrough = rowIndex / float(maxRows)
356                 dxByIndex = int(viewsPerPort * percentThrough * viewportHeight)
357
358                 dxMax = max(treeViewHeight - viewportHeight, 0)
359
360                 dx = min(dxByIndex, dxMax)
361                 adjustment = self._treeScroller.get_vadjustment()
362                 adjustment.value = dx
363
364
365 class PresenterWindow(BasicWindow):
366
367         def __init__(self, app, player, store, node):
368                 BasicWindow.__init__(self, app, player, store)
369                 self._node = node
370                 self._playerNode = self._player.node
371                 self._nextSearch = None
372                 self._updateSeek = None
373
374                 self.connect_auto(self._player, "state-change", self._on_player_state_change)
375                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
376                 self.connect_auto(self._player, "error", self._on_player_error)
377
378                 self._loadingBanner = banners.GenericBanner()
379
380                 self._presenter = presenter.StreamPresenter(self._store)
381                 self._presenter.set_context(
382                         self._get_background(),
383                         self._node.title,
384                         self._node.subtitle,
385                 )
386                 self._presenterNavigation = presenter.NavigationBox()
387                 self._presenterNavigation.toplevel.add(self._presenter.toplevel)
388                 self.connect_auto(self._presenterNavigation, "action", self._on_nav_action)
389                 self.connect_auto(self._presenterNavigation, "navigating", self._on_navigating)
390
391                 self._seekbar = hildonize.create_seekbar()
392                 self._seekbar.connect("change-value", self._on_user_seek)
393
394                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
395                 self._layout.pack_start(self._presenterNavigation.toplevel, True, True)
396                 self._layout.pack_start(self._seekbar, False, False)
397
398                 self._window.set_title(self._node.get_parent().title)
399
400         def _get_background(self):
401                 raise NotImplementedError()
402
403         def show(self):
404                 BasicWindow.show(self)
405                 self._window.show_all()
406                 self._errorBanner.toplevel.hide()
407                 self._loadingBanner.toplevel.hide()
408                 self._set_context(self._player.state)
409                 self._seekbar.hide()
410
411         def jump_to(self, node):
412                 assert self._node is node
413
414         @property
415         def _active(self):
416                 return self._playerNode is self._node
417
418         def _show_loading(self):
419                 animationPath = self._store.STORE_LOOKUP["loading"]
420                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
421                 self._loadingBanner.show(animation, "Loading...")
422
423         def _hide_loading(self):
424                 self._loadingBanner.hide()
425
426         def _set_context(self, state):
427                 if state == self._player.STATE_PLAY:
428                         if self._active:
429                                 self._presenter.set_state(self._store.STORE_LOOKUP["pause"])
430                         else:
431                                 self._presenter.set_state(self._store.STORE_LOOKUP["play"])
432                 elif state == self._player.STATE_PAUSE:
433                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
434                 elif state == self._player.STATE_STOP:
435                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
436                 else:
437                         _moduleLogger.info("Unhandled player state %s" % state)
438
439         @misc_utils.log_exception(_moduleLogger)
440         def _on_user_seek(self, widget, scroll, value):
441                 self._player.seek(value / 100.0)
442
443         @misc_utils.log_exception(_moduleLogger)
444         def _on_player_update_seek(self):
445                 if self._isDestroyed:
446                         return False
447                 self._seekbar.set_value(self._player.percent_elapsed * 100)
448                 return True
449
450         @misc_utils.log_exception(_moduleLogger)
451         def _on_player_state_change(self, player, newState):
452                 assert not self._isDestroyed
453                 if self._active and self._player.state == self._player.STATE_PLAY:
454                         self._seekbar.show()
455                         assert self._updateSeek is None
456                         self._updateSeek = go_utils.Timeout(self._on_player_update_seek, once=False)
457                         self._updateSeek.start(seconds=1)
458                 else:
459                         self._seekbar.hide()
460                         if self._updateSeek is not None:
461                                 self._updateSeek.cancel()
462                                 self._updateSeek = None
463
464                 if not self._presenterNavigation.is_active():
465                         self._set_context(newState)
466
467         @misc_utils.log_exception(_moduleLogger)
468         def _on_player_title_change(self, player, node):
469                 assert not self._isDestroyed
470                 if not self._active or node in [None, self._node]:
471                         self._playerNode = node
472                         return
473                 self._playerNode = node
474                 self.emit("jump-to", node)
475                 self._window.destroy()
476
477         @misc_utils.log_exception(_moduleLogger)
478         def _on_player_error(self, player, err, debug):
479                 assert not self._isDestroyed
480                 _moduleLogger.error("%r - %r" % (err, debug))
481
482         @misc_utils.log_exception(_moduleLogger)
483         def _on_navigating(self, widget, navState):
484                 if navState == "clicking":
485                         if self._player.state == self._player.STATE_PLAY:
486                                 if self._active:
487                                         imageName = "pause_pressed"
488                                 else:
489                                         imageName = "play_pressed"
490                         elif self._player.state == self._player.STATE_PAUSE:
491                                 imageName = "play_pressed"
492                         elif self._player.state == self._player.STATE_STOP:
493                                 imageName = "play_pressed"
494                         else:
495                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
496                 elif navState == "down":
497                         imageName = "home"
498                 elif navState == "up":
499                         if self._player.state == self._player.STATE_PLAY:
500                                 if self._active:
501                                         imageName = "pause"
502                                 else:
503                                         imageName = "play"
504                         elif self._player.state == self._player.STATE_PAUSE:
505                                 imageName = "play"
506                         elif self._player.state == self._player.STATE_STOP:
507                                 imageName = "play"
508                         else:
509                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
510                 elif navState == "left":
511                         imageName = "next"
512                 elif navState == "right":
513                         imageName = "prev"
514
515                 self._presenter.set_state(self._store.STORE_LOOKUP[imageName])
516
517         @misc_utils.log_exception(_moduleLogger)
518         def _on_nav_action(self, widget, navState):
519                 self._set_context(self._player.state)
520
521                 if navState == "clicking":
522                         if self._player.state == self._player.STATE_PLAY:
523                                 if self._active:
524                                         self._player.pause()
525                                 else:
526                                         self._player.set_piece_by_node(self._node)
527                                         self._player.play()
528                         elif self._player.state == self._player.STATE_PAUSE:
529                                 self._player.play()
530                         elif self._player.state == self._player.STATE_STOP:
531                                 self._player.set_piece_by_node(self._node)
532                                 self._player.play()
533                         else:
534                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
535                 elif navState == "down":
536                         self.emit("home")
537                         self._window.destroy()
538                 elif navState == "up":
539                         pass
540                 elif navState == "left":
541                         if self._active:
542                                 self._player.next()
543                         else:
544                                 assert self._nextSearch is None
545                                 self._nextSearch = stream_index.AsyncWalker(stream_index.get_next)
546                                 self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error)
547                 elif navState == "right":
548                         if self._active:
549                                 self._player.back()
550                         else:
551                                 assert self._nextSearch is None
552                                 self._nextSearch = stream_index.AsyncWalker(stream_index.get_previous)
553                                 self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error)
554
555         @misc_utils.log_exception(_moduleLogger)
556         def _on_next_node(self, node):
557                 self._nextSearch = None
558                 self.emit("jump-to", node)
559                 self._window.destroy()
560
561         @misc_utils.log_exception(_moduleLogger)
562         def _on_node_search_error(self, e):
563                 self._nextSearch = None
564                 self._errorBanner.push_message(str(e))
565
566
567 def show_about():
568         # @todo Turn this into a full-fledge window to keep it rotated
569         dialog = gtk.AboutDialog()
570         dialog.set_position(gtk.WIN_POS_CENTER)
571         dialog.set_name(constants.__pretty_app_name__)
572         dialog.set_version(constants.__version__)
573         dialog.set_copyright("(c) 2010 Intellectual Reserve, Inc. All rights reserved.")
574         dialog.set_website("http://www.lds.org")
575         comments = "Mormon Radio and Audiobook Player"
576         dialog.set_comments(comments)
577         dialog.set_authors(["The Church of Jesus Christ of Latter-day Saints"])
578         dialog.run()
579         dialog.destroy()