diff --git a/friture/FritureHost.qml b/friture/FritureHost.qml
deleted file mode 100644
index d814e06a..00000000
--- a/friture/FritureHost.qml
+++ /dev/null
@@ -1,11 +0,0 @@
-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/FritureMainWindow.qml b/friture/FritureMainWindow.qml
new file mode 100644
index 00000000..ab539a34
--- /dev/null
+++ b/friture/FritureMainWindow.qml
@@ -0,0 +1,84 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.2
+import Friture 1.0
+
+Rectangle { // eventually move to ApplicationWindow
+ id: mainWindow
+ anchors.fill: parent
+ // title: qsTr("Friture") // ApplicationWindow
+ // icon.source: "qrc:/images-src/window-icon.svg" // ApplicationWindow
+
+ required property MainWindowViewModel main_window_view_model
+ required property string fixedFont
+
+ ColumnLayout { // remove once we use ApplicationWindow
+ anchors.fill: parent
+ spacing: 0
+
+ ToolBar {
+ id: toolBar
+ Layout.fillWidth: true // remove once we use ApplicationWindow
+
+ RowLayout {
+ anchors.fill: toolBar
+ spacing: 0
+
+ ToolButton {
+ id: startButton
+ checkable: true
+ checked: mainWindow.main_window_view_model.toolbar_view_model.recording
+ icon.source: startButton.checked ? "qrc:/images-src/stop.svg" : "qrc:/images-src/start.svg"
+ text: startButton.checked ? qsTr("Stop") : qsTr("Start")
+ ToolTip.text: qsTr("Start/Stop")
+ icon.height: 32
+ icon.width: 32
+ //shortcut: "Space"
+ onClicked: {
+ mainWindow.main_window_view_model.toolbar_view_model.recording_toggle()
+ }
+ }
+ ToolButton {
+ id: newDockButton
+ icon.source: "qrc:/images-src/new-dock.svg"
+ text: qsTr("New dock")
+ ToolTip.text: qsTr("Add a new dock to Friture window")
+ icon.height: 32
+ icon.width: 32
+ onClicked: {
+ mainWindow.main_window_view_model.toolbar_view_model.new_dock()
+ }
+ }
+ ToolButton {
+ id: settingsButton
+ icon.source: "qrc:/images-src/tools.svg"
+ text: qsTr("Settings")
+ ToolTip.text: qsTr("Display settings dialog")
+ icon.height: 32
+ icon.width: 32
+ onClicked: {
+ mainWindow.main_window_view_model.toolbar_view_model.settings()
+ }
+ }
+ ToolButton {
+ id: aboutButton
+ icon.source: "qrc:/images-src/window-icon.svg"
+ text: qsTr("About Friture")
+ icon.height: 32
+ icon.width: 32
+ onClicked: {
+ mainWindow.main_window_view_model.toolbar_view_model.about()
+ }
+ }
+ }
+ }
+
+ MainWindow {
+ id: centralWidget
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ fixedFont: mainWindow.fixedFont
+ main_window_view_model: mainWindow.main_window_view_model
+ }
+ }
+}
diff --git a/friture/MainWindow.qml b/friture/MainWindow.qml
index 3a6b4dbe..5368a7d3 100644
--- a/friture/MainWindow.qml
+++ b/friture/MainWindow.qml
@@ -8,7 +8,6 @@ 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
diff --git a/friture/analyzer.py b/friture/analyzer.py
index 86152720..4e51c47e 100755
--- a/friture/analyzer.py
+++ b/friture/analyzer.py
@@ -26,19 +26,20 @@
import logging
import logging.handlers
-from PyQt5 import QtCore, QtWidgets
+from PyQt5 import QtCore
# specifically import from PyQt5.QtGui and QWidgets for startup time improvement :
-from PyQt5.QtWidgets import QMainWindow, QHBoxLayout, QVBoxLayout, QApplication, QSplashScreen
+from PyQt5.QtWidgets import QMainWindow, QApplication, QSplashScreen, QWidget
from PyQt5.QtGui import QPixmap, QFontDatabase
-from PyQt5.QtQml import QQmlEngine, qmlRegisterSingletonType, qmlRegisterType, QQmlComponent
-from PyQt5.QtQuickWidgets import QQuickWidget
+from PyQt5.QtQml import QQmlEngine, qmlRegisterSingletonType, qmlRegisterType
from PyQt5.QtCore import QObject
+from PyQt5.QtQuick import QQuickView
import platformdirs
# importing friture.exceptionhandler also installs a temporary exception hook
from friture.exceptionhandler import errorBox, fileexcepthook
import friture
+from friture.main_toolbar_view_model import MainToolbarViewModel
from friture.playback.playback_control_view_model import PlaybackControlViewModel
from friture.ui_friture import Ui_MainWindow
from friture.about import About_Dialog # About dialog
@@ -66,7 +67,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, component_raise_if_error
+from friture.qml_tools import qml_url, view_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
@@ -113,6 +114,7 @@ def __init__(self):
qmlRegisterType(LevelViewModel, 'Friture', 1, 0, 'LevelViewModel')
qmlRegisterType(PlaybackControlViewModel, 'Friture', 1, 0, 'PlaybackControlViewModel')
qmlRegisterType(MainWindowViewModel, 'Friture', 1, 0, 'MainWindowViewModel')
+ qmlRegisterType(MainToolbarViewModel, 'Friture', 1, 0, 'MainToolbarViewModel')
qmlRegisterType(Axis, 'Friture', 1, 0, 'Axis')
qmlRegisterType(Curve, 'Friture', 1, 0, 'Curve')
qmlRegisterType(FilledCurve, 'Friture', 1, 0, 'FilledCurve')
@@ -156,35 +158,22 @@ def __init__(self):
self.about_dialog = About_Dialog(self, self.slow_timer)
self.settings_dialog = Settings_Dialog(self)
- 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("FritureHost.qml"))
-
- raise_if_error(self.centralQuickWidget)
+ self._main_window_view_model = MainWindowViewModel(self.qml_engine)
- self.hboxLayout = QHBoxLayout(self.ui.centralwidget)
- self.hboxLayout.setContentsMargins(0, 0, 0, 0)
- self.hboxLayout.addWidget(self.centralQuickWidget)
+ self.quick_view = QQuickView(self.qml_engine, None)
+ self.quick_view.setResizeMode(QQuickView.SizeRootObjectToView)
+ self.quick_view.setInitialProperties({
+ "main_window_view_model": self._main_window_view_model,
+ "fixedFont": QFontDatabase.systemFont(QFontDatabase.FixedFont).family()
+ })
+ self.quick_view.setSource(qml_url("FritureMainWindow.qml"))
- qml_component = QQmlComponent(self.qml_engine)
- qml_component.loadUrl(qml_url("MainWindow.qml"))
- component_raise_if_error(qml_component)
+ view_raise_if_error(self.quick_view)
- self._main_window_view_model = MainWindowViewModel(self.qml_engine)
+ self.quick_container = QWidget.createWindowContainer(self.quick_view, self)
+ self.setCentralWidget(self.quick_container)
- 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")
+ self.main_tile_layout = self.quick_view.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)
@@ -201,10 +190,10 @@ def __init__(self):
self.display_timer.timeout.connect(AudioBackend().fetchAudioData)
# toolbar clicks
- self.ui.actionStart.triggered.connect(self.timer_toggle)
- 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._main_window_view_model.toolbar_view_model.recording_clicked.connect(self.timer_toggle)
+ self._main_window_view_model.toolbar_view_model.new_dock_clicked.connect(self.dockmanager.new_dock)
+ self._main_window_view_model.toolbar_view_model.settings_clicked.connect(self.settings_called)
+ self._main_window_view_model.toolbar_view_model.about_clicked.connect(self.about_called)
self.playback_widget.recording_toggled.connect(self.timer_changed)
# settings changes
@@ -214,15 +203,6 @@ def __init__(self):
# restore the settings and widgets geometries
self.restoreAppState()
- # make sure the toolbar is shown
- # in case it was closed by mistake (before it was made impossible)
- self.ui.toolBar.setVisible(True)
-
- # prevent from hiding or moving the toolbar
- self.ui.toolBar.toggleViewAction().setVisible(False)
- self.ui.toolBar.setMovable(False)
- self.ui.toolBar.setFloatable(False)
-
# start timers
self.timer_toggle()
self.slow_timer.start()
@@ -341,14 +321,14 @@ def timer_toggle(self):
if self.display_timer.isActive():
self.logger.info("Timer stop")
self.display_timer.stop()
- self.ui.actionStart.setText("Start")
+ self._main_window_view_model.toolbar_view_model.recording = False
self.playback_widget.stop_recording()
AudioBackend().pause()
self.dockmanager.pause()
else:
self.logger.info("Timer start")
self.display_timer.start()
- self.ui.actionStart.setText("Stop")
+ self._main_window_view_model.toolbar_view_model.recording = True
self.playback_widget.start_recording()
AudioBackend().restart()
self.dockmanager.restart()
@@ -358,7 +338,7 @@ 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._main_window_view_model.toolbar_view_model.recording = False
self.playback_widget.stop_recording()
AudioBackend().pause()
self.dockmanager.pause()
@@ -366,7 +346,7 @@ def timer_changed(self, recording: bool):
if recording and not self.display_timer.isActive():
self.logger.info("Timer start")
self.display_timer.start()
- self.ui.actionStart.setText("Stop")
+ self._main_window_view_model.toolbar_view_model.recording = True
self.playback_widget.start_recording()
AudioBackend().restart()
self.dockmanager.restart()
diff --git a/friture/main_toolbar_view_model.py b/friture/main_toolbar_view_model.py
new file mode 100644
index 00000000..d280e745
--- /dev/null
+++ b/friture/main_toolbar_view_model.py
@@ -0,0 +1,59 @@
+#!/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, pyqtSlot
+
+class MainToolbarViewModel(QtCore.QObject):
+ recording_changed = pyqtSignal(bool)
+ recording_clicked = pyqtSignal()
+ new_dock_clicked = pyqtSignal()
+ settings_clicked = pyqtSignal()
+ about_clicked = pyqtSignal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ self._recording = True
+
+ 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)
+
+ @pyqtSlot()
+ def recording_toggle(self) -> None:
+ self.recording_clicked.emit()
+
+ @pyqtSlot()
+ def new_dock(self) -> None:
+ self.new_dock_clicked.emit()
+
+ @pyqtSlot()
+ def settings(self) -> None:
+ self.settings_clicked.emit()
+
+ @pyqtSlot()
+ def about(self) -> None:
+ self.about_clicked.emit()
diff --git a/friture/main_window_view_model.py b/friture/main_window_view_model.py
index 3ebbf89a..588bd7bc 100644
--- a/friture/main_window_view_model.py
+++ b/friture/main_window_view_model.py
@@ -21,6 +21,7 @@
from PyQt5.QtCore import pyqtProperty, pyqtSignal
from friture.level_view_model import LevelViewModel
+from friture.main_toolbar_view_model import MainToolbarViewModel
from friture.playback.playback_control_view_model import PlaybackControlViewModel
class MainWindowViewModel(QtCore.QObject):
@@ -29,10 +30,15 @@ class MainWindowViewModel(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
+ self._toolbar_view_model = MainToolbarViewModel(self)
self._level_view_model = LevelViewModel(self)
self._playback_control_view_model = PlaybackControlViewModel(self)
self._playback_control_enabled = False
+ @pyqtProperty(MainToolbarViewModel, constant=True) # type: ignore
+ def toolbar_view_model(self):
+ return self._toolbar_view_model
+
@pyqtProperty(LevelViewModel, constant=True) # type: ignore
def level_view_model(self):
return self._level_view_model
diff --git a/friture/qml_tools.py b/friture/qml_tools.py
index 072c1f67..d455e99b 100644
--- a/friture/qml_tools.py
+++ b/friture/qml_tools.py
@@ -4,6 +4,7 @@
from PyQt5.QtCore import QUrl
from PyQt5.QtQuickWidgets import QQuickWidget
from PyQt5.QtQml import QQmlComponent
+from PyQt5.QtQuick import QQuickView
def qml_url(fileName):
return QUrl.fromLocalFile(qml_path(fileName))
@@ -20,12 +21,17 @@ def qml_path(fileName):
return os.path.join(application_path, fileName)
-def raise_if_error(quickWidget):
+def raise_if_error(quickWidget: QQuickWidget) -> None:
if quickWidget.status() == QQuickWidget.Error:
errors = '\n'.join(map(lambda x: x.toString(), quickWidget.errors()))
raise Exception("QML error(s): %s" % (errors))
-def component_raise_if_error(quickWidget):
- if quickWidget.status() == QQmlComponent.Error:
- errors = '\n'.join(map(lambda x: x.toString(), quickWidget.errors()))
+def view_raise_if_error(quickView: QQuickView) -> None:
+ if quickView.status() == QQuickView.Error:
+ errors = '\n'.join(map(lambda x: x.toString(), quickView.errors()))
+ raise Exception("QML error(s): %s" % (errors))
+
+def component_raise_if_error(component: QQmlComponent) -> None:
+ if component.status() == QQmlComponent.Error:
+ errors = '\n'.join(map(lambda x: x.toString(), component.errors()))
raise Exception("QML error(s): %s" % (errors))
\ No newline at end of file
diff --git a/friture/ui_friture.py b/friture/ui_friture.py
index 16973904..d90e9709 100644
--- a/friture/ui_friture.py
+++ b/friture/ui_friture.py
@@ -1,15 +1,6 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file 'ui/friture.ui'
-#
-# Created by: PyQt5 UI code generator 5.15.11
-#
-# WARNING: Any manual changes made to this file will be lost when pyuic5 is
-# run again. Do not edit this file unless you know what you are doing.
-
-
-from PyQt5 import QtCore, QtGui, QtWidgets
-
+from PyQt5 import QtCore, QtGui
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
@@ -18,42 +9,6 @@ def setupUi(self, MainWindow):
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/images-src/window-icon.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
MainWindow.setWindowIcon(icon)
- self.centralwidget = QtWidgets.QWidget(MainWindow)
- self.centralwidget.setStyleSheet("")
- self.centralwidget.setObjectName("centralwidget")
- MainWindow.setCentralWidget(self.centralwidget)
- self.toolBar = QtWidgets.QToolBar(MainWindow)
- self.toolBar.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
- self.toolBar.setObjectName("toolBar")
- MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar)
- self.actionStart = QtWidgets.QAction(MainWindow)
- self.actionStart.setCheckable(True)
- self.actionStart.setChecked(True)
- icon1 = QtGui.QIcon()
- icon1.addPixmap(QtGui.QPixmap(":/images-src/start.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- icon1.addPixmap(QtGui.QPixmap(":/images-src/stop.svg"), QtGui.QIcon.Normal, QtGui.QIcon.On)
- icon1.addPixmap(QtGui.QPixmap(":/images-src/stop.svg"), QtGui.QIcon.Disabled, QtGui.QIcon.On)
- icon1.addPixmap(QtGui.QPixmap(":/images-src/stop.svg"), QtGui.QIcon.Active, QtGui.QIcon.On)
- icon1.addPixmap(QtGui.QPixmap(":/images-src/stop.svg"), QtGui.QIcon.Selected, QtGui.QIcon.On)
- self.actionStart.setIcon(icon1)
- self.actionStart.setObjectName("actionStart")
- self.actionSettings = QtWidgets.QAction(MainWindow)
- icon2 = QtGui.QIcon()
- icon2.addPixmap(QtGui.QPixmap(":/images-src/tools.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- self.actionSettings.setIcon(icon2)
- self.actionSettings.setObjectName("actionSettings")
- self.actionAbout = QtWidgets.QAction(MainWindow)
- self.actionAbout.setIcon(icon)
- self.actionAbout.setObjectName("actionAbout")
- self.actionNew_dock = QtWidgets.QAction(MainWindow)
- icon3 = QtGui.QIcon()
- icon3.addPixmap(QtGui.QPixmap(":/images-src/new-dock.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- self.actionNew_dock.setIcon(icon3)
- self.actionNew_dock.setObjectName("actionNew_dock")
- self.toolBar.addAction(self.actionStart)
- self.toolBar.addAction(self.actionNew_dock)
- self.toolBar.addAction(self.actionSettings)
- self.toolBar.addAction(self.actionAbout)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
@@ -61,13 +16,5 @@ def setupUi(self, MainWindow):
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Friture"))
- self.toolBar.setWindowTitle(_translate("MainWindow", "toolBar"))
- self.actionStart.setText(_translate("MainWindow", "Stop"))
- self.actionStart.setToolTip(_translate("MainWindow", "Start/Stop"))
- self.actionStart.setShortcut(_translate("MainWindow", "Space"))
- self.actionSettings.setText(_translate("MainWindow", "Settings"))
- self.actionSettings.setToolTip(_translate("MainWindow", "Display settings dialog"))
- self.actionAbout.setText(_translate("MainWindow", "About Friture"))
- self.actionNew_dock.setText(_translate("MainWindow", "New dock"))
- self.actionNew_dock.setToolTip(_translate("MainWindow", "Add a new dock to Friture window"))
+
from . import friture_rc
diff --git a/ui/friture.ui b/ui/friture.ui
deleted file mode 100644
index a497f6c0..00000000
--- a/ui/friture.ui
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
- MainWindow
-
-
-
- 0
- 0
- 869
- 573
-
-
-
- Friture
-
-
-
- :/images-src/window-icon.svg:/images-src/window-icon.svg
-
-
-
-
-
-
-
-
- toolBar
-
-
- Qt::ToolButtonTextBesideIcon
-
-
- TopToolBarArea
-
-
- false
-
-
-
-
-
-
-
-
- true
-
-
- true
-
-
-
- :/images-src/start.svg
- :/images-src/stop.svg
- :/images-src/stop.svg
- :/images-src/stop.svg
- :/images-src/stop.svg:/images-src/start.svg
-
-
- Stop
-
-
- Start/Stop
-
-
- Space
-
-
-
-
-
- :/images-src/tools.svg:/images-src/tools.svg
-
-
- Settings
-
-
- Display settings dialog
-
-
-
-
-
- :/images-src/window-icon.svg:/images-src/window-icon.svg
-
-
- About Friture
-
-
-
-
-
- :/images-src/new-dock.svg:/images-src/new-dock.svg
-
-
- New dock
-
-
- Add a new dock to Friture window
-
-
-
-
-
-
-
-