Skip to content

Commit 21e28dc

Browse files
authored
Merge pull request #364 from tlecomte/toolbarQml
feat(QML): toolbar is now QML
2 parents e03d4e3 + 69415a6 commit 21e28dc

File tree

9 files changed

+188
-224
lines changed

9 files changed

+188
-224
lines changed

friture/FritureHost.qml

Lines changed: 0 additions & 11 deletions
This file was deleted.

friture/FritureMainWindow.qml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import QtQuick 2.15
2+
import QtQuick.Controls 2.15
3+
import QtQuick.Layouts 1.2
4+
import Friture 1.0
5+
6+
Rectangle { // eventually move to ApplicationWindow
7+
id: mainWindow
8+
anchors.fill: parent
9+
// title: qsTr("Friture") // ApplicationWindow
10+
// icon.source: "qrc:/images-src/window-icon.svg" // ApplicationWindow
11+
12+
required property MainWindowViewModel main_window_view_model
13+
required property string fixedFont
14+
15+
ColumnLayout { // remove once we use ApplicationWindow
16+
anchors.fill: parent
17+
spacing: 0
18+
19+
ToolBar {
20+
id: toolBar
21+
Layout.fillWidth: true // remove once we use ApplicationWindow
22+
23+
RowLayout {
24+
anchors.fill: toolBar
25+
spacing: 0
26+
27+
ToolButton {
28+
id: startButton
29+
checkable: true
30+
checked: mainWindow.main_window_view_model.toolbar_view_model.recording
31+
icon.source: startButton.checked ? "qrc:/images-src/stop.svg" : "qrc:/images-src/start.svg"
32+
text: startButton.checked ? qsTr("Stop") : qsTr("Start")
33+
ToolTip.text: qsTr("Start/Stop")
34+
icon.height: 32
35+
icon.width: 32
36+
//shortcut: "Space"
37+
onClicked: {
38+
mainWindow.main_window_view_model.toolbar_view_model.recording_toggle()
39+
}
40+
}
41+
ToolButton {
42+
id: newDockButton
43+
icon.source: "qrc:/images-src/new-dock.svg"
44+
text: qsTr("New dock")
45+
ToolTip.text: qsTr("Add a new dock to Friture window")
46+
icon.height: 32
47+
icon.width: 32
48+
onClicked: {
49+
mainWindow.main_window_view_model.toolbar_view_model.new_dock()
50+
}
51+
}
52+
ToolButton {
53+
id: settingsButton
54+
icon.source: "qrc:/images-src/tools.svg"
55+
text: qsTr("Settings")
56+
ToolTip.text: qsTr("Display settings dialog")
57+
icon.height: 32
58+
icon.width: 32
59+
onClicked: {
60+
mainWindow.main_window_view_model.toolbar_view_model.settings()
61+
}
62+
}
63+
ToolButton {
64+
id: aboutButton
65+
icon.source: "qrc:/images-src/window-icon.svg"
66+
text: qsTr("About Friture")
67+
icon.height: 32
68+
icon.width: 32
69+
onClicked: {
70+
mainWindow.main_window_view_model.toolbar_view_model.about()
71+
}
72+
}
73+
}
74+
}
75+
76+
MainWindow {
77+
id: centralWidget
78+
Layout.fillWidth: true
79+
Layout.fillHeight: true
80+
fixedFont: mainWindow.fixedFont
81+
main_window_view_model: mainWindow.main_window_view_model
82+
}
83+
}
84+
}

