From 74db32a3e870c99c6ea27107faff77146d871044 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Sat, 15 May 2010 12:29:27 -0500 Subject: [PATCH] Breaking out the various windows into separate files --- src/mormonchannel_gtk.py | 3 +- src/windows.py | 1053 -------------------------------------------- src/windows/__init__.py | 4 + src/windows/_base.py | 285 ++++++++++++ src/windows/conferences.py | 407 +++++++++++++++++ src/windows/radio.py | 262 +++++++++++ src/windows/source.py | 143 ++++++ 7 files changed, 1102 insertions(+), 1055 deletions(-) delete mode 100644 src/windows.py create mode 100644 src/windows/__init__.py create mode 100644 src/windows/_base.py create mode 100644 src/windows/conferences.py create mode 100644 src/windows/radio.py create mode 100644 src/windows/source.py diff --git a/src/mormonchannel_gtk.py b/src/mormonchannel_gtk.py index 2bb2c7f..967db54 100755 --- a/src/mormonchannel_gtk.py +++ b/src/mormonchannel_gtk.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- """ -@todo Restructure so there is a windows/ folder with a file per source @todo Add additional sources @todo Need to confirm id's are persistent (not just for todos but broken behavior on transition) @todo Track recent @@ -65,7 +64,7 @@ class MormonChannelProgram(hildonize.get_app_class()): self._osso_c = None self._deviceState = None - self._sourceSelector = windows.SourceSelector(self._player, self._store, self._index) + self._sourceSelector = windows.source.SourceSelector(self._player, self._store, self._index) self._sourceSelector.window.connect("destroy", self._on_destroy) self._sourceSelector.show() self._load_settings() diff --git a/src/windows.py b/src/windows.py deleted file mode 100644 index 0820900..0000000 --- a/src/windows.py +++ /dev/null @@ -1,1053 +0,0 @@ -# @todo Add icons to buttons/rows to indicate that the currently playing track is coming from that - -import ConfigParser -import datetime -import logging - -import gobject -import gtk - -import constants -import hildonize -import util.misc as misc_utils -import util.go_utils as go_utils - -import stream_index -import banners -import playcontrol -import presenter - - -_moduleLogger = logging.getLogger(__name__) - - -class BasicWindow(gobject.GObject, go_utils.AutoSignal): - - __gsignals__ = { - 'quit' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (), - ), - 'home' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (), - ), - 'jump-to' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_PYOBJECT, ), - ), - 'rotate' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_BOOLEAN, ), - ), - 'fullscreen' : ( - gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_BOOLEAN, ), - ), - } - - def __init__(self, player, store): - gobject.GObject.__init__(self) - self._isDestroyed = False - - self._player = player - self._store = store - - self._clipboard = gtk.clipboard_get() - self._windowInFullscreen = False - - self._errorBanner = banners.StackingBanner() - - self._layout = gtk.VBox() - self._layout.pack_start(self._errorBanner.toplevel, False, True) - - self._window = gtk.Window() - go_utils.AutoSignal.__init__(self, self.window) - self._window.add(self._layout) - self._window = hildonize.hildonize_window(self, self._window) - - self._window.set_icon(self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["icon"])) - self._window.connect("key-press-event", self._on_key_press) - self._window.connect("window-state-event", self._on_window_state_change) - self._window.connect("destroy", self._on_destroy) - - @property - def window(self): - return self._window - - def show(self): - self._window.show_all() - - def save_settings(self, config, sectionName): - config.add_section(sectionName) - config.set(sectionName, "fullscreen", str(self._windowInFullscreen)) - - def load_settings(self, config, sectionName): - try: - self._windowInFullscreen = config.getboolean(sectionName, "fullscreen") - except ConfigParser.NoSectionError, e: - _moduleLogger.info( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ) - ) - - if self._windowInFullscreen: - self._window.fullscreen() - else: - self._window.unfullscreen() - - def jump_to(self, node): - raise NotImplementedError("On %s" % self) - - @misc_utils.log_exception(_moduleLogger) - def _on_destroy(self, *args): - self._isDestroyed = True - - @misc_utils.log_exception(_moduleLogger) - def _on_window_state_change(self, widget, event, *args): - if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: - self._windowInFullscreen = True - else: - self._windowInFullscreen = False - self.emit("fullscreen", self._windowInFullscreen) - - @misc_utils.log_exception(_moduleLogger) - def _on_key_press(self, widget, event, *args): - RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter) - isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK) - if ( - event.keyval == gtk.keysyms.F6 or - event.keyval in RETURN_TYPES and isCtrl - ): - # The "Full screen" hardware key has been pressed - if self._windowInFullscreen: - self._window.unfullscreen () - else: - self._window.fullscreen () - return True - elif ( - event.keyval in (gtk.keysyms.w, ) and - event.get_state() & gtk.gdk.CONTROL_MASK - ): - self._window.destroy() - elif ( - event.keyval in (gtk.keysyms.q, ) and - event.get_state() & gtk.gdk.CONTROL_MASK - ): - self.emit("quit") - elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK: - with open(constants._user_logpath_, "r") as f: - logLines = f.xreadlines() - log = "".join(logLines) - self._clipboard.set_text(str(log)) - return True - - @misc_utils.log_exception(_moduleLogger) - def _on_home(self, *args): - self.emit("home") - self._window.destroy() - - @misc_utils.log_exception(_moduleLogger) - def _on_jump(self, source, node): - raise NotImplementedError("On %s" % self) - - @misc_utils.log_exception(_moduleLogger) - def _on_quit(self, *args): - self.emit("quit") - self._window.destroy() - - -class SourceSelector(BasicWindow): - - def __init__(self, player, store, index): - BasicWindow.__init__(self, player, store) - self._languages = [] - self._index = index - - self._loadingBanner = banners.GenericBanner() - - self._radioButton = self._create_button("radio", "Radio") - self._radioButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_RADIO) - self._radioWrapper = gtk.VBox() - self._radioWrapper.pack_start(self._radioButton, False, True) - - self._conferenceButton = self._create_button("conferences", "Conferences") - self._conferenceButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_CONFERENCES) - self._conferenceWrapper = gtk.VBox() - self._conferenceWrapper.pack_start(self._conferenceButton, False, True) - - self._magazineButton = self._create_button("magazines", "Magazines") - self._magazineButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_MAGAZINES) - self._magazineWrapper = gtk.VBox() - self._magazineWrapper.pack_start(self._magazineButton, False, True) - - self._scriptureButton = self._create_button("scriptures", "Scriptures") - self._scriptureButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_SCRIPTURES) - self._scriptureWrapper = gtk.VBox() - self._scriptureWrapper.pack_start(self._scriptureButton, False, True) - - self._buttonLayout = gtk.VButtonBox() - self._buttonLayout.set_layout(gtk.BUTTONBOX_SPREAD) - self._buttonLayout.pack_start(self._radioWrapper, True, True) - self._buttonLayout.pack_start(self._conferenceWrapper, True, True) - self._buttonLayout.pack_start(self._magazineWrapper, True, True) - self._buttonLayout.pack_start(self._scriptureWrapper, True, True) - - self._separator = gtk.HSeparator() - self._playcontrol = playcontrol.NavControl(player, store) - self._playcontrol.connect("jump-to", self._on_jump) - - self._layout.pack_start(self._loadingBanner.toplevel, False, False) - self._layout.pack_start(self._buttonLayout, True, True) - self._layout.pack_start(self._separator, False, True) - self._layout.pack_start(self._playcontrol.toplevel, False, True) - - self._window.set_title(constants.__pretty_app_name__) - - def show(self): - BasicWindow.show(self) - - self._errorBanner.toplevel.hide() - self._playcontrol.toplevel.hide() - - self._refresh() - - def _show_loading(self): - animationPath = self._store.STORE_LOOKUP["loading"] - animation = self._store.get_pixbuf_animation_from_store(animationPath) - self._loadingBanner.show(animation, "Loading...") - self._buttonLayout.set_sensitive(False) - - def _hide_loading(self): - self._loadingBanner.hide() - self._buttonLayout.set_sensitive(True) - - def _refresh(self): - self._show_loading() - self._index.get_languages(self._on_languages, self._on_error) - - def _create_button(self, icon, message): - image = self._store.get_image_from_store(self._store.STORE_LOOKUP[icon]) - - label = gtk.Label() - label.set_text(message) - - buttonLayout = gtk.HBox(False, 5) - buttonLayout.pack_start(image, False, False) - buttonLayout.pack_start(label, False, True) - button = gtk.Button() - button.add(buttonLayout) - - return button - - @misc_utils.log_exception(_moduleLogger) - def _on_languages(self, languages): - self._hide_loading() - self._languages = list(languages) - - @misc_utils.log_exception(_moduleLogger) - def _on_error(self, exception): - self._hide_loading() - self._errorBanner.push_message(str(exception)) - - def _window_from_node(self, node): - if node.id == stream_index.SOURCE_RADIO: - Source = RadioWindow - elif node.id == stream_index.SOURCE_CONFERENCES: - Source = ConferencesWindow - elif node.id == stream_index.SOURCE_MAGAZINES: - pass - elif node.id == stream_index.SOURCE_SCRIPTURES: - pass - sourceWindow = Source(self._player, self._store, node) - sourceWindow.window.set_modal(True) - sourceWindow.window.set_transient_for(self._window) - sourceWindow.window.set_default_size(*self._window.get_size()) - sourceWindow.connect("quit", self._on_quit) - sourceWindow.connect("jump-to", self._on_jump) - sourceWindow.show() - return sourceWindow - - @misc_utils.log_exception(_moduleLogger) - def _on_jump(self, source, node): - targetNodePath = list(reversed(list(stream_index.walk_ancestors(node)))) - ancestor = targetNodePath[0] - window = self._window_from_node(ancestor) - window.jump_to(node) - - @misc_utils.log_exception(_moduleLogger) - def _on_source_selected(self, widget, nodeName): - node = self._index.get_source(nodeName, self._languages[0]["id"]) - self._window_from_node(node) - - -gobject.type_register(SourceSelector) - - -class RadioWindow(BasicWindow): - - def __init__(self, player, store, node): - BasicWindow.__init__(self, player, store) - self._node = node - self._childNode = None - - self.connect_auto(self._player, "state-change", self._on_player_state_change) - self.connect_auto(self._player, "title-change", self._on_player_title_change) - - self._loadingBanner = banners.GenericBanner() - - headerPath = self._store.STORE_LOOKUP["radio_header"] - self._header = self._store.get_image_from_store(headerPath) - self._headerNavigation = presenter.NavigationBox() - self._headerNavigation.toplevel.add(self._header) - self._headerNavigation.connect("action", self._on_nav_action) - self._headerNavigation.connect("navigating", self._on_navigating) - - self._programmingModel = gtk.ListStore( - gobject.TYPE_STRING, - gobject.TYPE_STRING, - ) - - textrenderer = gtk.CellRendererText() - timeColumn = gtk.TreeViewColumn("Time") - timeColumn.pack_start(textrenderer, expand=True) - timeColumn.add_attribute(textrenderer, "text", 0) - - textrenderer = gtk.CellRendererText() - titleColumn = gtk.TreeViewColumn("Program") - titleColumn.pack_start(textrenderer, expand=True) - titleColumn.add_attribute(textrenderer, "text", 1) - - self._treeView = gtk.TreeView() - self._treeView.set_headers_visible(False) - self._treeView.set_model(self._programmingModel) - self._treeView.append_column(timeColumn) - self._treeView.append_column(titleColumn) - self._treeView.get_selection().connect("changed", self._on_row_changed) - - self._treeScroller = gtk.ScrolledWindow() - self._treeScroller.add(self._treeView) - self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - - self._presenter = presenter.StreamMiniPresenter(self._store) - self._presenterNavigation = presenter.NavigationBox() - self._presenterNavigation.toplevel.add(self._presenter.toplevel) - self._presenterNavigation.connect("action", self._on_nav_action) - self._presenterNavigation.connect("navigating", self._on_navigating) - - self._radioLayout = gtk.VBox(False) - self._radioLayout.pack_start(self._headerNavigation.toplevel, False, False) - self._radioLayout.pack_start(self._treeScroller, True, True) - self._radioLayout.pack_start(self._presenterNavigation.toplevel, False, True) - - self._layout.pack_start(self._loadingBanner.toplevel, False, False) - self._layout.pack_start(self._radioLayout, True, True) - - self._dateShown = datetime.datetime.now() - self._update_title() - - def show(self): - BasicWindow.show(self) - - self._errorBanner.toplevel.hide() - self._loadingBanner.toplevel.hide() - - self._refresh() - - def jump_to(self, node): - _moduleLogger.info("Only 1 channel, nothing to jump to") - - def _update_title(self): - self._window.set_title("%s - %s" % (self._node.title, self._dateShown.strftime("%m/%d"))) - - @property - def _active(self): - return self._player.node is self._childNode - - def _set_context(self, state): - if state == self._player.STATE_PLAY: - if self._active: - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) - else: - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - elif state == self._player.STATE_PAUSE: - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - elif state == self._player.STATE_STOP: - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - else: - _moduleLogger.info("Unhandled player state %s" % state) - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - - def _show_loading(self): - animationPath = self._store.STORE_LOOKUP["loading"] - animation = self._store.get_pixbuf_animation_from_store(animationPath) - self._loadingBanner.show(animation, "Loading...") - - def _hide_loading(self): - self._loadingBanner.hide() - - def _refresh(self): - self._show_loading() - self._programmingModel.clear() - self._node.get_children( - self._on_channels, - self._on_load_error, - ) - self._set_context(self._player.state) - - def _get_current_row(self): - nowTime = self._dateShown.strftime("%H:%M:%S") - i = 0 - for i, row in enumerate(self._programmingModel): - if nowTime < row[0]: - if i == 0: - return 0 - else: - return i - 1 - else: - return i - - @misc_utils.log_exception(_moduleLogger) - def _on_player_state_change(self, player, newState): - if self._headerNavigation.is_active() or self._presenterNavigation.is_active(): - return - - self._set_context(newState) - - @misc_utils.log_exception(_moduleLogger) - def _on_player_title_change(self, player, node): - if node is not self._childNode or node is None: - _moduleLogger.info("Player title magically changed to %s" % player.title) - return - - @misc_utils.log_exception(_moduleLogger) - def _on_navigating(self, widget, navState): - if navState == "clicking": - if self._player.state == self._player.STATE_PLAY: - if self._active: - imageName = "pause_pressed" - else: - imageName = "play_pressed" - elif self._player.state == self._player.STATE_PAUSE: - imageName = "play_pressed" - elif self._player.state == self._player.STATE_STOP: - imageName = "play_pressed" - else: - imageName = "play_pressed" - _moduleLogger.info("Unhandled player state %s" % self._player.state) - elif navState == "down": - imageName = "home" - else: - if self._player.state == self._player.STATE_PLAY: - imageName = "pause" - else: - imageName = "play" - - self._presenter.set_state(self._store.STORE_LOOKUP[imageName]) - - @misc_utils.log_exception(_moduleLogger) - def _on_nav_action(self, widget, navState): - self._set_context(self._player.state) - - if navState == "clicking": - if self._player.state == self._player.STATE_PLAY: - if self._active: - self._player.pause() - else: - self._player.set_piece_by_node(self._childNode) - self._player.play() - elif self._player.state == self._player.STATE_PAUSE: - self._player.play() - elif self._player.state == self._player.STATE_STOP: - self._player.set_piece_by_node(self._childNode) - self._player.play() - else: - _moduleLogger.info("Unhandled player state %s" % self._player.state) - elif navState == "down": - self.window.destroy() - elif navState == "up": - pass - elif navState == "left": - self._dateShown += datetime.timedelta(days=1) - self._update_title() - self._refresh() - elif navState == "right": - self._dateShown -= datetime.timedelta(days=1) - self._update_title() - self._refresh() - - @misc_utils.log_exception(_moduleLogger) - def _on_channels(self, channels): - if self._isDestroyed: - _moduleLogger.info("Download complete but window destroyed") - return - - channels = channels - if 1 < len(channels): - _moduleLogger.warning("More channels now available!") - self._childNode = channels[0] - self._childNode.get_programming( - self._dateShown, - self._on_channel, - self._on_load_error, - ) - - @misc_utils.log_exception(_moduleLogger) - def _on_channel(self, programs): - if self._isDestroyed: - _moduleLogger.info("Download complete but window destroyed") - return - - self._hide_loading() - for program in programs: - row = program["time"], program["title"] - self._programmingModel.append(row) - - currentDate = datetime.datetime.now() - if currentDate.date() != self._dateShown.date(): - self._treeView.get_selection().set_mode(gtk.SELECTION_NONE) - else: - self._treeView.get_selection().set_mode(gtk.SELECTION_SINGLE) - path = (self._get_current_row(), ) - self._treeView.scroll_to_cell(path) - self._treeView.get_selection().select_path(path) - - @misc_utils.log_exception(_moduleLogger) - def _on_load_error(self, exception): - self._hide_loading() - self._errorBanner.push_message(str(exception)) - - @misc_utils.log_exception(_moduleLogger) - def _on_row_changed(self, selection): - if len(self._programmingModel) == 0: - return - - rowIndex = self._get_current_row() - path = (rowIndex, ) - if not selection.path_is_selected(path): - # Undo the user's changing of the selection - selection.select_path(path) - - -gobject.type_register(RadioWindow) - - -class ListWindow(BasicWindow): - - def __init__(self, player, store, node): - BasicWindow.__init__(self, player, store) - self._node = node - - self.connect_auto(self._player, "title-change", self._on_player_title_change) - - self._loadingBanner = banners.GenericBanner() - - modelTypes, columns = zip(*self._get_columns()) - - self._model = gtk.ListStore(*modelTypes) - - self._treeView = gtk.TreeView() - self._treeView.connect("row-activated", self._on_row_activated) - self._treeView.set_headers_visible(False) - self._treeView.set_model(self._model) - for column in columns: - if column is not None: - self._treeView.append_column(column) - - self._treeScroller = gtk.ScrolledWindow() - self._treeScroller.add(self._treeView) - self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - - self._separator = gtk.HSeparator() - self._playcontrol = playcontrol.NavControl(self._player, self._store) - self._playcontrol.connect("home", self._on_home) - self._playcontrol.connect("jump-to", self._on_jump) - - self._contentLayout = gtk.VBox(False) - self._contentLayout.pack_start(self._treeScroller, True, True) - self._contentLayout.pack_start(self._separator, False, True) - self._contentLayout.pack_start(self._playcontrol.toplevel, False, True) - - self._layout.pack_start(self._loadingBanner.toplevel, False, False) - self._layout.pack_start(self._contentLayout, True, True) - - def show(self): - BasicWindow.show(self) - - self._errorBanner.toplevel.hide() - self._loadingBanner.toplevel.hide() - - self._refresh() - self._playcontrol.refresh() - - @classmethod - def _get_columns(cls): - raise NotImplementedError("") - - def _get_current_row(self): - if self._player.node is None: - return -1 - ancestors, current, descendants = stream_index.common_paths(self._player.node, self._node) - if not descendants: - return -1 - activeChild = descendants[0] - for i, row in enumerate(self._model): - if activeChild is row[0]: - return i - else: - return -1 - - def jump_to(self, node): - ancestors, current, descendants = stream_index.common_paths(node, self._node) - if current is None: - raise RuntimeError("Cannot jump to node %s" % node) - if not descendants: - _moduleLogger.info("Current node is the target") - return - child = descendants[0] - window = self._window_from_node(child) - window.jump_to(node) - - def _window_from_node(self, node): - raise NotImplementedError("") - - @misc_utils.log_exception(_moduleLogger) - def _on_row_activated(self, view, path, column): - raise NotImplementedError("") - - @misc_utils.log_exception(_moduleLogger) - def _on_player_title_change(self, player, node): - self._select_row() - - @misc_utils.log_exception(_moduleLogger) - def _on_jump(self, source, node): - ancestors, current, descendants = stream_index.common_paths(node, self._node) - if current is None: - _moduleLogger.info("%s is not the target, moving up" % self._node) - self.emit("jump-to", node) - self._window.destroy() - return - if not descendants: - _moduleLogger.info("Current node is the target") - return - child = descendants[0] - window = self._window_from_node(child) - window.jump_to(node) - - def _show_loading(self): - animationPath = self._store.STORE_LOOKUP["loading"] - animation = self._store.get_pixbuf_animation_from_store(animationPath) - self._loadingBanner.show(animation, "Loading...") - - def _hide_loading(self): - self._loadingBanner.hide() - - def _refresh(self): - self._show_loading() - self._model.clear() - - def _select_row(self): - rowIndex = self._get_current_row() - if rowIndex < 0: - return - path = (rowIndex, ) - self._treeView.scroll_to_cell(path) - self._treeView.get_selection().select_path(path) - - -class ConferencesWindow(ListWindow): - - def __init__(self, player, store, node): - ListWindow.__init__(self, player, store, node) - self._window.set_title(self._node.title) - - @classmethod - def _get_columns(cls): - yield gobject.TYPE_PYOBJECT, None - - textrenderer = gtk.CellRendererText() - column = gtk.TreeViewColumn("Date") - column.pack_start(textrenderer, expand=True) - column.add_attribute(textrenderer, "text", 1) - yield gobject.TYPE_STRING, column - - textrenderer = gtk.CellRendererText() - column = gtk.TreeViewColumn("Conference") - column.pack_start(textrenderer, expand=True) - column.add_attribute(textrenderer, "text", 2) - yield gobject.TYPE_STRING, column - - def _refresh(self): - ListWindow._refresh(self) - self._node.get_children( - self._on_conferences, - self._on_error, - ) - - @misc_utils.log_exception(_moduleLogger) - def _on_conferences(self, programs): - if self._isDestroyed: - _moduleLogger.info("Download complete but window destroyed") - return - - self._hide_loading() - for programNode in programs: - program = programNode.get_properties() - row = programNode, program["title"], program["full_title"] - self._model.append(row) - - self._select_row() - - @misc_utils.log_exception(_moduleLogger) - def _on_error(self, exception): - self._hide_loading() - self._errorBanner.push_message(str(exception)) - - def _window_from_node(self, node): - sessionsWindow = ConferenceSessionsWindow(self._player, self._store, node) - sessionsWindow.window.set_modal(True) - sessionsWindow.window.set_transient_for(self._window) - sessionsWindow.window.set_default_size(*self._window.get_size()) - sessionsWindow.connect("quit", self._on_quit) - sessionsWindow.connect("home", self._on_home) - sessionsWindow.connect("jump-to", self._on_jump) - sessionsWindow.show() - return sessionsWindow - - @misc_utils.log_exception(_moduleLogger) - def _on_row_activated(self, view, path, column): - itr = self._model.get_iter(path) - node = self._model.get_value(itr, 0) - self._window_from_node(node) - - -gobject.type_register(ConferencesWindow) - - -class ConferenceSessionsWindow(ListWindow): - - def __init__(self, player, store, node): - ListWindow.__init__(self, player, store, node) - self._window.set_title(self._node.title) - - @classmethod - def _get_columns(cls): - yield gobject.TYPE_PYOBJECT, None - - textrenderer = gtk.CellRendererText() - column = gtk.TreeViewColumn("Session") - column.pack_start(textrenderer, expand=True) - column.add_attribute(textrenderer, "text", 1) - yield gobject.TYPE_STRING, column - - def _refresh(self): - ListWindow._refresh(self) - self._node.get_children( - self._on_conference_sessions, - self._on_error, - ) - - @misc_utils.log_exception(_moduleLogger) - def _on_conference_sessions(self, programs): - if self._isDestroyed: - _moduleLogger.info("Download complete but window destroyed") - return - - self._hide_loading() - for programNode in programs: - program = programNode.get_properties() - row = programNode, program["title"] - self._model.append(row) - - self._select_row() - - @misc_utils.log_exception(_moduleLogger) - def _on_error(self, exception): - self._hide_loading() - self._errorBanner.push_message(str(exception)) - - def _window_from_node(self, node): - sessionsWindow = ConferenceTalksWindow(self._player, self._store, node) - sessionsWindow.window.set_modal(True) - sessionsWindow.window.set_transient_for(self._window) - sessionsWindow.window.set_default_size(*self._window.get_size()) - sessionsWindow.connect("quit", self._on_quit) - sessionsWindow.connect("home", self._on_home) - sessionsWindow.connect("jump-to", self._on_jump) - sessionsWindow.show() - return sessionsWindow - - @misc_utils.log_exception(_moduleLogger) - def _on_row_activated(self, view, path, column): - itr = self._model.get_iter(path) - node = self._model.get_value(itr, 0) - self._window_from_node(node) - - -gobject.type_register(ConferenceSessionsWindow) - - -class ConferenceTalksWindow(ListWindow): - - def __init__(self, player, store, node): - ListWindow.__init__(self, player, store, node) - self._window.set_title(self._node.title) - - @classmethod - def _get_columns(cls): - yield gobject.TYPE_PYOBJECT, None - - textrenderer = gtk.CellRendererText() - column = gtk.TreeViewColumn("Talk") - column.pack_start(textrenderer, expand=True) - column.add_attribute(textrenderer, "text", 1) - yield gobject.TYPE_STRING, column - - def _refresh(self): - ListWindow._refresh(self) - self._node.get_children( - self._on_conference_talks, - self._on_error, - ) - - @misc_utils.log_exception(_moduleLogger) - def _on_conference_talks(self, programs): - if self._isDestroyed: - _moduleLogger.info("Download complete but window destroyed") - return - - self._hide_loading() - for programNode in programs: - program = programNode.get_properties() - row = programNode, "%s\n%s" % (program["title"], program["speaker"]) - self._model.append(row) - - self._select_row() - - @misc_utils.log_exception(_moduleLogger) - def _on_error(self, exception): - self._hide_loading() - self._errorBanner.push_message(str(exception)) - - def _window_from_node(self, node): - sessionsWindow = ConferenceTalkWindow(self._player, self._store, node) - sessionsWindow.window.set_modal(True) - sessionsWindow.window.set_transient_for(self._window) - sessionsWindow.window.set_default_size(*self._window.get_size()) - sessionsWindow.connect("quit", self._on_quit) - sessionsWindow.connect("home", self._on_home) - sessionsWindow.connect("jump-to", self._on_jump) - sessionsWindow.show() - return sessionsWindow - - @misc_utils.log_exception(_moduleLogger) - def _on_row_activated(self, view, path, column): - itr = self._model.get_iter(path) - node = self._model.get_value(itr, 0) - self._window_from_node(node) - - -gobject.type_register(ConferenceTalksWindow) - - -class ConferenceTalkWindow(BasicWindow): - - def __init__(self, player, store, node): - BasicWindow.__init__(self, player, store) - self._node = node - self._playerNode = self._player.node - self._nextSearch = None - self._updateSeek = None - - self.connect_auto(self._player, "state-change", self._on_player_state_change) - self.connect_auto(self._player, "title-change", self._on_player_title_change) - self.connect_auto(self._player, "error", self._on_player_error) - - self._loadingBanner = banners.GenericBanner() - - self._presenter = presenter.StreamPresenter(self._store) - self._presenter.set_context( - self._store.STORE_LOOKUP["conference_background"], - self._node.title, - self._node.subtitle, - ) - self._presenterNavigation = presenter.NavigationBox() - self._presenterNavigation.toplevel.add(self._presenter.toplevel) - self._presenterNavigation.connect("action", self._on_nav_action) - self._presenterNavigation.connect("navigating", self._on_navigating) - - self._seekbar = hildonize.create_seekbar() - self._seekbar.connect("change-value", self._on_user_seek) - - self._layout.pack_start(self._loadingBanner.toplevel, False, False) - self._layout.pack_start(self._presenterNavigation.toplevel, True, True) - self._layout.pack_start(self._seekbar, False, False) - - self._window.set_title(self._node.title) - - def show(self): - BasicWindow.show(self) - self._window.show_all() - self._errorBanner.toplevel.hide() - self._loadingBanner.toplevel.hide() - self._set_context(self._player.state) - self._seekbar.hide() - - def jump_to(self, node): - assert self._node is node - - @property - def _active(self): - return self._playerNode is self._node - - def _show_loading(self): - animationPath = self._store.STORE_LOOKUP["loading"] - animation = self._store.get_pixbuf_animation_from_store(animationPath) - self._loadingBanner.show(animation, "Loading...") - - def _hide_loading(self): - self._loadingBanner.hide() - - def _set_context(self, state): - if state == self._player.STATE_PLAY: - if self._active: - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) - else: - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - elif state == self._player.STATE_PAUSE: - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - elif state == self._player.STATE_STOP: - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - else: - _moduleLogger.info("Unhandled player state %s" % state) - - @misc_utils.log_exception(_moduleLogger) - def _on_user_seek(self, widget, scroll, value): - self._player.seek(value / 100.0) - - @misc_utils.log_exception(_moduleLogger) - def _on_player_update_seek(self): - self._seekbar.set_value(self._player.percent_elapsed * 100) - return True if not self._isDestroyed else False - - @misc_utils.log_exception(_moduleLogger) - def _on_player_state_change(self, player, newState): - if self._active and self._player.state == self._player.STATE_PLAY: - self._seekbar.show() - assert self._updateSeek is None - self._updateSeek = go_utils.Timeout(self._updateSeek, once=False) - self._updateSeek.start(seconds=30) - else: - self._seekbar.hide() - self._updateSeek.cancel() - self._updateSeek = None - - if not self._presenterNavigation.is_active(): - self._set_context(newState) - - @misc_utils.log_exception(_moduleLogger) - def _on_player_title_change(self, player, node): - if not self._active or node in [None, self._node]: - self._playerNode = player.node - return - self._playerNode = player.node - self.emit("jump-to", node) - self._window.destroy() - - @misc_utils.log_exception(_moduleLogger) - def _on_player_error(self, player, err, debug): - _moduleLogger.error("%r - %r" % (err, debug)) - - @misc_utils.log_exception(_moduleLogger) - def _on_navigating(self, widget, navState): - if navState == "clicking": - if self._player.state == self._player.STATE_PLAY: - if self._active: - imageName = "pause_pressed" - else: - imageName = "play_pressed" - elif self._player.state == self._player.STATE_PAUSE: - imageName = "play_pressed" - elif self._player.state == self._player.STATE_STOP: - imageName = "play_pressed" - else: - _moduleLogger.info("Unhandled player state %s" % self._player.state) - elif navState == "down": - imageName = "home" - elif navState == "up": - if self._player.state == self._player.STATE_PLAY: - if self._active: - imageName = "pause" - else: - imageName = "play" - elif self._player.state == self._player.STATE_PAUSE: - imageName = "play" - elif self._player.state == self._player.STATE_STOP: - imageName = "play" - else: - _moduleLogger.info("Unhandled player state %s" % self._player.state) - elif navState == "left": - imageName = "next" - elif navState == "right": - imageName = "prev" - - self._presenter.set_state(self._store.STORE_LOOKUP[imageName]) - - @misc_utils.log_exception(_moduleLogger) - def _on_nav_action(self, widget, navState): - self._set_context(self._player.state) - - if navState == "clicking": - if self._player.state == self._player.STATE_PLAY: - if self._active: - self._player.pause() - else: - self._player.set_piece_by_node(self._node) - self._player.play() - elif self._player.state == self._player.STATE_PAUSE: - self._player.play() - elif self._player.state == self._player.STATE_STOP: - self._player.set_piece_by_node(self._node) - self._player.play() - else: - _moduleLogger.info("Unhandled player state %s" % self._player.state) - elif navState == "down": - self.emit("home") - self._window.destroy() - elif navState == "up": - pass - elif navState == "left": - if self._active: - self._player.next() - else: - assert self._nextSearch is None - self._nextSearch = stream_index.AsyncWalker(stream_index.get_next) - self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error) - elif navState == "right": - if self._active: - self._player.back() - else: - assert self._nextSearch is None - self._nextSearch = stream_index.AsyncWalker(stream_index.get_previous) - self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error) - - @misc_utils.log_exception(_moduleLogger) - def _on_next_node(self, node): - self._nextSearch = None - self.emit("jump-to", node) - self._window.destroy() - - @misc_utils.log_exception(_moduleLogger) - def _on_node_search_error(self, e): - self._nextSearch = None - self._errorBanner.push_message(str(e)) - - -gobject.type_register(ConferenceTalkWindow) diff --git a/src/windows/__init__.py b/src/windows/__init__.py new file mode 100644 index 0000000..7010b93 --- /dev/null +++ b/src/windows/__init__.py @@ -0,0 +1,4 @@ +import _base +import source +import radio +import conferences diff --git a/src/windows/_base.py b/src/windows/_base.py new file mode 100644 index 0000000..303b6b8 --- /dev/null +++ b/src/windows/_base.py @@ -0,0 +1,285 @@ +# @todo Add icons to buttons/rows to indicate that the currently playing track is coming from that + +import ConfigParser +import logging + +import gobject +import gtk + +import constants +import hildonize +import util.misc as misc_utils +import util.go_utils as go_utils + +import stream_index +import banners +import playcontrol + + +_moduleLogger = logging.getLogger(__name__) + + +class BasicWindow(gobject.GObject, go_utils.AutoSignal): + + __gsignals__ = { + 'quit' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (), + ), + 'home' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (), + ), + 'jump-to' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, ), + ), + 'rotate' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_BOOLEAN, ), + ), + 'fullscreen' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_BOOLEAN, ), + ), + } + + def __init__(self, player, store): + gobject.GObject.__init__(self) + self._isDestroyed = False + + self._player = player + self._store = store + + self._clipboard = gtk.clipboard_get() + self._windowInFullscreen = False + + self._errorBanner = banners.StackingBanner() + + self._layout = gtk.VBox() + self._layout.pack_start(self._errorBanner.toplevel, False, True) + + self._window = gtk.Window() + go_utils.AutoSignal.__init__(self, self.window) + self._window.add(self._layout) + self._window = hildonize.hildonize_window(self, self._window) + + self._window.set_icon(self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["icon"])) + self._window.connect("key-press-event", self._on_key_press) + self._window.connect("window-state-event", self._on_window_state_change) + self._window.connect("destroy", self._on_destroy) + + @property + def window(self): + return self._window + + def show(self): + self._window.show_all() + + def save_settings(self, config, sectionName): + config.add_section(sectionName) + config.set(sectionName, "fullscreen", str(self._windowInFullscreen)) + + def load_settings(self, config, sectionName): + try: + self._windowInFullscreen = config.getboolean(sectionName, "fullscreen") + except ConfigParser.NoSectionError, e: + _moduleLogger.info( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ) + ) + + if self._windowInFullscreen: + self._window.fullscreen() + else: + self._window.unfullscreen() + + def jump_to(self, node): + raise NotImplementedError("On %s" % self) + + @misc_utils.log_exception(_moduleLogger) + def _on_destroy(self, *args): + self._isDestroyed = True + + @misc_utils.log_exception(_moduleLogger) + def _on_window_state_change(self, widget, event, *args): + if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: + self._windowInFullscreen = True + else: + self._windowInFullscreen = False + self.emit("fullscreen", self._windowInFullscreen) + + @misc_utils.log_exception(_moduleLogger) + def _on_key_press(self, widget, event, *args): + RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter) + isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK) + if ( + event.keyval == gtk.keysyms.F6 or + event.keyval in RETURN_TYPES and isCtrl + ): + # The "Full screen" hardware key has been pressed + if self._windowInFullscreen: + self._window.unfullscreen () + else: + self._window.fullscreen () + return True + elif ( + event.keyval in (gtk.keysyms.w, ) and + event.get_state() & gtk.gdk.CONTROL_MASK + ): + self._window.destroy() + elif ( + event.keyval in (gtk.keysyms.q, ) and + event.get_state() & gtk.gdk.CONTROL_MASK + ): + self.emit("quit") + elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK: + with open(constants._user_logpath_, "r") as f: + logLines = f.xreadlines() + log = "".join(logLines) + self._clipboard.set_text(str(log)) + return True + + @misc_utils.log_exception(_moduleLogger) + def _on_home(self, *args): + self.emit("home") + self._window.destroy() + + @misc_utils.log_exception(_moduleLogger) + def _on_jump(self, source, node): + raise NotImplementedError("On %s" % self) + + @misc_utils.log_exception(_moduleLogger) + def _on_quit(self, *args): + self.emit("quit") + self._window.destroy() + + +class ListWindow(BasicWindow): + + def __init__(self, player, store, node): + BasicWindow.__init__(self, player, store) + self._node = node + + self.connect_auto(self._player, "title-change", self._on_player_title_change) + + self._loadingBanner = banners.GenericBanner() + + modelTypes, columns = zip(*self._get_columns()) + + self._model = gtk.ListStore(*modelTypes) + + self._treeView = gtk.TreeView() + self._treeView.connect("row-activated", self._on_row_activated) + self._treeView.set_headers_visible(False) + self._treeView.set_model(self._model) + for column in columns: + if column is not None: + self._treeView.append_column(column) + + self._treeScroller = gtk.ScrolledWindow() + self._treeScroller.add(self._treeView) + self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + + self._separator = gtk.HSeparator() + self._playcontrol = playcontrol.NavControl(self._player, self._store) + self._playcontrol.connect("home", self._on_home) + self._playcontrol.connect("jump-to", self._on_jump) + + self._contentLayout = gtk.VBox(False) + self._contentLayout.pack_start(self._treeScroller, True, True) + self._contentLayout.pack_start(self._separator, False, True) + self._contentLayout.pack_start(self._playcontrol.toplevel, False, True) + + self._layout.pack_start(self._loadingBanner.toplevel, False, False) + self._layout.pack_start(self._contentLayout, True, True) + + def show(self): + BasicWindow.show(self) + + self._errorBanner.toplevel.hide() + self._loadingBanner.toplevel.hide() + + self._refresh() + self._playcontrol.refresh() + + @classmethod + def _get_columns(cls): + raise NotImplementedError("") + + def _get_current_row(self): + if self._player.node is None: + return -1 + ancestors, current, descendants = stream_index.common_paths(self._player.node, self._node) + if not descendants: + return -1 + activeChild = descendants[0] + for i, row in enumerate(self._model): + if activeChild is row[0]: + return i + else: + return -1 + + def jump_to(self, node): + ancestors, current, descendants = stream_index.common_paths(node, self._node) + if current is None: + raise RuntimeError("Cannot jump to node %s" % node) + if not descendants: + _moduleLogger.info("Current node is the target") + return + child = descendants[0] + window = self._window_from_node(child) + window.jump_to(node) + + def _window_from_node(self, node): + raise NotImplementedError("") + + @misc_utils.log_exception(_moduleLogger) + def _on_row_activated(self, view, path, column): + raise NotImplementedError("") + + @misc_utils.log_exception(_moduleLogger) + def _on_player_title_change(self, player, node): + self._select_row() + + @misc_utils.log_exception(_moduleLogger) + def _on_jump(self, source, node): + ancestors, current, descendants = stream_index.common_paths(node, self._node) + if current is None: + _moduleLogger.info("%s is not the target, moving up" % self._node) + self.emit("jump-to", node) + self._window.destroy() + return + if not descendants: + _moduleLogger.info("Current node is the target") + return + child = descendants[0] + window = self._window_from_node(child) + window.jump_to(node) + + def _show_loading(self): + animationPath = self._store.STORE_LOOKUP["loading"] + animation = self._store.get_pixbuf_animation_from_store(animationPath) + self._loadingBanner.show(animation, "Loading...") + + def _hide_loading(self): + self._loadingBanner.hide() + + def _refresh(self): + self._show_loading() + self._model.clear() + + def _select_row(self): + rowIndex = self._get_current_row() + if rowIndex < 0: + return + path = (rowIndex, ) + self._treeView.scroll_to_cell(path) + self._treeView.get_selection().select_path(path) diff --git a/src/windows/conferences.py b/src/windows/conferences.py new file mode 100644 index 0000000..7e18f5e --- /dev/null +++ b/src/windows/conferences.py @@ -0,0 +1,407 @@ +import logging + +import gobject +import gtk + +import hildonize +import util.go_utils as go_utils +import util.misc as misc_utils +import banners +import presenter +import stream_index + +import windows + + +_moduleLogger = logging.getLogger(__name__) + + +class ConferencesWindow(windows._base.ListWindow): + + def __init__(self, player, store, node): + windows._base.ListWindow.__init__(self, player, store, node) + self._window.set_title(self._node.title) + + @classmethod + def _get_columns(cls): + yield gobject.TYPE_PYOBJECT, None + + textrenderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("Date") + column.pack_start(textrenderer, expand=True) + column.add_attribute(textrenderer, "text", 1) + yield gobject.TYPE_STRING, column + + textrenderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("Conference") + column.pack_start(textrenderer, expand=True) + column.add_attribute(textrenderer, "text", 2) + yield gobject.TYPE_STRING, column + + def _refresh(self): + windows._base.ListWindow._refresh(self) + self._node.get_children( + self._on_conferences, + self._on_error, + ) + + @misc_utils.log_exception(_moduleLogger) + def _on_conferences(self, programs): + if self._isDestroyed: + _moduleLogger.info("Download complete but window destroyed") + return + + self._hide_loading() + for programNode in programs: + program = programNode.get_properties() + row = programNode, program["title"], program["full_title"] + self._model.append(row) + + self._select_row() + + @misc_utils.log_exception(_moduleLogger) + def _on_error(self, exception): + self._hide_loading() + self._errorBanner.push_message(str(exception)) + + def _window_from_node(self, node): + sessionsWindow = ConferenceSessionsWindow(self._player, self._store, node) + sessionsWindow.window.set_modal(True) + sessionsWindow.window.set_transient_for(self._window) + sessionsWindow.window.set_default_size(*self._window.get_size()) + sessionsWindow.connect("quit", self._on_quit) + sessionsWindow.connect("home", self._on_home) + sessionsWindow.connect("jump-to", self._on_jump) + sessionsWindow.show() + return sessionsWindow + + @misc_utils.log_exception(_moduleLogger) + def _on_row_activated(self, view, path, column): + itr = self._model.get_iter(path) + node = self._model.get_value(itr, 0) + self._window_from_node(node) + + +gobject.type_register(ConferencesWindow) + + +class ConferenceSessionsWindow(windows._base.ListWindow): + + def __init__(self, player, store, node): + windows._base.ListWindow.__init__(self, player, store, node) + self._window.set_title(self._node.title) + + @classmethod + def _get_columns(cls): + yield gobject.TYPE_PYOBJECT, None + + textrenderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("Session") + column.pack_start(textrenderer, expand=True) + column.add_attribute(textrenderer, "text", 1) + yield gobject.TYPE_STRING, column + + def _refresh(self): + windows._base.ListWindow._refresh(self) + self._node.get_children( + self._on_conference_sessions, + self._on_error, + ) + + @misc_utils.log_exception(_moduleLogger) + def _on_conference_sessions(self, programs): + if self._isDestroyed: + _moduleLogger.info("Download complete but window destroyed") + return + + self._hide_loading() + for programNode in programs: + program = programNode.get_properties() + row = programNode, program["title"] + self._model.append(row) + + self._select_row() + + @misc_utils.log_exception(_moduleLogger) + def _on_error(self, exception): + self._hide_loading() + self._errorBanner.push_message(str(exception)) + + def _window_from_node(self, node): + sessionsWindow = ConferenceTalksWindow(self._player, self._store, node) + sessionsWindow.window.set_modal(True) + sessionsWindow.window.set_transient_for(self._window) + sessionsWindow.window.set_default_size(*self._window.get_size()) + sessionsWindow.connect("quit", self._on_quit) + sessionsWindow.connect("home", self._on_home) + sessionsWindow.connect("jump-to", self._on_jump) + sessionsWindow.show() + return sessionsWindow + + @misc_utils.log_exception(_moduleLogger) + def _on_row_activated(self, view, path, column): + itr = self._model.get_iter(path) + node = self._model.get_value(itr, 0) + self._window_from_node(node) + + +gobject.type_register(ConferenceSessionsWindow) + + +class ConferenceTalksWindow(windows._base.ListWindow): + + def __init__(self, player, store, node): + windows._base.ListWindow.__init__(self, player, store, node) + self._window.set_title(self._node.title) + + @classmethod + def _get_columns(cls): + yield gobject.TYPE_PYOBJECT, None + + textrenderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("Talk") + column.pack_start(textrenderer, expand=True) + column.add_attribute(textrenderer, "text", 1) + yield gobject.TYPE_STRING, column + + def _refresh(self): + windows._base.ListWindow._refresh(self) + self._node.get_children( + self._on_conference_talks, + self._on_error, + ) + + @misc_utils.log_exception(_moduleLogger) + def _on_conference_talks(self, programs): + if self._isDestroyed: + _moduleLogger.info("Download complete but window destroyed") + return + + self._hide_loading() + for programNode in programs: + program = programNode.get_properties() + row = programNode, "%s\n%s" % (program["title"], program["speaker"]) + self._model.append(row) + + self._select_row() + + @misc_utils.log_exception(_moduleLogger) + def _on_error(self, exception): + self._hide_loading() + self._errorBanner.push_message(str(exception)) + + def _window_from_node(self, node): + sessionsWindow = ConferenceTalkWindow(self._player, self._store, node) + sessionsWindow.window.set_modal(True) + sessionsWindow.window.set_transient_for(self._window) + sessionsWindow.window.set_default_size(*self._window.get_size()) + sessionsWindow.connect("quit", self._on_quit) + sessionsWindow.connect("home", self._on_home) + sessionsWindow.connect("jump-to", self._on_jump) + sessionsWindow.show() + return sessionsWindow + + @misc_utils.log_exception(_moduleLogger) + def _on_row_activated(self, view, path, column): + itr = self._model.get_iter(path) + node = self._model.get_value(itr, 0) + self._window_from_node(node) + + +gobject.type_register(ConferenceTalksWindow) + + +class ConferenceTalkWindow(windows._base.BasicWindow): + + def __init__(self, player, store, node): + windows._base.BasicWindow.__init__(self, player, store) + self._node = node + self._playerNode = self._player.node + self._nextSearch = None + self._updateSeek = None + + self.connect_auto(self._player, "state-change", self._on_player_state_change) + self.connect_auto(self._player, "title-change", self._on_player_title_change) + self.connect_auto(self._player, "error", self._on_player_error) + + self._loadingBanner = banners.GenericBanner() + + self._presenter = presenter.StreamPresenter(self._store) + self._presenter.set_context( + self._store.STORE_LOOKUP["conference_background"], + self._node.title, + self._node.subtitle, + ) + self._presenterNavigation = presenter.NavigationBox() + self._presenterNavigation.toplevel.add(self._presenter.toplevel) + self._presenterNavigation.connect("action", self._on_nav_action) + self._presenterNavigation.connect("navigating", self._on_navigating) + + self._seekbar = hildonize.create_seekbar() + self._seekbar.connect("change-value", self._on_user_seek) + + self._layout.pack_start(self._loadingBanner.toplevel, False, False) + self._layout.pack_start(self._presenterNavigation.toplevel, True, True) + self._layout.pack_start(self._seekbar, False, False) + + self._window.set_title(self._node.title) + + def show(self): + windows._base.BasicWindow.show(self) + self._window.show_all() + self._errorBanner.toplevel.hide() + self._loadingBanner.toplevel.hide() + self._set_context(self._player.state) + self._seekbar.hide() + + def jump_to(self, node): + assert self._node is node + + @property + def _active(self): + return self._playerNode is self._node + + def _show_loading(self): + animationPath = self._store.STORE_LOOKUP["loading"] + animation = self._store.get_pixbuf_animation_from_store(animationPath) + self._loadingBanner.show(animation, "Loading...") + + def _hide_loading(self): + self._loadingBanner.hide() + + def _set_context(self, state): + if state == self._player.STATE_PLAY: + if self._active: + self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + else: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + elif state == self._player.STATE_PAUSE: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + elif state == self._player.STATE_STOP: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + else: + _moduleLogger.info("Unhandled player state %s" % state) + + @misc_utils.log_exception(_moduleLogger) + def _on_user_seek(self, widget, scroll, value): + self._player.seek(value / 100.0) + + @misc_utils.log_exception(_moduleLogger) + def _on_player_update_seek(self): + self._seekbar.set_value(self._player.percent_elapsed * 100) + return True if not self._isDestroyed else False + + @misc_utils.log_exception(_moduleLogger) + def _on_player_state_change(self, player, newState): + if self._active and self._player.state == self._player.STATE_PLAY: + self._seekbar.show() + assert self._updateSeek is None + self._updateSeek = go_utils.Timeout(self._updateSeek, once=False) + self._updateSeek.start(seconds=30) + else: + self._seekbar.hide() + self._updateSeek.cancel() + self._updateSeek = None + + if not self._presenterNavigation.is_active(): + self._set_context(newState) + + @misc_utils.log_exception(_moduleLogger) + def _on_player_title_change(self, player, node): + if not self._active or node in [None, self._node]: + self._playerNode = player.node + return + self._playerNode = player.node + self.emit("jump-to", node) + self._window.destroy() + + @misc_utils.log_exception(_moduleLogger) + def _on_player_error(self, player, err, debug): + _moduleLogger.error("%r - %r" % (err, debug)) + + @misc_utils.log_exception(_moduleLogger) + def _on_navigating(self, widget, navState): + if navState == "clicking": + if self._player.state == self._player.STATE_PLAY: + if self._active: + imageName = "pause_pressed" + else: + imageName = "play_pressed" + elif self._player.state == self._player.STATE_PAUSE: + imageName = "play_pressed" + elif self._player.state == self._player.STATE_STOP: + imageName = "play_pressed" + else: + _moduleLogger.info("Unhandled player state %s" % self._player.state) + elif navState == "down": + imageName = "home" + elif navState == "up": + if self._player.state == self._player.STATE_PLAY: + if self._active: + imageName = "pause" + else: + imageName = "play" + elif self._player.state == self._player.STATE_PAUSE: + imageName = "play" + elif self._player.state == self._player.STATE_STOP: + imageName = "play" + else: + _moduleLogger.info("Unhandled player state %s" % self._player.state) + elif navState == "left": + imageName = "next" + elif navState == "right": + imageName = "prev" + + self._presenter.set_state(self._store.STORE_LOOKUP[imageName]) + + @misc_utils.log_exception(_moduleLogger) + def _on_nav_action(self, widget, navState): + self._set_context(self._player.state) + + if navState == "clicking": + if self._player.state == self._player.STATE_PLAY: + if self._active: + self._player.pause() + else: + self._player.set_piece_by_node(self._node) + self._player.play() + elif self._player.state == self._player.STATE_PAUSE: + self._player.play() + elif self._player.state == self._player.STATE_STOP: + self._player.set_piece_by_node(self._node) + self._player.play() + else: + _moduleLogger.info("Unhandled player state %s" % self._player.state) + elif navState == "down": + self.emit("home") + self._window.destroy() + elif navState == "up": + pass + elif navState == "left": + if self._active: + self._player.next() + else: + assert self._nextSearch is None + self._nextSearch = stream_index.AsyncWalker(stream_index.get_next) + self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error) + elif navState == "right": + if self._active: + self._player.back() + else: + assert self._nextSearch is None + self._nextSearch = stream_index.AsyncWalker(stream_index.get_previous) + self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error) + + @misc_utils.log_exception(_moduleLogger) + def _on_next_node(self, node): + self._nextSearch = None + self.emit("jump-to", node) + self._window.destroy() + + @misc_utils.log_exception(_moduleLogger) + def _on_node_search_error(self, e): + self._nextSearch = None + self._errorBanner.push_message(str(e)) + + +gobject.type_register(ConferenceTalkWindow) diff --git a/src/windows/radio.py b/src/windows/radio.py new file mode 100644 index 0000000..0f80479 --- /dev/null +++ b/src/windows/radio.py @@ -0,0 +1,262 @@ +import datetime +import logging + +import gobject +import gtk + +import util.misc as misc_utils +import banners +import presenter + +import windows + + +_moduleLogger = logging.getLogger(__name__) + + +class RadioWindow(windows._base.BasicWindow): + + def __init__(self, player, store, node): + windows._base.BasicWindow.__init__(self, player, store) + self._node = node + self._childNode = None + + self.connect_auto(self._player, "state-change", self._on_player_state_change) + self.connect_auto(self._player, "title-change", self._on_player_title_change) + + self._loadingBanner = banners.GenericBanner() + + headerPath = self._store.STORE_LOOKUP["radio_header"] + self._header = self._store.get_image_from_store(headerPath) + self._headerNavigation = presenter.NavigationBox() + self._headerNavigation.toplevel.add(self._header) + self._headerNavigation.connect("action", self._on_nav_action) + self._headerNavigation.connect("navigating", self._on_navigating) + + self._programmingModel = gtk.ListStore( + gobject.TYPE_STRING, + gobject.TYPE_STRING, + ) + + textrenderer = gtk.CellRendererText() + timeColumn = gtk.TreeViewColumn("Time") + timeColumn.pack_start(textrenderer, expand=True) + timeColumn.add_attribute(textrenderer, "text", 0) + + textrenderer = gtk.CellRendererText() + titleColumn = gtk.TreeViewColumn("Program") + titleColumn.pack_start(textrenderer, expand=True) + titleColumn.add_attribute(textrenderer, "text", 1) + + self._treeView = gtk.TreeView() + self._treeView.set_headers_visible(False) + self._treeView.set_model(self._programmingModel) + self._treeView.append_column(timeColumn) + self._treeView.append_column(titleColumn) + self._treeView.get_selection().connect("changed", self._on_row_changed) + + self._treeScroller = gtk.ScrolledWindow() + self._treeScroller.add(self._treeView) + self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + + self._presenter = presenter.StreamMiniPresenter(self._store) + self._presenterNavigation = presenter.NavigationBox() + self._presenterNavigation.toplevel.add(self._presenter.toplevel) + self._presenterNavigation.connect("action", self._on_nav_action) + self._presenterNavigation.connect("navigating", self._on_navigating) + + self._radioLayout = gtk.VBox(False) + self._radioLayout.pack_start(self._headerNavigation.toplevel, False, False) + self._radioLayout.pack_start(self._treeScroller, True, True) + self._radioLayout.pack_start(self._presenterNavigation.toplevel, False, True) + + self._layout.pack_start(self._loadingBanner.toplevel, False, False) + self._layout.pack_start(self._radioLayout, True, True) + + self._dateShown = datetime.datetime.now() + self._update_title() + + def show(self): + windows._base.BasicWindow.show(self) + + self._errorBanner.toplevel.hide() + self._loadingBanner.toplevel.hide() + + self._refresh() + + def jump_to(self, node): + _moduleLogger.info("Only 1 channel, nothing to jump to") + + def _update_title(self): + self._window.set_title("%s - %s" % (self._node.title, self._dateShown.strftime("%m/%d"))) + + @property + def _active(self): + return self._player.node is self._childNode + + def _set_context(self, state): + if state == self._player.STATE_PLAY: + if self._active: + self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + else: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + elif state == self._player.STATE_PAUSE: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + elif state == self._player.STATE_STOP: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + else: + _moduleLogger.info("Unhandled player state %s" % state) + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + + def _show_loading(self): + animationPath = self._store.STORE_LOOKUP["loading"] + animation = self._store.get_pixbuf_animation_from_store(animationPath) + self._loadingBanner.show(animation, "Loading...") + + def _hide_loading(self): + self._loadingBanner.hide() + + def _refresh(self): + self._show_loading() + self._programmingModel.clear() + self._node.get_children( + self._on_channels, + self._on_load_error, + ) + self._set_context(self._player.state) + + def _get_current_row(self): + nowTime = self._dateShown.strftime("%H:%M:%S") + i = 0 + for i, row in enumerate(self._programmingModel): + if nowTime < row[0]: + if i == 0: + return 0 + else: + return i - 1 + else: + return i + + @misc_utils.log_exception(_moduleLogger) + def _on_player_state_change(self, player, newState): + if self._headerNavigation.is_active() or self._presenterNavigation.is_active(): + return + + self._set_context(newState) + + @misc_utils.log_exception(_moduleLogger) + def _on_player_title_change(self, player, node): + if node is not self._childNode or node is None: + _moduleLogger.info("Player title magically changed to %s" % player.title) + return + + @misc_utils.log_exception(_moduleLogger) + def _on_navigating(self, widget, navState): + if navState == "clicking": + if self._player.state == self._player.STATE_PLAY: + if self._active: + imageName = "pause_pressed" + else: + imageName = "play_pressed" + elif self._player.state == self._player.STATE_PAUSE: + imageName = "play_pressed" + elif self._player.state == self._player.STATE_STOP: + imageName = "play_pressed" + else: + imageName = "play_pressed" + _moduleLogger.info("Unhandled player state %s" % self._player.state) + elif navState == "down": + imageName = "home" + else: + if self._player.state == self._player.STATE_PLAY: + imageName = "pause" + else: + imageName = "play" + + self._presenter.set_state(self._store.STORE_LOOKUP[imageName]) + + @misc_utils.log_exception(_moduleLogger) + def _on_nav_action(self, widget, navState): + self._set_context(self._player.state) + + if navState == "clicking": + if self._player.state == self._player.STATE_PLAY: + if self._active: + self._player.pause() + else: + self._player.set_piece_by_node(self._childNode) + self._player.play() + elif self._player.state == self._player.STATE_PAUSE: + self._player.play() + elif self._player.state == self._player.STATE_STOP: + self._player.set_piece_by_node(self._childNode) + self._player.play() + else: + _moduleLogger.info("Unhandled player state %s" % self._player.state) + elif navState == "down": + self.window.destroy() + elif navState == "up": + pass + elif navState == "left": + self._dateShown += datetime.timedelta(days=1) + self._update_title() + self._refresh() + elif navState == "right": + self._dateShown -= datetime.timedelta(days=1) + self._update_title() + self._refresh() + + @misc_utils.log_exception(_moduleLogger) + def _on_channels(self, channels): + if self._isDestroyed: + _moduleLogger.info("Download complete but window destroyed") + return + + channels = channels + if 1 < len(channels): + _moduleLogger.warning("More channels now available!") + self._childNode = channels[0] + self._childNode.get_programming( + self._dateShown, + self._on_channel, + self._on_load_error, + ) + + @misc_utils.log_exception(_moduleLogger) + def _on_channel(self, programs): + if self._isDestroyed: + _moduleLogger.info("Download complete but window destroyed") + return + + self._hide_loading() + for program in programs: + row = program["time"], program["title"] + self._programmingModel.append(row) + + currentDate = datetime.datetime.now() + if currentDate.date() != self._dateShown.date(): + self._treeView.get_selection().set_mode(gtk.SELECTION_NONE) + else: + self._treeView.get_selection().set_mode(gtk.SELECTION_SINGLE) + path = (self._get_current_row(), ) + self._treeView.scroll_to_cell(path) + self._treeView.get_selection().select_path(path) + + @misc_utils.log_exception(_moduleLogger) + def _on_load_error(self, exception): + self._hide_loading() + self._errorBanner.push_message(str(exception)) + + @misc_utils.log_exception(_moduleLogger) + def _on_row_changed(self, selection): + if len(self._programmingModel) == 0: + return + + rowIndex = self._get_current_row() + path = (rowIndex, ) + if not selection.path_is_selected(path): + # Undo the user's changing of the selection + selection.select_path(path) + + +gobject.type_register(RadioWindow) diff --git a/src/windows/source.py b/src/windows/source.py new file mode 100644 index 0000000..39ffb44 --- /dev/null +++ b/src/windows/source.py @@ -0,0 +1,143 @@ +import logging + +import gobject +import gtk + +import constants +import util.misc as misc_utils +import banners +import playcontrol +import stream_index + +import windows + + +_moduleLogger = logging.getLogger(__name__) + + +class SourceSelector(windows._base.BasicWindow): + + def __init__(self, player, store, index): + windows._base.BasicWindow.__init__(self, player, store) + self._languages = [] + self._index = index + + self._loadingBanner = banners.GenericBanner() + + self._radioButton = self._create_button("radio", "Radio") + self._radioButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_RADIO) + self._radioWrapper = gtk.VBox() + self._radioWrapper.pack_start(self._radioButton, False, True) + + self._conferenceButton = self._create_button("conferences", "Conferences") + self._conferenceButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_CONFERENCES) + self._conferenceWrapper = gtk.VBox() + self._conferenceWrapper.pack_start(self._conferenceButton, False, True) + + self._magazineButton = self._create_button("magazines", "Magazines") + self._magazineButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_MAGAZINES) + self._magazineWrapper = gtk.VBox() + self._magazineWrapper.pack_start(self._magazineButton, False, True) + + self._scriptureButton = self._create_button("scriptures", "Scriptures") + self._scriptureButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_SCRIPTURES) + self._scriptureWrapper = gtk.VBox() + self._scriptureWrapper.pack_start(self._scriptureButton, False, True) + + self._buttonLayout = gtk.VButtonBox() + self._buttonLayout.set_layout(gtk.BUTTONBOX_SPREAD) + self._buttonLayout.pack_start(self._radioWrapper, True, True) + self._buttonLayout.pack_start(self._conferenceWrapper, True, True) + self._buttonLayout.pack_start(self._magazineWrapper, True, True) + self._buttonLayout.pack_start(self._scriptureWrapper, True, True) + + self._separator = gtk.HSeparator() + self._playcontrol = playcontrol.NavControl(player, store) + self._playcontrol.connect("jump-to", self._on_jump) + + self._layout.pack_start(self._loadingBanner.toplevel, False, False) + self._layout.pack_start(self._buttonLayout, True, True) + self._layout.pack_start(self._separator, False, True) + self._layout.pack_start(self._playcontrol.toplevel, False, True) + + self._window.set_title(constants.__pretty_app_name__) + + def show(self): + windows._base.BasicWindow.show(self) + + self._errorBanner.toplevel.hide() + self._playcontrol.toplevel.hide() + + self._refresh() + + def _show_loading(self): + animationPath = self._store.STORE_LOOKUP["loading"] + animation = self._store.get_pixbuf_animation_from_store(animationPath) + self._loadingBanner.show(animation, "Loading...") + self._buttonLayout.set_sensitive(False) + + def _hide_loading(self): + self._loadingBanner.hide() + self._buttonLayout.set_sensitive(True) + + def _refresh(self): + self._show_loading() + self._index.get_languages(self._on_languages, self._on_error) + + def _create_button(self, icon, message): + image = self._store.get_image_from_store(self._store.STORE_LOOKUP[icon]) + + label = gtk.Label() + label.set_text(message) + + buttonLayout = gtk.HBox(False, 5) + buttonLayout.pack_start(image, False, False) + buttonLayout.pack_start(label, False, True) + button = gtk.Button() + button.add(buttonLayout) + + return button + + @misc_utils.log_exception(_moduleLogger) + def _on_languages(self, languages): + self._hide_loading() + self._languages = list(languages) + + @misc_utils.log_exception(_moduleLogger) + def _on_error(self, exception): + self._hide_loading() + self._errorBanner.push_message(str(exception)) + + def _window_from_node(self, node): + if node.id == stream_index.SOURCE_RADIO: + Source = windows.radio.RadioWindow + elif node.id == stream_index.SOURCE_CONFERENCES: + Source = windows.conferences.ConferencesWindow + elif node.id == stream_index.SOURCE_MAGAZINES: + pass + elif node.id == stream_index.SOURCE_SCRIPTURES: + pass + sourceWindow = Source(self._player, self._store, node) + sourceWindow.window.set_modal(True) + sourceWindow.window.set_transient_for(self._window) + sourceWindow.window.set_default_size(*self._window.get_size()) + sourceWindow.connect("quit", self._on_quit) + sourceWindow.connect("jump-to", self._on_jump) + sourceWindow.show() + return sourceWindow + + @misc_utils.log_exception(_moduleLogger) + def _on_jump(self, source, node): + targetNodePath = list(reversed(list(stream_index.walk_ancestors(node)))) + ancestor = targetNodePath[0] + window = self._window_from_node(ancestor) + window.jump_to(node) + + @misc_utils.log_exception(_moduleLogger) + def _on_source_selected(self, widget, nodeName): + node = self._index.get_source(nodeName, self._languages[0]["id"]) + self._window_from_node(node) + + +gobject.type_register(SourceSelector) + -- 1.7.9.5