Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions friture/FritureHost.qml

This file was deleted.

84 changes: 84 additions & 0 deletions friture/FritureMainWindow.qml
Original file line number Diff line number Diff line change
@@ -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
}
}
}
1 change: 0 additions & 1 deletion friture/MainWindow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 27 additions & 47 deletions friture/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -358,15 +338,15 @@ 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()

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()
Expand Down
59 changes: 59 additions & 0 deletions friture/main_toolbar_view_model.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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()
6 changes: 6 additions & 0 deletions friture/main_window_view_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
14 changes: 10 additions & 4 deletions friture/qml_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Loading