diff --git a/friture/FritureHost.qml b/friture/FritureHost.qml new file mode 100644 index 00000000..d814e06a --- /dev/null +++ b/friture/FritureHost.qml @@ -0,0 +1,11 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.2 +import Friture 1.0 + +Rectangle { + id: mainWindow + SystemPalette { id: systemPalette; colorGroup: SystemPalette.Active } + color: systemPalette.window + anchors.fill: parent +} \ No newline at end of file diff --git a/friture/Levels.qml b/friture/Levels.qml index b5757dec..ced45c65 100644 --- a/friture/Levels.qml +++ b/friture/Levels.qml @@ -8,13 +8,8 @@ Rectangle { SystemPalette { id: systemPalette; colorGroup: SystemPalette.Active } color: systemPalette.window - property var stateId - property LevelViewModel level_view_model: Store.dock_states[stateId] - - property string fixedFont - - // parent here will be unset on exit - height: parent ? parent.height : 0 + required property LevelViewModel level_view_model + required property string fixedFont // make width dependent on the text labels // but do not bind directly to their widths diff --git a/friture/MainWindow.qml b/friture/MainWindow.qml new file mode 100644 index 00000000..3a6b4dbe --- /dev/null +++ b/friture/MainWindow.qml @@ -0,0 +1,56 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.2 +import Friture 1.0 +import "./playback" + +Rectangle { + id: main_window + SystemPalette { id: systemPalette; colorGroup: SystemPalette.Active } + color: systemPalette.window + anchors.fill: parent + + required property MainWindowViewModel main_window_view_model + required property string fixedFont + + GridLayout { + objectName: "main_row_layout" + anchors.fill: parent + rows: main_window.main_window_view_model.playback_control_enabled ? 2 : 1 + columns: 2 + rowSpacing: 3 + columnSpacing: 3 + + Levels { + level_view_model: main_window.main_window_view_model.level_view_model + Layout.row: 0 + Layout.rowSpan: main_window.main_window_view_model.playback_control_enabled ? 2 : 1 + Layout.column: 0 + Layout.fillHeight: true + Layout.margins: 5 + fixedFont: main_window.fixedFont + } + + TileLayout { + id: tileLayout + objectName: "main_tile_layout" + Layout.row: 0 + Layout.column: 1 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 5 + } + + PlaybackControl { + id: playbackControl + Layout.row: 1 + Layout.column: 1 + Layout.fillWidth: true + Layout.margins: 5 + + viewModel: main_window.main_window_view_model.playback_control_view_model + + visible: main_window.main_window_view_model.playback_control_enabled + } + } +} \ No newline at end of file diff --git a/friture/analyzer.py b/friture/analyzer.py index 42584b63..86152720 100755 --- a/friture/analyzer.py +++ b/friture/analyzer.py @@ -29,8 +29,8 @@ from PyQt5 import QtCore, QtWidgets # specifically import from PyQt5.QtGui and QWidgets for startup time improvement : from PyQt5.QtWidgets import QMainWindow, QHBoxLayout, QVBoxLayout, QApplication, QSplashScreen -from PyQt5.QtGui import QPixmap -from PyQt5.QtQml import QQmlEngine, qmlRegisterSingletonType, qmlRegisterType +from PyQt5.QtGui import QPixmap, QFontDatabase +from PyQt5.QtQml import QQmlEngine, qmlRegisterSingletonType, qmlRegisterType, QQmlComponent from PyQt5.QtQuickWidgets import QQuickWidget from PyQt5.QtCore import QObject @@ -39,16 +39,18 @@ # importing friture.exceptionhandler also installs a temporary exception hook from friture.exceptionhandler import errorBox, fileexcepthook import friture +from friture.playback.playback_control_view_model import PlaybackControlViewModel from friture.ui_friture import Ui_MainWindow from friture.about import About_Dialog # About dialog from friture.settings import Settings_Dialog # Setting dialog from friture.audiobuffer import AudioBuffer # audio ring buffer class from friture.audiobackend import AudioBackend # audio backend class from friture.dockmanager import DockManager -from friture.tileLayout import TileLayout +from friture.tilelayout import TileLayout from friture.level_view_model import LevelViewModel from friture.level_data import LevelData from friture.levels import Levels_Widget +from friture.main_window_view_model import MainWindowViewModel from friture.store import GetStore, Store from friture.scope_data import Scope_Data from friture.axis import Axis @@ -64,7 +66,7 @@ from friture.spectrum_data import Spectrum_Data from friture.plotFilledCurve import PlotFilledCurve from friture.filled_curve import FilledCurve -from friture.qml_tools import qml_url, raise_if_error +from friture.qml_tools import qml_url, raise_if_error, component_raise_if_error from friture.generators.sine import Sine_Generator_Settings_View_Model from friture.generators.white import White_Generator_Settings_View_Model from friture.generators.pink import Pink_Generator_Settings_View_Model @@ -109,6 +111,8 @@ def __init__(self): qmlRegisterType(Spectrum_Data, 'Friture', 1, 0, 'SpectrumData') qmlRegisterType(LevelData, 'Friture', 1, 0, 'LevelData') qmlRegisterType(LevelViewModel, 'Friture', 1, 0, 'LevelViewModel') + qmlRegisterType(PlaybackControlViewModel, 'Friture', 1, 0, 'PlaybackControlViewModel') + qmlRegisterType(MainWindowViewModel, 'Friture', 1, 0, 'MainWindowViewModel') qmlRegisterType(Axis, 'Friture', 1, 0, 'Axis') qmlRegisterType(Curve, 'Friture', 1, 0, 'Curve') qmlRegisterType(FilledCurve, 'Friture', 1, 0, 'FilledCurve') @@ -152,36 +156,44 @@ def __init__(self): self.about_dialog = About_Dialog(self, self.slow_timer) self.settings_dialog = Settings_Dialog(self) - self.level_widget = Levels_Widget(self, self.qml_engine) - self.level_widget.set_buffer(self.audiobuffer) - self.audiobuffer.new_data_available.connect(self.level_widget.handle_new_data) - - self.hboxLayout = QHBoxLayout(self.ui.centralwidget) - self.hboxLayout.setContentsMargins(0, 0, 0, 0) - self.hboxLayout.addWidget(self.level_widget) - - self.vboxLayout = QVBoxLayout() - self.hboxLayout.addLayout(self.vboxLayout) - self.centralQuickWidget = QQuickWidget(self.qml_engine, self) self.centralQuickWidget.setObjectName("centralQuickWidget") self.centralQuickWidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self.centralQuickWidget.setResizeMode(QQuickWidget.SizeRootObjectToView) - self.centralQuickWidget.setSource(qml_url("CentralWidget.qml")) - self.vboxLayout.addWidget(self.centralQuickWidget) + self.centralQuickWidget.setSource(qml_url("FritureHost.qml")) raise_if_error(self.centralQuickWidget) - central_widget_root = self.centralQuickWidget.rootObject() - self.main_grid_layout = central_widget_root.findChild(QObject, "main_tile_layout") - assert self.main_grid_layout is not None, "Main grid layout not found in CentralWidget.qml" + self.hboxLayout = QHBoxLayout(self.ui.centralwidget) + self.hboxLayout.setContentsMargins(0, 0, 0, 0) + self.hboxLayout.addWidget(self.centralQuickWidget) + + qml_component = QQmlComponent(self.qml_engine) + qml_component.loadUrl(qml_url("MainWindow.qml")) + component_raise_if_error(qml_component) + + self._main_window_view_model = MainWindowViewModel(self.qml_engine) + + context = self.qml_engine.rootContext() + central_widget_root = qml_component.createWithInitialProperties( + { + "main_window_view_model": self._main_window_view_model, + "fixedFont": QFontDatabase.systemFont(QFontDatabase.FixedFont).family() + }, + context) # type: ignore + central_widget_root.setParent(self.qml_engine) + central_widget_root.setParentItem(self.centralQuickWidget.rootObject()) # type: ignore + + self.main_tile_layout = central_widget_root.findChild(QObject, "main_tile_layout") + assert self.main_tile_layout is not None, "Main tile layout not found in CentralWidget.qml" + + self.level_widget = Levels_Widget(self, self._main_window_view_model.level_view_model) + self.level_widget.set_buffer(self.audiobuffer) + self.audiobuffer.new_data_available.connect(self.level_widget.handle_new_data) - self.playback_widget = PlaybackControlWidget( - self, self.qml_engine, self.player) - self.playback_widget.setVisible(self.settings_dialog.show_playback) - self.vboxLayout.addWidget(self.playback_widget) + self.playback_widget = PlaybackControlWidget(self, self.player, self._main_window_view_model.playback_control_view_model) - self.dockmanager = DockManager(self, self.main_grid_layout) + self.dockmanager = DockManager(self, self.main_tile_layout) # timer ticks self.display_timer.timeout.connect(self.dockmanager.canvasUpdate) @@ -193,7 +205,7 @@ def __init__(self): self.ui.actionSettings.triggered.connect(self.settings_called) self.ui.actionAbout.triggered.connect(self.about_called) self.ui.actionNew_dock.triggered.connect(self.dockmanager.new_dock) - self.playback_widget.recording_toggled.connect(self.timer_toggle) + self.playback_widget.recording_toggled.connect(self.timer_changed) # settings changes self.settings_dialog.show_playback_changed.connect(self.show_playback_changed) @@ -237,7 +249,7 @@ def settings_called(self): self.settings_dialog.show() def show_playback_changed(self, show: bool) -> None: - self.playback_widget.setVisible(show) + self._main_window_view_model.playback_control_enabled = show # slot def about_called(self): @@ -341,6 +353,23 @@ def timer_toggle(self): AudioBackend().restart() self.dockmanager.restart() + # slot + def timer_changed(self, recording: bool): + if not recording and self.display_timer.isActive(): + self.logger.info("Timer stop") + self.display_timer.stop() + self.ui.actionStart.setText("Start") + self.playback_widget.stop_recording() + AudioBackend().pause() + self.dockmanager.pause() + + if recording and not self.display_timer.isActive(): + self.logger.info("Timer start") + self.display_timer.start() + self.ui.actionStart.setText("Stop") + self.playback_widget.start_recording() + AudioBackend().restart() + self.dockmanager.restart() def qt_message_handler(mode, context, message): logger = logging.getLogger(__name__) diff --git a/friture/dock.py b/friture/dock.py index 30144c3d..c1dc22e2 100644 --- a/friture/dock.py +++ b/friture/dock.py @@ -69,7 +69,7 @@ def __init__( context = self.qml_engine.rootContext() self.dock_qml = dock_component.createWithInitialProperties({}, context) self.dock_qml.setParent(self.qml_engine) - self.dock_qml.setParentItem(self.parent().main_grid_layout) # type: ignore + self.dock_qml.setParentItem(self.parent().main_tile_layout) # type: ignore initialProperties = {"viewModel": self.controlbar_viewmodel} component = QQmlComponent(self.qml_engine) diff --git a/friture/dockmanager.py b/friture/dockmanager.py index 1c8dc27e..39295373 100644 --- a/friture/dockmanager.py +++ b/friture/dockmanager.py @@ -23,7 +23,7 @@ from PyQt5.QtWidgets import QMainWindow from friture.defaults import DEFAULT_DOCKS from friture.dock import Dock -from friture.tileLayout import TileLayout +from friture.tilelayout import TileLayout from typing import Dict, List, Optional, TYPE_CHECKING if TYPE_CHECKING: diff --git a/friture/levels.py b/friture/levels.py index 55f108f5..e47c02b4 100644 --- a/friture/levels.py +++ b/friture/levels.py @@ -19,62 +19,32 @@ """Level widget that displays peak and RMS levels for 1 or 2 ports.""" -from PyQt5 import QtWidgets -from PyQt5.QtQml import QQmlComponent -from PyQt5.QtQuick import QQuickWindow # type: ignore -from PyQt5.QtGui import QFontDatabase +from PyQt5.QtCore import QObject import numpy as np -from friture.store import GetStore from friture.levels_settings import Levels_Settings_Dialog # settings dialog from friture.audioproc import audioproc -from friture.level_view_model import LevelViewModel from friture.iec import dB_to_IEC from friture_extensions.exp_smoothing_conv import pyx_exp_smoothed_value from friture.audiobackend import SAMPLING_RATE -from friture.qml_tools import qml_url, raise_if_error SMOOTH_DISPLAY_TIMER_PERIOD_MS = 25 LEVEL_TEXT_LABEL_PERIOD_MS = 250 LEVEL_TEXT_LABEL_STEPS = LEVEL_TEXT_LABEL_PERIOD_MS / SMOOTH_DISPLAY_TIMER_PERIOD_MS -class Levels_Widget(QtWidgets.QWidget): +class Levels_Widget(QObject): - def __init__(self, parent, engine): + def __init__(self, parent, view_model): super().__init__(parent) - self.setObjectName("Levels_Widget") - self.gridLayout = QtWidgets.QVBoxLayout(self) - self.gridLayout.setObjectName("gridLayout") - - store = GetStore() - self.level_view_model = LevelViewModel(store) - store._dock_states.append(self.level_view_model) - state_id = len(store._dock_states) - 1 - - self.quickWindow = QQuickWindow() - component = QQmlComponent(engine, qml_url("Levels.qml"), self) - raise_if_error(component) - - fixedFont = QFontDatabase.systemFont(QFontDatabase.FixedFont) - - engineContext = engine.rootContext() - initialProperties = {"parent": self.quickWindow.contentItem(), "stateId": state_id, "fixedFont": fixedFont } - self.qmlObject = component.createWithInitialProperties(initialProperties, engineContext) - self.qmlObject.setParent(self.quickWindow) - - self.quickWidget = QtWidgets.QWidget.createWindowContainer(self.quickWindow, self) - self.quickWidget.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding) - self.gridLayout.addWidget(self.quickWidget) - - self.qmlObject.widthChanged.connect(self.onWidthChanged) - self.onWidthChanged() + self._parent = parent + self.level_view_model = view_model self.audiobuffer = None # initialize the settings dialog - self.settings_dialog = Levels_Settings_Dialog(self) + self.settings_dialog = Levels_Settings_Dialog(parent) # initialize the class instance that will do the fft self.proc = audioproc() @@ -107,9 +77,6 @@ def __init__(self, parent, engine): self.i = 0 - def onWidthChanged(self): - self.quickWidget.setFixedWidth(int(self.qmlObject.width())) - # method def set_buffer(self, buffer): self.audiobuffer = buffer @@ -165,7 +132,7 @@ def handle_new_data(self, floatdata): # method def canvasUpdate(self): - if not self.isVisible(): + if not self._parent.isVisible(): return self.i += 1 diff --git a/friture/main_window_view_model.py b/friture/main_window_view_model.py new file mode 100644 index 00000000..3ebbf89a --- /dev/null +++ b/friture/main_window_view_model.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2025 Timothée Lecomte + +# This file is part of Friture. +# +# Friture is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as published by +# the Free Software Foundation. +# +# Friture 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 Friture. If not, see . + +from PyQt5 import QtCore +from PyQt5.QtCore import pyqtProperty, pyqtSignal + +from friture.level_view_model import LevelViewModel +from friture.playback.playback_control_view_model import PlaybackControlViewModel + +class MainWindowViewModel(QtCore.QObject): + playback_control_enabled_changed = pyqtSignal(bool) + + def __init__(self, parent=None): + super().__init__(parent) + + self._level_view_model = LevelViewModel(self) + self._playback_control_view_model = PlaybackControlViewModel(self) + self._playback_control_enabled = False + + @pyqtProperty(LevelViewModel, constant=True) # type: ignore + def level_view_model(self): + return self._level_view_model + + @pyqtProperty(PlaybackControlViewModel, constant=True) # type: ignore + def playback_control_view_model(self): + return self._playback_control_view_model + + def get_playback_control_enabled(self) -> bool: + return self._playback_control_enabled + + def set_playback_control_enabled(self, playback_control_enabled: bool) -> None: + if self._playback_control_enabled != playback_control_enabled: + self._playback_control_enabled = playback_control_enabled + self.playback_control_enabled_changed.emit(playback_control_enabled) + + playback_control_enabled = pyqtProperty(int, fget=get_playback_control_enabled, fset=set_playback_control_enabled, notify=playback_control_enabled_changed) diff --git a/friture/playback/Control.qml b/friture/playback/Control.qml deleted file mode 100644 index b8b749bb..00000000 --- a/friture/playback/Control.qml +++ /dev/null @@ -1,115 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.15 -import QtQuick.Window 2.2 -import QtQuick.Layouts 1.15 -import Friture 1.0 - -RowLayout { - id: controlRow - - signal recordClicked() - signal stopClicked() - signal playClicked() - signal positionChanged(real value) - - function showRecording() { - record.enabled = false; - record.down = true; - stop.enabled = true; - play.enabled = false; - play.down = undefined; - position.enabled = true; - } - - function showStopped() { - record.enabled = true; - record.down = undefined; - stop.enabled = false; - play.enabled = true; - play.down = undefined; - position.enabled = true; - } - - function showPlaying() { - record.enabled = false; - record.down = undefined; - stop.enabled = true; - play.enabled = false; - play.down = true; - position.enabled = false; - } - - function setRecordingStartTime(time) { - startTime.text = time.toFixed(1); - position.from = time; - } - - function setPlaybackPosition(time) { - position.value = time; - selectedTime.text = time.toFixed(1); - } - - Button { - id: record - text: "Record" - down: true - enabled: false - onClicked: { - controlRow.showRecording(); - controlRow.recordClicked(); - } - } - - Button { - id: stop - text: "Stop" - onClicked: { - controlRow.showStopped(); - controlRow.stopClicked(); - } - } - - Button { - id: play - text: "Play" - onClicked: { - controlRow.showPlaying(); - controlRow.playClicked(); - } - } - - FontMetrics { - id: fontMetrics - } - - Text { - id: startTime - text: "-5.0" - // fixed width so the slider position doesn't change with this value - width: fontMetrics.boundingRect("-000.0").width - horizontalAlignment: Text.AlignRight - } - - Slider { - id: position - from: -5.0 - to: 0.0 - value: 0.0 - stepSize: 0.1 - - Layout.fillWidth: true - - onMoved: { - selectedTime.text = position.value.toFixed(1); - controlRow.positionChanged(position.value); - } - } - - Text { - id: selectedTime - text: "0.0" - // fixed width so the slider position doesn't change with this value - width: fontMetrics.boundingRect("-000.0").width - horizontalAlignment: Text.AlignLeft - } -} diff --git a/friture/playback/PlaybackControl.qml b/friture/playback/PlaybackControl.qml new file mode 100644 index 00000000..0dab84ec --- /dev/null +++ b/friture/playback/PlaybackControl.qml @@ -0,0 +1,82 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.2 +import QtQuick.Layouts 1.15 +import Friture 1.0 + +RowLayout { + id: controlRow + + required property PlaybackControlViewModel viewModel + + SystemPalette { id: systemPalette; colorGroup: SystemPalette.Active } + + Button { + id: record + text: "Record" + down: controlRow.viewModel.recording === true ? true : undefined + enabled: controlRow.viewModel.playing === false && controlRow.viewModel.recording === false + onClicked: { + controlRow.viewModel.recording = true; + } + } + + Button { + id: stop + text: "Stop" + down: controlRow.viewModel.playing === false && controlRow.viewModel.recording === false ? true : undefined + enabled: controlRow.viewModel.playing === true || controlRow.viewModel.recording === true + onClicked: { + controlRow.viewModel.playing = false; + controlRow.viewModel.recording = false; + } + } + + Button { + id: play + text: "Play" + down: controlRow.viewModel.playing === true ? true : undefined + enabled: controlRow.viewModel.recording === false + onClicked: { + controlRow.viewModel.playing = true; + } + } + + FontMetrics { + id: fontMetrics + } + + Text { + id: startTime + text: controlRow.viewModel.recording_start_time.toFixed(1) + color: systemPalette.text + // fixed width so the slider position doesn't change with this value + Layout.preferredWidth: fontMetrics.boundingRect("-000.0").width + horizontalAlignment: Text.AlignRight + } + + Slider { + id: position + from: controlRow.viewModel.recording_start_time + to: 0.0 + value: controlRow.viewModel.position + stepSize: 0.1 + + enabled: controlRow.viewModel.playing === false + + Layout.fillWidth: true + + onMoved: { + controlRow.viewModel.position = position.value; + } + } + + Text { + id: selectedTime + text: controlRow.viewModel.position.toFixed(1) + color: systemPalette.text + // fixed width so the slider position doesn't change with this value + Layout.preferredWidth: fontMetrics.boundingRect("-000.0").width + horizontalAlignment: Text.AlignLeft + } +} diff --git a/friture/playback/control.py b/friture/playback/control.py index db433048..515ed5c9 100644 --- a/friture/playback/control.py +++ b/friture/playback/control.py @@ -18,81 +18,60 @@ # along with Friture. If not, see . import logging -from typing import Any -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtGui import QPalette -from PyQt5.QtWidgets import QSizePolicy, QWidget -from PyQt5.QtQml import QQmlEngine -from PyQt5.QtQuickWidgets import QQuickWidget +from PyQt5.QtCore import pyqtSignal, QObject -from friture.qml_tools import qml_url, raise_if_error +from friture.playback.playback_control_view_model import PlaybackControlViewModel from friture.playback.player import Player logger = logging.getLogger(__name__) -class PlaybackControlWidget(QQuickWidget): - recording_toggled = pyqtSignal() - - def __init__(self, parent: QWidget, engine: QQmlEngine, player: Player): - super().__init__(engine, parent) - self.statusChanged.connect(self.on_status_changed) - self.setResizeMode(QQuickWidget.SizeRootObjectToView) - self.setSizePolicy( - QSizePolicy.Expanding, QSizePolicy.Preferred - ) - self.setClearColor(self.palette().color(QPalette.Window)) - self.setSource(qml_url("playback/Control.qml")) - raise_if_error(self) - - self.root: Any = self.rootObject() - self.root.stopClicked.connect(self.on_stopped) - self.root.recordClicked.connect(self.on_record) - self.root.playClicked.connect(self.on_played) - self.root.positionChanged.connect(self.on_playback_position_changed) - self.root.setRecordingStartTime(-0.1) +class PlaybackControlWidget(QObject): + recording_toggled = pyqtSignal(bool) + + def __init__(self, parent: QObject, player: Player, playback_control_view_model: PlaybackControlViewModel) -> None: + super().__init__(parent) + + self._playback_control_view_model = playback_control_view_model + + self._playback_control_view_model.recording_changed.connect(self.on_recording_changed) + self._playback_control_view_model.playing_changed.connect(self.on_playing_changed) + self._playback_control_view_model.position_changed.connect(self.on_playback_position_changed) self.player = player self.player.stopped.connect(self.on_playback_stopped) self.player.recorded_length_changed.connect(self.on_recorded_len_changed) self.player.playback_time_changed.connect(self.on_playback_time_changed) - def start_recording(self) -> None: if not self.player.is_stopped(): self.player.stop() - self.root.showRecording() + self._playback_control_view_model.set_playing(False) + self._playback_control_view_model.set_recording(True) def stop_recording(self) -> None: - self.root.showStopped() + self._playback_control_view_model.set_playing(False) + self._playback_control_view_model.set_recording(False) - def on_status_changed(self, status: QQuickWidget.Status) -> None: - if status == QQuickWidget.Error: - for error in self.errors(): - logger.error("QML error: " + error.toString()) + def on_recording_changed(self, recording: bool) -> None: + self.recording_toggled.emit(recording) - def on_stopped(self) -> None: - if self.player.is_stopped(): - self.recording_toggled.emit() + def on_playing_changed(self, playing: bool) -> None: + if playing: + self.player.play() else: self.player.stop() - def on_record(self) -> None: - self.recording_toggled.emit() - - def on_played(self) -> None: - self.player.play() - def on_playback_stopped(self) -> None: - self.root.showStopped() - self.root.setPlaybackPosition(self.player.play_start_time) + self._playback_control_view_model.set_playing(False) + self._playback_control_view_model.set_position(self.player.play_start_time) def on_playback_position_changed(self, value: float) -> None: self.player.play_start_time = value def on_recorded_len_changed(self, length: float) -> None: # Always give the slider a nonzero length even if nothing is recorded - self.root.setRecordingStartTime(-max(length, 0.1)) + self._playback_control_view_model.set_recording_start_time(-max(length, 0.1)) def on_playback_time_changed(self, time: float) -> None: - self.root.setPlaybackPosition(time) + self._playback_control_view_model.set_position(time) diff --git a/friture/playback/playback_control_view_model.py b/friture/playback/playback_control_view_model.py new file mode 100644 index 00000000..a38c54e5 --- /dev/null +++ b/friture/playback/playback_control_view_model.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2024 Celeste Sinéad, Timothée Lecomte + +# This file is part of Friture. +# +# Friture is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as published by +# the Free Software Foundation. +# +# Friture 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 Friture. If not, see . + +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal + +class PlaybackControlViewModel(QObject): + recording_changed = pyqtSignal(bool) + playing_changed = pyqtSignal(bool) + position_changed = pyqtSignal(float) + recording_start_time_changed = pyqtSignal(float) + + def __init__(self, parent: QObject): + super().__init__(parent) + + self._recording = True + self._playing = False + self._position = 0. + self._recording_start_time = -0.1 + + def get_recording(self) -> bool: + return self._recording + + def set_recording(self, recording: bool) -> None: + if self._recording != recording: + self._recording = recording + self.recording_changed.emit(recording) + + recording = pyqtProperty(bool, fget=get_recording, fset=set_recording, notify=recording_changed) + + def get_playing(self) -> bool: + return self._playing + + def set_playing(self, playing: bool) -> None: + if self._playing != playing: + self._playing = playing + self.playing_changed.emit(playing) + + playing = pyqtProperty(bool, fget=get_playing, fset=set_playing, notify=playing_changed) + + def get_position(self) -> float: + return self._position + + def set_position(self, position: float) -> None: + if self._position != position: + self._position = position + self.position_changed.emit(position) + + position = pyqtProperty(float, fget=get_position, fset=set_position, notify=position_changed) + + def get_recording_start_time(self) -> float: + return self._recording_start_time + + def set_recording_start_time(self, recording_start_time: float) -> None: + if self._recording_start_time != recording_start_time: + self._recording_start_time = recording_start_time + self.recording_start_time_changed.emit(recording_start_time) + + recording_start_time = pyqtProperty(float, fget=get_recording_start_time, fset=set_recording_start_time, notify=recording_start_time_changed) diff --git a/friture/tilelayout.py b/friture/tilelayout.py index 51effff5..4e229de3 100644 --- a/friture/tilelayout.py +++ b/friture/tilelayout.py @@ -106,8 +106,8 @@ def updatePolish(self): columnWidth = self.width()//columnCount for columnIndex in range(columnCount): item = self.childItems()[i] - x = self.x() + columnIndex*columnWidth - y = self.y() + rowIndex*rowHeight + x = columnIndex*columnWidth + y = rowIndex*rowHeight item.setWidth(columnWidth) item.setHeight(rowHeight)