Each ProjectView has its own toolbar. PlayerState owns the actions to control the player.

time
Ben Niemann 2020-02-21 15:00:25 +01:00
parent e9518d6635
commit 9e298f3966
3 changed files with 143 additions and 208 deletions

View File

@ -23,7 +23,6 @@
import asyncio
import contextlib
import logging
import os.path
import time
import traceback
import typing
@ -34,7 +33,6 @@ from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import constants
from noisicaa.core import storage
from noisicaa import audioproc
from . import project_view
@ -295,19 +293,11 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
# Could not figure out how to define a signal that takes either an instance
# of a specific class or None.
currentProjectChanged = QtCore.pyqtSignal(object)
playingChanged = QtCore.pyqtSignal(bool)
loopEnabledChanged = QtCore.pyqtSignal(bool)
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__engine_state_listener = None # type: core.Listener[audioproc.EngineStateChange]
self.__current_project_view = None # type: Optional[project_view.ProjectView]
self.setWindowTitle("noisicaä")
self.resize(1200, 800)
@ -316,12 +306,8 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
self.createActions()
self.createMenus()
self.createToolBar()
self.createStatusBar()
self.playingChanged.connect(self.onPlayingChanged)
self.loopEnabledChanged.connect(self.onLoopEnabledChanged)
self.__project_tabs = QtWidgets.QTabWidget(self)
self.__project_tabs.setObjectName('project-tabs')
self.__project_tabs.setTabBarAutoHide(True)
@ -331,7 +317,6 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
self.__project_tabs.setDocumentMode(True)
self.__project_tabs.tabCloseRequested.connect(
lambda idx: self.call_async(self.onCloseProjectTab(idx)))
self.__project_tabs.currentChanged.connect(self.onCurrentProjectTabChanged)
self.__main_layout = QtWidgets.QVBoxLayout()
self.__main_layout.setContentsMargins(0, 0, 0, 0)
@ -410,7 +395,6 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
def addProjectTab(self) -> ProjectTabPage:
page = ProjectTabPage(parent=self.__project_tabs, context=self.context)
page.hasProjectView.connect(self.onCurrentProjectTabChanged)
idx = self.__project_tabs.addTab(page, '')
self.__project_tabs.setCurrentIndex(idx)
return page
@ -444,58 +428,6 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
self._set_bpm_action.setStatusTip("Set the project's beats per second")
self._set_bpm_action.triggered.connect(self.onSetBPM)
self.__player_actions = []
self._player_move_to_start_action = QtWidgets.QAction("Move to start", self)
self._player_move_to_start_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-skip-backward.svg')))
self._player_move_to_start_action.setShortcut(QtGui.QKeySequence('Home'))
self._player_move_to_start_action.setShortcutContext(Qt.ApplicationShortcut)
self._player_move_to_start_action.triggered.connect(lambda: self.onPlayerMoveTo('start'))
self.__player_actions.append(self._player_move_to_start_action)
self._player_move_to_end_action = QtWidgets.QAction("Move to end", self)
self._player_move_to_end_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-skip-forward.svg')))
self._player_move_to_end_action.setShortcut(QtGui.QKeySequence('End'))
self._player_move_to_end_action.setShortcutContext(Qt.ApplicationShortcut)
self._player_move_to_end_action.triggered.connect(lambda: self.onPlayerMoveTo('end'))
self.__player_actions.append(self._player_move_to_end_action)
self._player_move_to_prev_action = QtWidgets.QAction("Move to previous measure", self)
self._player_move_to_prev_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-seek-backward.svg')))
self._player_move_to_prev_action.setShortcut(QtGui.QKeySequence('PgUp'))
self._player_move_to_prev_action.setShortcutContext(Qt.ApplicationShortcut)
self._player_move_to_prev_action.triggered.connect(lambda: self.onPlayerMoveTo('prev'))
self.__player_actions.append(self._player_move_to_prev_action)
self._player_move_to_next_action = QtWidgets.QAction("Move to next measure", self)
self._player_move_to_next_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-seek-forward.svg')))
self._player_move_to_next_action.setShortcut(QtGui.QKeySequence('PgDown'))
self._player_move_to_next_action.setShortcutContext(Qt.ApplicationShortcut)
self._player_move_to_next_action.triggered.connect(lambda: self.onPlayerMoveTo('next'))
self.__player_actions.append(self._player_move_to_next_action)
self._player_toggle_action = QtWidgets.QAction("Play", self)
self._player_toggle_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-playback-start.svg')))
self._player_toggle_action.setShortcut(QtGui.QKeySequence('Space'))
self._player_toggle_action.setShortcutContext(Qt.ApplicationShortcut)
self._player_toggle_action.triggered.connect(self.onPlayerToggle)
self.__player_actions.append(self._player_toggle_action)
self._player_loop_action = QtWidgets.QAction("Loop playback", self)
self._player_loop_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-playlist-repeat.svg')))
self._player_loop_action.setCheckable(True)
self._player_loop_action.toggled.connect(self.onPlayerLoop)
self.__player_actions.append(self._player_loop_action)
for action in self.__player_actions:
action.setEnabled(False)
def createMenus(self) -> None:
menu_bar = self.menuBar()
@ -543,19 +475,6 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
self._help_menu.addAction(self.app.about_action)
self._help_menu.addAction(self.app.aboutqt_action)
def createToolBar(self) -> None:
self.toolbar = QtWidgets.QToolBar()
self.toolbar.setObjectName('toolbar:main')
self.toolbar.addAction(self._player_toggle_action)
self.toolbar.addAction(self._player_loop_action)
self.toolbar.addSeparator()
self.toolbar.addAction(self._player_move_to_start_action)
#self.toolbar.addAction(self._player_move_to_prev_action)
#self.toolbar.addAction(self._player_move_to_next_action)
self.toolbar.addAction(self._player_move_to_end_action)
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
def createStatusBar(self) -> None:
self.statusbar = QtWidgets.QStatusBar()
@ -603,30 +522,6 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
event.ignore()
self.call_async(self.app.deleteWindow(self))
def setCurrentProjectView(self, view: Optional[project_view.ProjectView]) -> None:
if view == self.__current_project_view:
return
if self.__current_project_view is not None:
self.__current_project_view.playingChanged.disconnect(self.playingChanged)
self.__current_project_view.loopEnabledChanged.disconnect(self.loopEnabledChanged)
if view is not None:
view.playingChanged.connect(self.playingChanged)
self.playingChanged.emit(view.playing())
view.loopEnabledChanged.connect(self.loopEnabledChanged)
self.loopEnabledChanged.emit(view.loopEnabled())
self.__current_project_view = view
for action in self.__player_actions:
action.setEnabled(view is not None)
if view is not None:
self.currentProjectChanged.emit(view.project)
else:
self.currentProjectChanged.emit(None)
def onCloseCurrentProject(self) -> None:
idx = self.__project_tabs.currentIndex()
tab = cast(ProjectTabPage, self.__project_tabs.widget(idx))
@ -637,10 +532,6 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
if self.__project_tabs.count() > 1:
self.__project_tabs.removeTab(idx)
def onCurrentProjectTabChanged(self) -> None:
tab = cast(ProjectTabPage, self.__project_tabs.currentWidget())
self.setCurrentProjectView(tab.projectView() if tab is not None else None)
async def onCloseProjectTab(self, idx: int) -> None:
tab = cast(ProjectTabPage, self.__project_tabs.widget(idx))
if tab.projectView() is not None:
@ -677,29 +568,3 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
view = self.getCurrentProjectView()
if view is not None:
view.onSetBPM()
def onPlayingChanged(self, playing: bool) -> None:
if playing:
self._player_toggle_action.setIcon(
QtGui.QIcon(os.path.join(constants.DATA_DIR, 'icons', 'media-playback-pause.svg')))
else:
self._player_toggle_action.setIcon(
QtGui.QIcon(os.path.join(constants.DATA_DIR, 'icons', 'media-playback-start.svg')))
def onLoopEnabledChanged(self, loop_enabled: bool) -> None:
self._player_loop_action.setChecked(loop_enabled)
def onPlayerMoveTo(self, where: str) -> None:
view = self.getCurrentProjectView()
if view is not None:
view.onPlayerMoveTo(where)
def onPlayerToggle(self) -> None:
view = self.getCurrentProjectView()
if view is not None:
view.onPlayerToggle()
def onPlayerLoop(self, loop: bool) -> None:
view = self.getCurrentProjectView()
if view is not None:
view.onPlayerLoop(loop)

