Move the engine load widget into the toolbar.
And split it into separate classes for tracking the engine state and rendering it (so there can be multiple widgets of the latter without replicating the state tracking).time
parent
dec49c3db4
commit
4d0c2df377
|
@ -195,18 +195,6 @@ class ScoreToolBase(measured_track_editor.MeasuredToolBase):
|
|||
|
||||
self._updateGhost(measure, evt.pos())
|
||||
|
||||
# ymid = target.height() // 2
|
||||
# stave_line = (
|
||||
# int(ymid + 5 - evt.pos().y()) // 10 + target.measure.clef.center_pitch.stave_line)
|
||||
|
||||
# idx, _, _ = target.getEditArea(evt.pos().x())
|
||||
# if idx < 0:
|
||||
# self.editor_window.setInfoMessage('')
|
||||
# else:
|
||||
# pitch = value_types.Pitch.name_from_stave_line(
|
||||
# stave_line, target.measure.key_signature)
|
||||
# self.editor_window.setInfoMessage(pitch)
|
||||
|
||||
super().mouseMoveMeasureEvent(measure, evt)
|
||||
|
||||
|
||||
|
|
|
@ -40,8 +40,8 @@ from . import project_debugger
|
|||
from . import ui_base
|
||||
from . import qprogressindicator
|
||||
from . import project_registry as project_registry_lib
|
||||
from . import load_history
|
||||
from . import open_project_dialog
|
||||
from . import engine_state as engine_state_lib
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from noisicaa import core
|
||||
|
@ -87,10 +87,16 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
|
|||
currentPageChanged = QtCore.pyqtSignal(QtWidgets.QWidget)
|
||||
hasProjectView = QtCore.pyqtSignal(bool)
|
||||
|
||||
def __init__(self, parent: QtWidgets.QTabWidget, **kwargs: Any) -> None:
|
||||
def __init__(
|
||||
self, *,
|
||||
parent: QtWidgets.QTabWidget,
|
||||
engine_state: engine_state_lib.EngineState,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self.__tab_widget = parent
|
||||
self.__engine_state = engine_state
|
||||
|
||||
self.__page = None # type: QtWidgets.QWidget
|
||||
self.__page_cleanup_func = None # type: Callable[[], None]
|
||||
|
@ -198,7 +204,10 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
|
|||
exc, "Failed to open project \"%s\"." % project.name)
|
||||
|
||||
else:
|
||||
view = project_view.ProjectView(project_connection=project, context=self.context)
|
||||
view = project_view.ProjectView(
|
||||
project_connection=project,
|
||||
engine_state=self.__engine_state,
|
||||
context=self.context)
|
||||
view.setObjectName('project-view')
|
||||
await view.setup()
|
||||
self.setProjectView(project.name, view)
|
||||
|
@ -217,7 +226,10 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
|
|||
|
||||
else:
|
||||
await self.app.project_registry.refresh()
|
||||
view = project_view.ProjectView(project_connection=project, context=self.context)
|
||||
view = project_view.ProjectView(
|
||||
project_connection=project,
|
||||
engine_state=self.__engine_state,
|
||||
context=self.context)
|
||||
view.setObjectName('project-view')
|
||||
await view.setup()
|
||||
self.setProjectView(project.name, view)
|
||||
|
@ -236,7 +248,10 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
|
|||
|
||||
else:
|
||||
await self.app.project_registry.refresh()
|
||||
view = project_view.ProjectView(project_connection=project, context=self.context)
|
||||
view = project_view.ProjectView(
|
||||
project_connection=project,
|
||||
engine_state=self.__engine_state,
|
||||
context=self.context)
|
||||
view.setObjectName('project-view')
|
||||
await view.setup()
|
||||
self.setProjectView(project.name, view)
|
||||
|
@ -296,6 +311,7 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
|
|||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__engine_state = engine_state_lib.EngineState(self)
|
||||
self.__engine_state_listener = None # type: core.Listener[audioproc.EngineStateChange]
|
||||
|
||||
self.setWindowTitle("noisicaä")
|
||||
|
@ -306,7 +322,6 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
|
|||
|
||||
self.createActions()
|
||||
self.createMenus()
|
||||
self.createStatusBar()
|
||||
|
||||
self.__project_tabs = QtWidgets.QTabWidget(self)
|
||||
self.__project_tabs.setObjectName('project-tabs')
|
||||
|
@ -358,7 +373,7 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
|
|||
|
||||
def audioprocReady(self) -> None:
|
||||
self.__engine_state_listener = self.audioproc_client.engine_state_changed.add(
|
||||
self.__engineStateChanged)
|
||||
self.__engine_state.updateState)
|
||||
|
||||
def createSetupProgress(self) -> SetupProgressWidget:
|
||||
assert self.__setup_progress is None
|
||||
|
@ -394,7 +409,10 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
|
|||
self.__setup_progress_fade_task = None
|
||||
|
||||
def addProjectTab(self) -> ProjectTabPage:
|
||||
page = ProjectTabPage(parent=self.__project_tabs, context=self.context)
|
||||
page = ProjectTabPage(
|
||||
parent=self.__project_tabs,
|
||||
engine_state=self.__engine_state,
|
||||
context=self.context)
|
||||
idx = self.__project_tabs.addTab(page, '')
|
||||
self.__project_tabs.setCurrentIndex(idx)
|
||||
return page
|
||||
|
@ -475,48 +493,11 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
|
|||
self._help_menu.addAction(self.app.about_action)
|
||||
self._help_menu.addAction(self.app.aboutqt_action)
|
||||
|
||||
def createStatusBar(self) -> None:
|
||||
self.statusbar = QtWidgets.QStatusBar()
|
||||
|
||||
self.pipeline_load = load_history.LoadHistoryWidget(100, 30)
|
||||
self.pipeline_load.setToolTip("Load of the playback engine.")
|
||||
self.statusbar.addPermanentWidget(self.pipeline_load)
|
||||
|
||||
self.pipeline_status = QtWidgets.QLabel()
|
||||
self.statusbar.addPermanentWidget(self.pipeline_status)
|
||||
|
||||
self.setStatusBar(self.statusbar)
|
||||
|
||||
def storeState(self) -> None:
|
||||
logger.info("Saving current EditorWindow geometry.")
|
||||
self.app.settings.setValue('mainwindow/geometry', self.saveGeometry())
|
||||
self.app.settings.setValue('mainwindow/state', self.saveState())
|
||||
|
||||
def __engineStateChanged(self, engine_state: audioproc.EngineStateChange) -> None:
|
||||
show_status, show_load = False, False
|
||||
if engine_state.state == audioproc.EngineStateChange.SETUP:
|
||||
self.pipeline_status.setText("Starting engine...")
|
||||
show_status = True
|
||||
elif engine_state.state == audioproc.EngineStateChange.CLEANUP:
|
||||
self.pipeline_status.setText("Stopping engine...")
|
||||
show_status = True
|
||||
elif engine_state.state == audioproc.EngineStateChange.RUNNING:
|
||||
if engine_state.HasField('load'):
|
||||
self.pipeline_load.addValue(engine_state.load)
|
||||
show_load = True
|
||||
else:
|
||||
self.pipeline_status.setText("Engine running")
|
||||
show_status = True
|
||||
elif engine_state.state == audioproc.EngineStateChange.STOPPED:
|
||||
self.pipeline_status.setText("Engine stopped")
|
||||
show_status = True
|
||||
|
||||
self.pipeline_status.setVisible(show_status)
|
||||
self.pipeline_load.setVisible(show_load)
|
||||
|
||||
def setInfoMessage(self, msg: str) -> None:
|
||||
self.statusbar.showMessage(msg)
|
||||
|
||||
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
|
||||
logger.info("CloseEvent received")
|
||||
event.ignore()
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
# @end:license
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import math
|
||||
import typing
|
||||
from typing import Any, List
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import audioproc
|
||||
from . import slots
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from noisicaa import core
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EngineState(slots.SlotContainer, QtCore.QObject):
|
||||
class State(enum.IntEnum):
|
||||
Setup = 0
|
||||
Cleanup = 1
|
||||
Running = 2
|
||||
Stopped = 3
|
||||
|
||||
state, setState, stateChanged = slots.slot(
|
||||
State, 'state', default=State.Stopped)
|
||||
currentLoad, setCurrentLoad, currentLoadChanged = slots.slot(
|
||||
float, 'currentLoad', default=0.0)
|
||||
loadHistoryChanged = QtCore.pyqtSignal()
|
||||
|
||||
HISTORY_LENGTH = 1000
|
||||
|
||||
def __init__(self, parent: QtCore.QObject, **kwargs: Any) -> None:
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self.__history = [None] * self.HISTORY_LENGTH # type: List[float]
|
||||
self.__latest_values = [] # type: List[float]
|
||||
self.currentLoadChanged.connect(self.__latest_values.append)
|
||||
|
||||
self.__timer = QtCore.QTimer(self)
|
||||
self.__timer.setInterval(1000 // 25)
|
||||
self.__timer.timeout.connect(self.__updateHistory)
|
||||
self.__timer.start()
|
||||
|
||||
def updateState(self, msg: audioproc.EngineStateChange) -> None:
|
||||
self.setState({
|
||||
audioproc.EngineStateChange.SETUP: self.State.Setup,
|
||||
audioproc.EngineStateChange.CLEANUP: self.State.Cleanup,
|
||||
audioproc.EngineStateChange.RUNNING: self.State.Running,
|
||||
audioproc.EngineStateChange.STOPPED: self.State.Stopped,
|
||||
}[msg.state])
|
||||
|
||||
if msg.state == audioproc.EngineStateChange.RUNNING and msg.HasField('load'):
|
||||
self.setCurrentLoad(msg.load)
|
||||
|
||||
def loadHistory(self, num_ticks: int) -> List[float]:
|
||||
num_ticks = min(num_ticks, self.HISTORY_LENGTH)
|
||||
return self.__history[-num_ticks:]
|
||||
|
||||
def __updateHistory(self) -> None:
|
||||
if self.__latest_values:
|
||||
self.__history.append(max(self.__latest_values))
|
||||
self.__latest_values.clear()
|
||||
elif self.state() == self.State.Running:
|
||||
self.__history.append(self.currentLoad())
|
||||
else:
|
||||
self.__history.append(None)
|
||||
if len(self.__history) > self.HISTORY_LENGTH:
|
||||
del self.__history[:-self.HISTORY_LENGTH]
|
||||
self.loadHistoryChanged.emit()
|
||||
|
||||
|
||||
class LoadHistory(QtWidgets.QWidget):
|
||||
def __init__(self, parent: QtWidgets.QWidget, engine_state: EngineState) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.__font = QtGui.QFont(self.font())
|
||||
self.__font.setPixelSize(12)
|
||||
|
||||
self.__engine_state = engine_state
|
||||
self.__engine_state.loadHistoryChanged.connect(self.update)
|
||||
|
||||
def paintEvent(self, evt: QtGui.QPaintEvent) -> None:
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
painter.fillRect(self.rect(), QtGui.QColor(0, 0, 0))
|
||||
|
||||
history = self.__engine_state.loadHistory(max(25, self.width() // 2))
|
||||
x = self.width() - 2
|
||||
for value in reversed(history):
|
||||
if value is not None:
|
||||
value = max(0.0, min(value, 1.0))
|
||||
vh = int(self.height() * value)
|
||||
painter.fillRect(
|
||||
x, self.height() - vh, 2, vh,
|
||||
QtGui.QColor(int(255 * value), 255 - int(255 * value), 0))
|
||||
x -= 2
|
||||
|
||||
if self.width() > 50 and self.height() > 16:
|
||||
last_second = [v for v in history[-25:] if v is not None]
|
||||
if len(last_second) > 5:
|
||||
avg = sum(last_second) / len(last_second)
|
||||
stddev = math.sqrt(sum((v - avg) ** 2 for v in last_second) / len(last_second))
|
||||
|
||||
painter.setPen(Qt.white)
|
||||
painter.setFont(self.__font)
|
||||
painter.drawText(
|
||||
4, 1, self.width() - 4, self.height() - 1,
|
||||
Qt.AlignTop,
|
||||
"%d\u00b1%d%%" % (100 * avg, 100 * stddev))
|
||||
|
||||
return super().paintEvent(evt)
|
|
@ -1,79 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
# @end:license
|
||||
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
|
||||
|
||||
class LoadHistoryWidget(QtWidgets.QWidget):
|
||||
def __init__(self, width, height):
|
||||
super().__init__()
|
||||
|
||||
self.__width = width
|
||||
self.__height = height
|
||||
self.setFixedSize(self.__width, self.__height)
|
||||
|
||||
self.__pixmap = QtGui.QPixmap(self.__width, self.__height)
|
||||
self.__pixmap.fill(Qt.black)
|
||||
|
||||
self.__history = [] # type: List[float]
|
||||
|
||||
self.__font = QtGui.QFont("Helvetica")
|
||||
self.__font.setPixelSize(12)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.drawPixmap(0, 0, self.__pixmap)
|
||||
|
||||
if len(self.__history) > 5:
|
||||
avg = sum(self.__history) / len(self.__history)
|
||||
stddev = math.sqrt(sum((v - avg) ** 2 for v in self.__history) / len(self.__history))
|
||||
|
||||
painter.setPen(Qt.white)
|
||||
painter.setFont(self.__font)
|
||||
painter.drawText(
|
||||
4, 1, self.__width - 4, self.__height - 1,
|
||||
Qt.AlignTop,
|
||||
"%d\u00b1%d%%" % (avg, stddev))
|
||||
|
||||
return super().paintEvent(event)
|
||||
|
||||
def addValue(self, value):
|
||||
value = max(0, min(value, 1))
|
||||
vh = int(self.__height * value)
|
||||
|
||||
self.__pixmap.scroll(-2, 0, 0, 0, self.__width, self.__height)
|
||||
painter = QtGui.QPainter(self.__pixmap)
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.setBrush(Qt.black)
|
||||
painter.drawRect(self.__width - 2, 0, 2, self.__height)
|
||||
painter.setBrush(QtGui.QColor(int(255 * value), 255 - int(255 * value), 0))
|
||||
painter.drawRect(self.__width - 2, self.__height - vh, 2, vh)
|
||||
|
||||
self.__history.append(100 * value)
|
||||
if len(self.__history) > 50:
|
||||
del self.__history[:-50]
|
||||
|
||||
self.update()
|
|
@ -42,6 +42,7 @@ from .track_list import view as track_list_view
|
|||
from . import player_state as player_state_lib
|
||||
from . import vumeter
|
||||
from . import slots
|
||||
from . import engine_state as engine_state_lib
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from noisicaa import core
|
||||
|
@ -109,6 +110,7 @@ class ProjectView(ui_base.AbstractProjectView):
|
|||
def __init__(
|
||||
self, *,
|
||||
project_connection: project_registry.Project,
|
||||
engine_state: engine_state_lib.EngineState,
|
||||
context: ui_base.CommonContext,
|
||||
**kwargs: Any) -> None:
|
||||
context = ui_base.ProjectContext(
|
||||
|
@ -117,6 +119,8 @@ class ProjectView(ui_base.AbstractProjectView):
|
|||
app=context.app)
|
||||
super().__init__(parent=None, context=context, **kwargs)
|
||||
|
||||
self.__engine_state = engine_state
|
||||
|
||||
self.__session_prefix = 'project-view:%016x:' % self.project.id
|
||||
|
||||
self.__player_id = None # type: str
|
||||
|
@ -156,6 +160,9 @@ class ProjectView(ui_base.AbstractProjectView):
|
|||
self.__vumeter = vumeter.VUMeter(self)
|
||||
self.__vumeter.setMinimumWidth(250)
|
||||
|
||||
self.__engine_load = engine_state_lib.LoadHistory(self, self.__engine_state)
|
||||
self.__engine_load.setFixedWidth(100)
|
||||
|
||||
self.__toggle_playback_button = QtWidgets.QToolButton(self)
|
||||
self.__toggle_playback_button.setDefaultAction(self.__player_state.togglePlaybackAction())
|
||||
self.__toggle_playback_button.setIconSize(QtCore.QSize(54, 54))
|
||||
|
@ -213,6 +220,8 @@ class ProjectView(ui_base.AbstractProjectView):
|
|||
tb_layout.addWidget(self.__vumeter, 0, c, 2, 1)
|
||||
c += 1
|
||||
tb_layout.setColumnStretch(c, 1)
|
||||
c += 1
|
||||
tb_layout.addWidget(self.__engine_load, 0, c, 2, 1)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
|
@ -34,12 +34,12 @@ def build(ctx):
|
|||
ctx.py_module('editor_app.py')
|
||||
ctx.py_test('editor_app_test.py')
|
||||
ctx.py_module('editor_window.py')
|
||||
ctx.py_module('engine_state.py')
|
||||
ctx.py_module('flowlayout.py', mypy='loose')
|
||||
ctx.py_module('gain_slider.py')
|
||||
ctx.py_module('instrument_library.py')
|
||||
ctx.py_module('instrument_list.py')
|
||||
ctx.py_test('instrument_list_test.py')
|
||||
ctx.py_module('load_history.py', mypy='loose')
|
||||
ctx.py_module('misc.py')
|
||||
ctx.py_module('mute_button.py')
|
||||
ctx.py_module('object_list_editor.py')
|
||||
|
|
Loading…
Reference in New Issue