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