friture/MainWindow.qml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ Rectangle {
88
id: main_window
99
SystemPalette { id: systemPalette; colorGroup: SystemPalette.Active }
1010
color: systemPalette.window
11-
anchors.fill: parent
1211

1312
required property MainWindowViewModel main_window_view_model
1413
required property string fixedFont

friture/analyzer.py

Lines changed: 27 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,20 @@
2626
import logging
2727
import logging.handlers
2828

29-
from PyQt5 import QtCore, QtWidgets
29+
from PyQt5 import QtCore
3030
# specifically import from PyQt5.QtGui and QWidgets for startup time improvement :
31-
from PyQt5.QtWidgets import QMainWindow, QHBoxLayout, QVBoxLayout, QApplication, QSplashScreen
31+
from PyQt5.QtWidgets import QMainWindow, QApplication, QSplashScreen, QWidget
3232
from PyQt5.QtGui import QPixmap, QFontDatabase
33-
from PyQt5.QtQml import QQmlEngine, qmlRegisterSingletonType, qmlRegisterType, QQmlComponent
34-
from PyQt5.QtQuickWidgets import QQuickWidget
33+
from PyQt5.QtQml import QQmlEngine, qmlRegisterSingletonType, qmlRegisterType
3534
from PyQt5.QtCore import QObject
35+
from PyQt5.QtQuick import QQuickView
3636

3737
import platformdirs
3838

3939
# importing friture.exceptionhandler also installs a temporary exception hook
4040
from friture.exceptionhandler import errorBox, fileexcepthook
4141
import friture
42+
from friture.main_toolbar_view_model import MainToolbarViewModel
4243
from friture.playback.playback_control_view_model import PlaybackControlViewModel
4344
from friture.ui_friture import Ui_MainWindow
4445
from friture.about import About_Dialog # About dialog
@@ -66,7 +67,7 @@
6667
from friture.spectrum_data import Spectrum_Data
6768
from friture.plotFilledCurve import PlotFilledCurve
6869
from friture.filled_curve import FilledCurve
69-
from friture.qml_tools import qml_url, raise_if_error, component_raise_if_error
70+
from friture.qml_tools import qml_url, view_raise_if_error
7071
from friture.generators.sine import Sine_Generator_Settings_View_Model
7172
from friture.generators.white import White_Generator_Settings_View_Model
7273
from friture.generators.pink import Pink_Generator_Settings_View_Model
@@ -113,6 +114,7 @@ def __init__(self):
113114
qmlRegisterType(LevelViewModel, 'Friture', 1, 0, 'LevelViewModel')
114115
qmlRegisterType(PlaybackControlViewModel, 'Friture', 1, 0, 'PlaybackControlViewModel')
115116
qmlRegisterType(MainWindowViewModel, 'Friture', 1, 0, 'MainWindowViewModel')
117+
qmlRegisterType(MainToolbarViewModel, 'Friture', 1, 0, 'MainToolbarViewModel')
116118
qmlRegisterType(Axis, 'Friture', 1, 0, 'Axis')
117119
qmlRegisterType(Curve, 'Friture', 1, 0, 'Curve')
118120
qmlRegisterType(FilledCurve, 'Friture', 1, 0, 'FilledCurve')
@@ -156,35 +158,22 @@ def __init__(self):
156158
self.about_dialog = About_Dialog(self, self.slow_timer)
157159
self.settings_dialog = Settings_Dialog(self)
158160

159-
self.centralQuickWidget = QQuickWidget(self.qml_engine, self)
160-
self.centralQuickWidget.setObjectName("centralQuickWidget")
161-
self.centralQuickWidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
162-
self.centralQuickWidget.setResizeMode(QQuickWidget.SizeRootObjectToView)
163-
self.centralQuickWidget.setSource(qml_url("FritureHost.qml"))
164-
165-
raise_if_error(self.centralQuickWidget)
161+
self._main_window_view_model = MainWindowViewModel(self.qml_engine)
166162

167-
self.hboxLayout = QHBoxLayout(self.ui.centralwidget)
168-
self.hboxLayout.setContentsMargins(0, 0, 0, 0)
169-
self.hboxLayout.addWidget(self.centralQuickWidget)
163+
self.quick_view = QQuickView(self.qml_engine, None)
164+
self.quick_view.setResizeMode(QQuickView.SizeRootObjectToView)
165+
self.quick_view.setInitialProperties({
166+
"main_window_view_model": self._main_window_view_model,
167+
"fixedFont": QFontDatabase.systemFont(QFontDatabase.FixedFont).family()
168+
})
169+
self.quick_view.setSource(qml_url("FritureMainWindow.qml"))
170170

171-
qml_component = QQmlComponent(self.qml_engine)
172-
qml_component.loadUrl(qml_url("MainWindow.qml"))
173-
component_raise_if_error(qml_component)
171+
view_raise_if_error(self.quick_view)
174172

175-
self._main_window_view_model = MainWindowViewModel(self.qml_engine)
173+
self.quick_container = QWidget.createWindowContainer(self.quick_view, self)
174+
self.setCentralWidget(self.quick_container)
176175

177-
context = self.qml_engine.rootContext()
178-
central_widget_root = qml_component.createWithInitialProperties(
179-
{
180-
"main_window_view_model": self._main_window_view_model,
181-
"fixedFont": QFontDatabase.systemFont(QFontDatabase.FixedFont).family()
182-
},
183-
context) # type: ignore
184-
central_widget_root.setParent(self.qml_engine)
185-
central_widget_root.setParentItem(self.centralQuickWidget.rootObject()) # type: ignore
186-
187-
self.main_tile_layout = central_widget_root.findChild(QObject, "main_tile_layout")
176+
self.main_tile_layout = self.quick_view.findChild(QObject, "main_tile_layout")
188177
assert self.main_tile_layout is not None, "Main tile layout not found in CentralWidget.qml"
189178

190179
self.level_widget = Levels_Widget(self, self._main_window_view_model.level_view_model)
@@ -201,10 +190,10 @@ def __init__(self):
201190
self.display_timer.timeout.connect(AudioBackend().fetchAudioData)
202191

203192
# toolbar clicks
204-
self.ui.actionStart.triggered.connect(self.timer_toggle)
205-
self.ui.actionSettings.triggered.connect(self.settings_called)
206-
self.ui.actionAbout.triggered.connect(self.about_called)
207-
self.ui.actionNew_dock.triggered.connect(self.dockmanager.new_dock)
193+
self._main_window_view_model.toolbar_view_model.recording_clicked.connect(self.timer_toggle)
194+
self._main_window_view_model.toolbar_view_model.new_dock_clicked.connect(self.dockmanager.new_dock)
195+
self._main_window_view_model.toolbar_view_model.settings_clicked.connect(self.settings_called)
196+
self._main_window_view_model.toolbar_view_model.about_clicked.connect(self.about_called)
208197
self.playback_widget.recording_toggled.connect(self.timer_changed)
209198

210199
# settings changes
@@ -214,15 +203,6 @@ def __init__(self):
214203
# restore the settings and widgets geometries
215204
self.restoreAppState()
216205

217-
# make sure the toolbar is shown
218-
# in case it was closed by mistake (before it was made impossible)
219-
self.ui.toolBar.setVisible(True)
220-
221-
# prevent from hiding or moving the toolbar
222-
self.ui.toolBar.toggleViewAction().setVisible(False)
223-
self.ui.toolBar.setMovable(False)
224-
self.ui.toolBar.setFloatable(False)
225-
226206
# start timers
227207
self.timer_toggle()
228208
self.slow_timer.start()
@@ -341,14 +321,14 @@ def timer_toggle(self):
341321
if self.display_timer.isActive():
342322
self.logger.info("Timer stop")
343323
self.display_timer.stop()
344-
self.ui.actionStart.setText("Start")
324+
self._main_window_view_model.toolbar_view_model.recording = False
345325
self.playback_widget.stop_recording()
346326
AudioBackend().pause()
347327
self.dockmanager.pause()
348328
else:
349329
self.logger.info("Timer start")
350330
self.display_timer.start()
351-
self.ui.actionStart.setText("Stop")
331+
self._main_window_view_model.toolbar_view_model.recording = True
352332
self.playback_widget.start_recording()
353333
AudioBackend().restart()
354334
self.dockmanager.restart()
@@ -358,15 +338,15 @@ def timer_changed(self, recording: bool):
358338
if not recording and self.display_timer.isActive():
359339
self.logger.info("Timer stop")
360340
self.display_timer.stop()
361-
self.ui.actionStart.setText("Start")
341+
self._main_window_view_model.toolbar_view_model.recording = False
362342
self.playback_widget.stop_recording()
363343
AudioBackend().pause()
364344
self.dockmanager.pause()
365345

366346
if recording and not self.display_timer.isActive():
367347
self.logger.info("Timer start")
368348
self.display_timer.start()
369-
self.ui.actionStart.setText("Stop")
349+
self._main_window_view_model.toolbar_view_model.recording = True
370350
self.playback_widget.start_recording()
371351
AudioBackend().restart()
372352
self.dockmanager.restart()

friture/main_toolbar_view_model.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright (C) 2025 Timothée Lecomte
5+
6+
# This file is part of Friture.
7+
#
8+
# Friture is free software: you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License version 3 as published by
10+
# the Free Software Foundation.
11+
#
12+
# Friture is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with Friture. If not, see <http://www.gnu.org/licenses/>.
19+
20+
from PyQt5 import QtCore
21+
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
22+
23+
class MainToolbarViewModel(QtCore.QObject):
24+
recording_changed = pyqtSignal(bool)
25+
recording_clicked = pyqtSignal()
26+
new_dock_clicked = pyqtSignal()
27+
settings_clicked = pyqtSignal()
28+
about_clicked = pyqtSignal()
29+
30+
def __init__(self, parent=None):
31+
super().__init__(parent)
32+
33+
self._recording = True
34+
35+
def get_recording(self) -> bool:
36+
return self._recording
37+
38+
def set_recording(self, recording: bool) -> None:
39+
if self._recording != recording:
40+
self._recording = recording
41+
self.recording_changed.emit(recording)
42+
43+
recording = pyqtProperty(bool, fget=get_recording, fset=set_recording, notify=recording_changed)
44+
45+
@pyqtSlot()
46+
def recording_toggle(self) -> None:
47+
self.recording_clicked.emit()
48+
49+
@pyqtSlot()
50+
def new_dock(self) -> None:
51+
self.new_dock_clicked.emit()
52+
53+
@pyqtSlot()
54+
def settings(self) -> None:
55+
self.settings_clicked.emit()
56+
57+
@pyqtSlot()
58+
def about(self) -> None:
59+
self.about_clicked.emit()

friture/main_window_view_model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from PyQt5.QtCore import pyqtProperty, pyqtSignal
2222

2323
from friture.level_view_model import LevelViewModel
24+
from friture.main_toolbar_view_model import MainToolbarViewModel
2425
from friture.playback.playback_control_view_model import PlaybackControlViewModel
2526

2627
class MainWindowViewModel(QtCore.QObject):
@@ -29,10 +30,15 @@ class MainWindowViewModel(QtCore.QObject):
2930
def __init__(self, parent=None):
3031
super().__init__(parent)
3132

33+
self._toolbar_view_model = MainToolbarViewModel(self)
3234
self._level_view_model = LevelViewModel(self)
3335
self._playback_control_view_model = PlaybackControlViewModel(self)
3436
self._playback_control_enabled = False
3537

38+
@pyqtProperty(MainToolbarViewModel, constant=True) # type: ignore
39+
def toolbar_view_model(self):
40+
return self._toolbar_view_model
41+
3642
@pyqtProperty(LevelViewModel, constant=True) # type: ignore
3743
def level_view_model(self):
3844
return self._level_view_model

friture/qml_tools.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from PyQt5.QtCore import QUrl
55
from PyQt5.QtQuickWidgets import QQuickWidget
66
from PyQt5.QtQml import QQmlComponent
7+
from PyQt5.QtQuick import QQuickView
78

89
def qml_url(fileName):
910
return QUrl.fromLocalFile(qml_path(fileName))
@@ -20,12 +21,17 @@ def qml_path(fileName):
2021

2122
return os.path.join(application_path, fileName)
2223

23-
def raise_if_error(quickWidget):
24+
def raise_if_error(quickWidget: QQuickWidget) -> None:
2425
if quickWidget.status() == QQuickWidget.Error:
2526
errors = '\n'.join(map(lambda x: x.toString(), quickWidget.errors()))
2627
raise Exception("QML error(s): %s" % (errors))
2728

28-
def component_raise_if_error(quickWidget):
29-
if quickWidget.status() == QQmlComponent.Error:
30-
errors = '\n'.join(map(lambda x: x.toString(), quickWidget.errors()))
29+
def view_raise_if_error(quickView: QQuickView) -> None:
30+
if quickView.status() == QQuickView.Error:
31+
errors = '\n'.join(map(lambda x: x.toString(), quickView.errors()))
32+
raise Exception("QML error(s): %s" % (errors))
33+
34+
def component_raise_if_error(component: QQmlComponent) -> None:
35+
if component.status() == QQmlComponent.Error:
36+
errors = '\n'.join(map(lambda x: x.toString(), component.errors()))
3137
raise Exception("QML error(s): %s" % (errors))

0 commit comments

Comments
 (0)