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)