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
Ben Niemann 2020-02-22 11:01:42 +01:00
parent dec49c3db4
commit 4d0c2df377
6 changed files with 173 additions and 137 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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')