View File

@ -22,11 +22,16 @@
import enum
import logging
import os.path
import time as time_lib
from typing import Any
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import constants
from noisicaa import audioproc
from noisicaa.audioproc.public import musical_time_pb2
from . import ui_base
@ -39,6 +44,13 @@ class TimeMode(enum.Enum):
Manual = 1
class MoveTo(enum.Enum):
Start = 0
End = 1
PrevBeat = 2
NextBeat = 3
class PlayerState(ui_base.ProjectMixin, QtCore.QObject):
playingChanged = QtCore.pyqtSignal(bool)
currentTimeChanged = QtCore.pyqtSignal(object)
@ -62,6 +74,47 @@ class PlayerState(ui_base.ProjectMixin, QtCore.QObject):
self.__player_id = None # type: str
self.__move_to_start_action = QtWidgets.QAction("Move to start", self)
self.__move_to_start_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-skip-backward.svg')))
self.__move_to_start_action.setShortcut(QtGui.QKeySequence('Home'))
self.__move_to_start_action.setShortcutContext(Qt.ApplicationShortcut)
self.__move_to_start_action.triggered.connect(lambda: self.__onMoveTo(MoveTo.Start))
self.__move_to_end_action = QtWidgets.QAction("Move to end", self)
self.__move_to_end_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-skip-forward.svg')))
self.__move_to_end_action.setShortcut(QtGui.QKeySequence('End'))
self.__move_to_end_action.setShortcutContext(Qt.ApplicationShortcut)
self.__move_to_end_action.triggered.connect(lambda: self.__onMoveTo(MoveTo.End))
self.__move_to_prev_action = QtWidgets.QAction("Move to previous beat", self)
self.__move_to_prev_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-seek-backward.svg')))
self.__move_to_prev_action.setShortcut(QtGui.QKeySequence('PgUp'))
self.__move_to_prev_action.setShortcutContext(Qt.ApplicationShortcut)
self.__move_to_prev_action.triggered.connect(lambda: self.__onMoveTo(MoveTo.PrevBeat))
self.__move_to_next_action = QtWidgets.QAction("Move to next beat", self)
self.__move_to_next_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-seek-forward.svg')))
self.__move_to_next_action.setShortcut(QtGui.QKeySequence('PgDown'))
self.__move_to_next_action.setShortcutContext(Qt.ApplicationShortcut)
self.__move_to_next_action.triggered.connect(lambda: self.__onMoveTo(MoveTo.NextBeat))
self.__toggle_action = QtWidgets.QAction("Play", self)
self.__toggle_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-playback-start.svg')))
self.__toggle_action.setShortcut(QtGui.QKeySequence('Space'))
self.__toggle_action.setShortcutContext(Qt.ApplicationShortcut)
self.__toggle_action.triggered.connect(self.__onToggle)
self.__loop_action = QtWidgets.QAction("Loop playback", self)
self.__loop_action.setIcon(QtGui.QIcon(
os.path.join(constants.DATA_DIR, 'icons', 'media-playlist-repeat.svg')))
self.__loop_action.setCheckable(True)
self.__loop_action.toggled.connect(self.__onLoop)
def __get_session_value(self, key: str, default: Any) -> Any:
return self.get_session_value(self.__session_prefix + key, default)
@ -90,6 +143,13 @@ class PlayerState(ui_base.ProjectMixin, QtCore.QObject):
if player_state.HasField('loop_end_time'):
self.setLoopEndTime(audioproc.MusicalTime.from_proto(player_state.loop_end_time))
def populateToolBar(self, toolbar: QtWidgets.QToolBar) -> None:
toolbar.addAction(self.__toggle_action)
toolbar.addAction(self.__loop_action)
toolbar.addSeparator()
toolbar.addAction(self.__move_to_start_action)
toolbar.addAction(self.__move_to_end_action)
def setTimeMode(self, mode: TimeMode) -> None:
self.__time_mode = mode
@ -97,6 +157,13 @@ class PlayerState(ui_base.ProjectMixin, QtCore.QObject):
if playing == self.__playing:
return
if playing:
self.__toggle_action.setIcon(
QtGui.QIcon(os.path.join(constants.DATA_DIR, 'icons', 'media-playback-pause.svg')))
else:
self.__toggle_action.setIcon(
QtGui.QIcon(os.path.join(constants.DATA_DIR, 'icons', 'media-playback-start.svg')))
self.__playing = playing
self.playingChanged.emit(playing)
@ -159,9 +226,77 @@ class PlayerState(ui_base.ProjectMixin, QtCore.QObject):
if loop_enabled == self.__loop_enabled:
return
self.__loop_action.setChecked(loop_enabled)
self.__loop_enabled = loop_enabled
self.__set_session_value('loop_enabled', loop_enabled)
self.loopEnabledChanged.emit(loop_enabled)
def loopEnabled(self) -> bool:
return self.__loop_enabled
def __onMoveTo(self, where: MoveTo) -> None:
if self.__player_id is None:
logger.warning("Player action without active player.")
return
new_time = None
if where == MoveTo.Start:
new_time = audioproc.MusicalTime()
elif where == MoveTo.End:
new_time = self.time_mapper.end_time
elif where == MoveTo.PrevBeat:
raise NotImplementedError
# measure_start_time = audioproc.MusicalTime()
# current_time = self.__player_state.currentTime()
# for mref in self.project.property_track.measure_list:
# measure = mref.measure
# if measure_start_time <= current_time < (measure_start_time + measure.duration
# + audioproc.MusicalDuration(1, 16)):
# new_time = measure_start_time
# break
# measure_start_time += measure.duration
elif where == MoveTo.NextBeat:
raise NotImplementedError
# measure_start_time = audioproc.MusicalTime()
# current_time = self.__player_state.currentTime()
# for mref in self.project.property_track.measure_list:
# measure = mref.measure
# if measure_start_time <= current_time < measure_start_time + measure.duration:
# new_time = measure_start_time + measure.duration
# break
# measure_start_time += measure.duration
else:
raise ValueError(where)
if new_time is not None:
self.call_async(
self.project_client.update_player_state(
self.__player_id,
audioproc.PlayerState(current_time=new_time.to_proto())))
def __onToggle(self) -> None:
if self.__player_id is None:
logger.warning("Player action without active player.")
return
self.call_async(
self.project_client.update_player_state(
self.__player_id,
audioproc.PlayerState(playing=not self.__playing)))
def __onLoop(self, loop: bool) -> None:
if self.__player_id is None:
logger.warning("Player action without active player.")
return
self.call_async(
self.project_client.update_player_state(
self.__player_id,
audioproc.PlayerState(loop_enabled=loop)))

