1 from __future__ import with_statement
11 import util.misc as misc_utils
12 import util.go_utils as go_utils
19 _moduleLogger = logging.getLogger(__name__)
22 class BasicWindow(gobject.GObject, go_utils.AutoSignal):
26 gobject.SIGNAL_RUN_LAST,
31 gobject.SIGNAL_RUN_LAST,
36 gobject.SIGNAL_RUN_LAST,
38 (gobject.TYPE_PYOBJECT, ),
41 gobject.SIGNAL_RUN_LAST,
43 (gobject.TYPE_BOOLEAN, ),
46 gobject.SIGNAL_RUN_LAST,
48 (gobject.TYPE_BOOLEAN, ),
52 def __init__(self, app, player, store):
53 gobject.GObject.__init__(self)
54 self._isDestroyed = False
60 self._clipboard = gtk.clipboard_get()
61 self._windowInFullscreen = False
63 self._errorBanner = banners.StackingBanner()
65 self._layout = gtk.VBox()
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)
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)
77 if hildonize.GTK_MENU_USED:
78 aboutMenuItem = gtk.MenuItem("About")
79 aboutMenuItem.connect("activate", self._on_about)
82 helpMenu.append(aboutMenuItem)
84 helpMenuItem = gtk.MenuItem("Help")
85 helpMenuItem.set_submenu(helpMenu)
87 menuBar = gtk.MenuBar()
88 menuBar.append(helpMenuItem)
90 self._layout.pack_start(menuBar, False, False)
92 aboutMenuItem = gtk.Button("About")
93 aboutMenuItem.connect("clicked", self._on_about)
95 appMenu = hildonize.hildon.AppMenu()
96 appMenu.append(aboutMenuItem)
98 self._window.set_app_menu(appMenu)
100 self._layout.pack_start(self._errorBanner.toplevel, False, True)
107 hildonize.window_to_portrait(self._window)
108 self._window.show_all()
110 def save_settings(self, config, sectionName):
111 config.add_section(sectionName)
112 config.set(sectionName, "fullscreen", str(self._windowInFullscreen))
114 def load_settings(self, config, sectionName):
116 windowInFullscreen = config.getboolean(sectionName, "fullscreen")
117 except ConfigParser.NoSectionError, e:
119 "Settings file %s is missing section %s" % (
120 constants._user_settings_,
124 windowInFullscreen = self._windowInFullscreen
126 if windowInFullscreen:
127 self._window.fullscreen()
129 self._window.unfullscreen()
131 def jump_to(self, node):
132 raise NotImplementedError("On %s" % self)
134 @misc_utils.log_exception(_moduleLogger)
135 def _on_about(self, *args):
138 @misc_utils.log_exception(_moduleLogger)
139 def _on_destroy(self, *args):
140 self._isDestroyed = True
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
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)
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)
158 event.keyval == gtk.keysyms.F6 or
159 event.keyval in RETURN_TYPES and isCtrl
161 # The "Full screen" hardware key has been pressed
162 if self._windowInFullscreen:
163 self._window.unfullscreen ()
165 self._window.fullscreen ()
168 event.keyval in (gtk.keysyms.w, ) and
169 event.get_state() & gtk.gdk.CONTROL_MASK
171 self._window.destroy()
173 event.keyval in (gtk.keysyms.q, ) and
174 event.get_state() & gtk.gdk.CONTROL_MASK
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))
185 @misc_utils.log_exception(_moduleLogger)
186 def _on_home(self, *args):
188 self._window.destroy()
190 @misc_utils.log_exception(_moduleLogger)
191 def _on_child_fullscreen(self, source, isFull):
193 _moduleLogger.info("Full screen %r to mirror child %r" % (self, source))
194 self._window.fullscreen()
196 _moduleLogger.info("Unfull screen %r to mirror child %r" % (self, source))
197 self._window.unfullscreen()
199 @misc_utils.log_exception(_moduleLogger)
200 def _on_jump(self, source, node):
201 raise NotImplementedError("On %s" % self)
203 @misc_utils.log_exception(_moduleLogger)
204 def _on_quit(self, *args):
206 self._window.destroy()
209 class ListWindow(BasicWindow):
211 def __init__(self, app, player, store, node):
212 BasicWindow.__init__(self, app, player, store)
215 self.connect_auto(self._player, "title-change", self._on_player_title_change)
217 self._loadingBanner = banners.GenericBanner()
219 modelTypes, columns = zip(*self._get_columns())
221 self._model = gtk.ListStore(*modelTypes)
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)
232 self._viewport = gtk.Viewport()
233 self._viewport.add(self._treeView)
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)
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)
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)
250 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
251 self._layout.pack_start(self._contentLayout, True, True)
254 BasicWindow.show(self)
256 self._errorBanner.toplevel.hide()
257 self._loadingBanner.toplevel.hide()
260 self._presenter.refresh()
263 def _get_columns(cls):
264 raise NotImplementedError("")
266 def _get_current_row(self):
267 if self._player.node is None:
269 ancestors, current, descendants = stream_index.common_paths(self._player.node, self._node)
272 activeChild = descendants[0]
273 for i, row in enumerate(self._model):
274 if activeChild is row[0]:
279 def jump_to(self, node):
280 ancestors, current, descendants = stream_index.common_paths(node, self._node)
282 raise RuntimeError("Cannot jump to node %s" % node)
284 _moduleLogger.info("Current node is the target")
286 child = descendants[0]
287 window = self._window_from_node(child)
290 def _window_from_node(self, node):
291 raise NotImplementedError("")
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)
299 @misc_utils.log_exception(_moduleLogger)
300 def _on_player_title_change(self, player, node):
301 assert not self._isDestroyed
304 @misc_utils.log_exception(_moduleLogger)
305 def _on_jump(self, source, node):
306 ancestors, current, descendants = stream_index.common_paths(node, self._node)
308 _moduleLogger.info("%s is not the target, moving up" % self._node)
309 self.emit("jump-to", node)
310 self._window.destroy()
313 _moduleLogger.info("Current node is the target")
315 child = descendants[0]
316 window = self._window_from_node(child)
319 @misc_utils.log_exception(_moduleLogger)
320 def _on_delay_scroll(self, *args):
321 self._scroll_to_row()
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...")
328 def _hide_loading(self):
329 self._loadingBanner.hide()
335 def _select_row(self):
336 rowIndex = self._get_current_row()
340 self._treeView.get_selection().select_path(path)
342 def _scroll_to_row(self):
343 rowIndex = self._get_current_row()
348 self._treeView.scroll_to_cell(path)
350 treeViewHeight = self._treeView.get_allocation().height
351 viewportHeight = self._viewport.get_allocation().height
353 viewsPerPort = treeViewHeight / float(viewportHeight)
354 maxRows = len(self._model)
355 percentThrough = rowIndex / float(maxRows)
356 dxByIndex = int(viewsPerPort * percentThrough * viewportHeight)
358 dxMax = max(treeViewHeight - viewportHeight, 0)
360 dx = min(dxByIndex, dxMax)
361 adjustment = self._treeScroller.get_vadjustment()
362 adjustment.value = dx
365 class PresenterWindow(BasicWindow):
367 def __init__(self, app, player, store, node):
368 BasicWindow.__init__(self, app, player, store)
370 self._playerNode = self._player.node
371 self._nextSearch = None
372 self._updateSeek = None
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)
378 self._loadingBanner = banners.GenericBanner()
380 self._presenter = presenter.StreamPresenter(self._store)
381 self._presenter.set_context(
382 self._get_background(),
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)
391 self._seekbar = hildonize.create_seekbar()
392 self._seekbar.connect("change-value", self._on_user_seek)
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)
398 self._window.set_title(self._node.get_parent().title)
400 def _get_background(self):
401 raise NotImplementedError()
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)
411 def jump_to(self, node):
412 assert self._node is node
416 return self._playerNode is self._node
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...")
423 def _hide_loading(self):
424 self._loadingBanner.hide()
426 def _set_context(self, state):
427 if state == self._player.STATE_PLAY:
429 self._presenter.set_state(self._store.STORE_LOOKUP["pause"])
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"])
437 _moduleLogger.info("Unhandled player state %s" % state)
439 @misc_utils.log_exception(_moduleLogger)
440 def _on_user_seek(self, widget, scroll, value):
441 self._player.seek(value / 100.0)
443 @misc_utils.log_exception(_moduleLogger)
444 def _on_player_update_seek(self):
445 if self._isDestroyed:
447 self._seekbar.set_value(self._player.percent_elapsed * 100)
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:
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)
460 if self._updateSeek is not None:
461 self._updateSeek.cancel()
462 self._updateSeek = None
464 if not self._presenterNavigation.is_active():
465 self._set_context(newState)
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
473 self._playerNode = node
474 self.emit("jump-to", node)
475 self._window.destroy()
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))
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:
487 imageName = "pause_pressed"
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"
495 _moduleLogger.info("Unhandled player state %s" % self._player.state)
496 elif navState == "down":
498 elif navState == "up":
499 if self._player.state == self._player.STATE_PLAY:
504 elif self._player.state == self._player.STATE_PAUSE:
506 elif self._player.state == self._player.STATE_STOP:
509 _moduleLogger.info("Unhandled player state %s" % self._player.state)
510 elif navState == "left":
512 elif navState == "right":
515 self._presenter.set_state(self._store.STORE_LOOKUP[imageName])
517 @misc_utils.log_exception(_moduleLogger)
518 def _on_nav_action(self, widget, navState):
519 self._set_context(self._player.state)
521 if navState == "clicking":
522 if self._player.state == self._player.STATE_PLAY:
526 self._player.set_piece_by_node(self._node)
528 elif self._player.state == self._player.STATE_PAUSE:
530 elif self._player.state == self._player.STATE_STOP:
531 self._player.set_piece_by_node(self._node)
534 _moduleLogger.info("Unhandled player state %s" % self._player.state)
535 elif navState == "down":
537 self._window.destroy()
538 elif navState == "up":
540 elif navState == "left":
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":
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)
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()
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))
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"])