View File

@ -69,8 +69,6 @@ class ProjectView(ui_base.AbstractProjectView):
self.__player_status_listener = None # type: core.Listener
self.__player_state = player_state_lib.PlayerState(context=self.context)
self.__player_state.playingChanged.connect(self.playingChanged)
self.__player_state.loopEnabledChanged.connect(self.loopEnabledChanged)
self.__track_list = track_list_view.TrackListView(
project_view=self, player_state=self.__player_state,
@ -88,8 +86,14 @@ class ProjectView(ui_base.AbstractProjectView):
self.__splitter.setCollapsible(0, False)
self.__splitter.addWidget(self.__graph)
self.__toolbar = QtWidgets.QToolBar(self)
self.__toolbar.setObjectName('toolbar:%016x' % self.project.id)
self.__player_state.populateToolBar(self.__toolbar)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.__toolbar)
layout.addWidget(self.__splitter)
self.setLayout(layout)
@ -150,11 +154,8 @@ class ProjectView(ui_base.AbstractProjectView):
self.__track_list.cleanup()
def playing(self) -> bool:
return self.__player_state.playing()
def loopEnabled(self) -> bool:
return self.__player_state.loopEnabled()
def playerState(self) -> player_state_lib.PlayerState:
return self.__player_state
async def createPluginUI(self, node_id: str) -> Tuple[int, Tuple[int, int]]:
return await self.project_client.create_plugin_ui(self.__player_id, node_id)
@ -166,72 +167,6 @@ class ProjectView(ui_base.AbstractProjectView):
await self.audioproc_client.send_node_messages(
self.__player_realm, audioproc.ProcessorMessageList(messages=[msg]))
def onPlayerMoveTo(self, where: str) -> None:
if self.__player_id is None:
logger.warning("Player action without active player.")
return
new_time = None
if where == 'start':
new_time = audioproc.MusicalTime()
elif where == 'end':
new_time = self.time_mapper.end_time
elif where == 'prev':
raise NotImplementedError
# measure_start_time = audioproc.MusicalTime()
# current_time = self.__player_state.currentTime()
# for mref in self.project.property_track.measure_list:
# measure = mref.measure
# if measure_start_time <= current_time < (measure_start_time + measure.duration
# + audioproc.MusicalDuration(1, 16)):
# new_time = measure_start_time
# break
# measure_start_time += measure.duration
elif where == 'next':
raise NotImplementedError
# measure_start_time = audioproc.MusicalTime()
# current_time = self.__player_state.currentTime()
# for mref in self.project.property_track.measure_list:
# measure = mref.measure
# if measure_start_time <= current_time < measure_start_time + measure.duration:
# new_time = measure_start_time + measure.duration
# break
# measure_start_time += measure.duration
else:
raise ValueError(where)
if new_time is not None:
self.call_async(
self.project_client.update_player_state(
self.__player_id,
audioproc.PlayerState(current_time=new_time.to_proto())))
def onPlayerToggle(self) -> None:
if self.__player_id is None:
logger.warning("Player action without active player.")
return
self.call_async(
self.project_client.update_player_state(
self.__player_id,
audioproc.PlayerState(playing=not self.__player_state.playing())))
def onPlayerLoop(self, loop: bool) -> None:
if self.__player_id is None:
logger.warning("Player action without active player.")
return
self.call_async(
self.project_client.update_player_state(
self.__player_id,
audioproc.PlayerState(loop_enabled=loop)))
def onRender(self) -> None:
dialog = render_dialog.RenderDialog(parent=self, context=self.context)
dialog.setModal(True)