diff options
| author | Jaime Resano <Jaime.Resano-Aisa@qt.io> | 2025-03-10 17:06:36 +0100 |
|---|---|---|
| committer | Cristián Maureira-Fredes <Cristian.Maureira-Fredes@qt.io> | 2025-07-29 14:12:10 +0200 |
| commit | efd8944b263a099e901ab588bc2d9357554f6ead (patch) | |
| tree | 17e30db34633188558e5878535f6e8b86d9a7895 /examples/tutorials | |
| parent | 7ddd042bf3080ac354ea5c673ba244ea0796117e (diff) | |
Add Qt Design Studio tutorial
Add a new Qt Design Studio tutorial to the documentation. The "Drumpad"
example is added to the examples/tutorials folder, with both initial and
final projects.
The required Sounds are in a directory at the same level as both
tutorials in order to reduce space.
Change-Id: I59fdd662775ae48ee04e663d4aa5aa8cb333fcd5
Reviewed-by: Shyamnath Premnadh <Shyamnath.Premnadh@qt.io>
Diffstat (limited to 'examples/tutorials')
56 files changed, 1882 insertions, 0 deletions
diff --git a/examples/tutorials/drumpad/Sounds/Bongo Loop 125bpm.wav b/examples/tutorials/drumpad/Sounds/Bongo Loop 125bpm.wav Binary files differnew file mode 100644 index 000000000..b90bc45e8 --- /dev/null +++ b/examples/tutorials/drumpad/Sounds/Bongo Loop 125bpm.wav diff --git a/examples/tutorials/drumpad/Sounds/Clap.wav b/examples/tutorials/drumpad/Sounds/Clap.wav Binary files differnew file mode 100644 index 000000000..aceee331c --- /dev/null +++ b/examples/tutorials/drumpad/Sounds/Clap.wav diff --git a/examples/tutorials/drumpad/Sounds/Closed Hat.wav b/examples/tutorials/drumpad/Sounds/Closed Hat.wav Binary files differnew file mode 100644 index 000000000..d062e723a --- /dev/null +++ b/examples/tutorials/drumpad/Sounds/Closed Hat.wav diff --git a/examples/tutorials/drumpad/Sounds/Kick Drum.wav b/examples/tutorials/drumpad/Sounds/Kick Drum.wav Binary files differnew file mode 100644 index 000000000..e28337136 --- /dev/null +++ b/examples/tutorials/drumpad/Sounds/Kick Drum.wav diff --git a/examples/tutorials/drumpad/Sounds/Open Hat.wav b/examples/tutorials/drumpad/Sounds/Open Hat.wav Binary files differnew file mode 100644 index 000000000..e6fcb130c --- /dev/null +++ b/examples/tutorials/drumpad/Sounds/Open Hat.wav diff --git a/examples/tutorials/drumpad/Sounds/Sine Bass Ebm.wav b/examples/tutorials/drumpad/Sounds/Sine Bass Ebm.wav Binary files differnew file mode 100644 index 000000000..5925d0fcf --- /dev/null +++ b/examples/tutorials/drumpad/Sounds/Sine Bass Ebm.wav diff --git a/examples/tutorials/drumpad/final_project/.gitignore b/examples/tutorials/drumpad/final_project/.gitignore new file mode 100644 index 000000000..855f31da5 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +.DS_Store +build/ +deployment/ +pysidedeploy.spec +resources.py +*.autosave +*.dist/ +Dependencies/ +*.qtds +.qmlls.ini diff --git a/examples/tutorials/drumpad/final_project/Drumpad.qmlproject b/examples/tutorials/drumpad/final_project/Drumpad.qmlproject new file mode 100644 index 000000000..591606bce --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad.qmlproject @@ -0,0 +1,69 @@ +// prop: json-converted +// prop: auto-generated + +import QmlProject + +Project { + mainFile: "DrumpadContent/App.qml" + mainUiFile: "DrumpadContent/MainScreen.qml" + targetDirectory: "/opt/Drumpad" + enableCMakeGeneration: false + enablePythonGeneration: true + widgetApp: true + importPaths: [ "." ] + mockImports: [ "Mocks" ] + + qdsVersion: "4.5" + quickVersion: "6.7" + qt6Project: true + qtForMCUs: false + + multilanguageSupport: true + primaryLanguage: "en" + supportedLanguages: [ "en" ] + + Environment { + QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT: "1" + QT_AUTO_SCREEN_SCALE_FACTOR: "1" + QT_ENABLE_HIGHDPI_SCALING: "0" + QT_LOGGING_RULES: "qt.qml.connections=false" + QT_QUICK_CONTROLS_CONF: "qtquickcontrols2.conf" + } + + QmlFiles { + directory: "Drumpad" + } + + QmlFiles { + directory: "DrumpadContent" + } + + QmlFiles { + directory: "Generated" + } + + Files { + directory: "../Sounds" + filter: "*.mp3;*.wav" + } + + QmlFiles { + directory: "Mocks/Audio" + } + + Files { + files: [ + "qtquickcontrols2.conf" + ] + } + + Files { + directory: "Drumpad" + filter: "qmldir" + } + + Files { + directory: "DrumpadContent" + filter: "*.ttf;*.otf" + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad.qrc b/examples/tutorials/drumpad/final_project/Drumpad.qrc new file mode 100644 index 000000000..b856a46a9 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad.qrc @@ -0,0 +1,23 @@ +<RCC> + <qresource> + <file>Drumpad.qmlproject</file> + <file>Drumpad/AvailableSoundsComboBox.qml</file> + <file>Drumpad/CenteredFlow.qml</file> + <file>Drumpad/Constants.qml</file> + <file>Drumpad/PadButton.qml</file> + <file>Drumpad/qmldir</file> + <file>Drumpad/SoundEffectPlayer.qml</file> + <file>Drumpad/StyledSpinBox.qml</file> + <file>Drumpad/VolumeSlider.qml</file> + <file>DrumpadContent/App.qml</file> + <file>DrumpadContent/MainScreen.qml</file> + <file>DrumpadContent/qmldir</file> + <file>qtquickcontrols2.conf</file> + <file>../Sounds/Bongo Loop 125bpm.wav</file> + <file>../Sounds/Clap.wav</file> + <file>../Sounds/Closed Hat.wav</file> + <file>../Sounds/Kick Drum.wav</file> + <file>../Sounds/Open Hat.wav</file> + <file>../Sounds/Sine Bass Ebm.wav</file> + </qresource> +</RCC> diff --git a/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml b/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml new file mode 100644 index 000000000..e0e6a72ba --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml @@ -0,0 +1,111 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Audio + +ComboBox { + id: root + + property string currentFile: currentText ? `../Sounds/${currentText}` : "" + required property int initialIndex + + model: audioFilesModel.getModel() + + background: Rectangle { + border.color: root.pressed ? Constants.primaryColor : Constants.secondaryColor + border.width: root.visualFocus ? 3 : 2 + color: root.pressed ? Constants.secondaryColor : "black" + implicitHeight: 30 + radius: 2 + } + contentItem: Text { + color: "white" + elide: Text.ElideRight + leftPadding: 10 + rightPadding: root.indicator.width + 10 + text: root.displayText + verticalAlignment: Text.AlignVCenter + } + delegate: ItemDelegate { + id: delegate + + required property int index + + highlighted: root.highlightedIndex === index + + background: Rectangle { + color: delegate.highlighted ? Constants.darkGray : "black" + implicitWidth: delegate.contentItem.implicitWidth + width: popup.width + } + contentItem: Text { + anchors.fill: parent + color: delegate.highlighted ? "#ff0000" : "white" + elide: Text.ElideRight + leftPadding: 10 + text: root.model[delegate.index] + verticalAlignment: Text.AlignVCenter + } + } + indicator: Canvas { + id: canvas + + contextType: "2d" + height: 8 + width: 12 + x: root.width - canvas.width - root.rightPadding + y: root.topPadding + (root.availableHeight - canvas.height) / 2 + + onPaint: { + let margin = 2; + context.reset(); + context.lineWidth = 2; + context.strokeStyle = "white"; + context.lineCap = "round"; + context.beginPath(); + context.moveTo(margin, margin); + context.lineTo(width / 2, height - margin); + context.lineTo(width - margin, margin); + context.stroke(); + } + + Connections { + function onPressedChanged() { + canvas.requestPaint(); + } + + target: root + } + } + popup: Popup { + id: popup + + implicitHeight: contentItem.implicitHeight + implicitWidth: 200 + padding: 2 + y: root.height + 2 + + background: Rectangle { + border.color: Constants.primaryColor + border.width: 2 + color: "black" + } + contentItem: ListView { + clip: true + currentIndex: root.highlightedIndex + implicitHeight: Math.min(contentHeight, 200) + model: popup.visible ? root.delegateModel : null + } + } + + Component.onCompleted: { + currentIndex = root.initialIndex % model.length; + } + + AudioFilesModel { + id: audioFilesModel + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml b/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml new file mode 100644 index 000000000..44911c1b2 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick + +// A Flow layout that centers its children horizontally +// Note that the implementation adds unnecessary spacing in rows that are not full +Flow { + property int customMargin: (children.length && (children[0].width + spacing <= parentWidth)) + ? (parentWidth - rowWidth) / 2 + padding + : padding + property int parentWidth: parent.width - 2 * padding + property int rowCount: children.length ? parentWidth / (children[0].width + spacing) : 0 + property int rowWidth: children.length + ? rowCount * children[0].width + (rowCount - 1) * spacing + 2 * padding + : 0 + + anchors { + leftMargin: customMargin + rightMargin: customMargin + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml b/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml new file mode 100644 index 000000000..6afab9c87 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml @@ -0,0 +1,12 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +pragma Singleton +import QtQuick + +QtObject { + readonly property string darkGray: "#333333" + readonly property string mediumGray: "#9B9B9B" + readonly property string primaryColor: "#FF0000" + readonly property string secondaryColor: "#8C0000" +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml b/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml new file mode 100644 index 000000000..b95642692 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml @@ -0,0 +1,110 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Shapes + +Rectangle { + id: root + + property bool isPlaying: false + property bool isError: false + property bool isLoading: false + property int cornerRadius: 10 + signal pressed() + + color: "transparent" + + Shape { + anchors.fill: parent + + ShapePath { + strokeColor: "black" + strokeWidth: 2 + + fillGradient: RadialGradient { + centerRadius: root.height + centerX: root.width / 2 + centerY: root.height / 2 + focalX: centerX + focalY: centerY + + GradientStop { + position: 0 + color: { + if (isError) + return "black"; + if (isLoading) + return "yellow"; + if (isPlaying) + return Qt.darker(Constants.primaryColor, 1.25); + return Qt.darker(Constants.secondaryColor, 1.25); + } + } + GradientStop { + position: 0.5 + color: { + if (isError) + return Constants.darkGray; + if (isLoading) + return "orange"; + if (isPlaying) + return Constants.primaryColor; + return Constants.secondaryColor; + } + } + } + + // Rounded shape path + PathMove { + x: root.cornerRadius + y: 0 + } + PathQuad { + controlX: 0 + controlY: 0 + x: 0 + y: root.cornerRadius + } + PathLine { + x: 0 + y: root.height - root.cornerRadius + } + PathQuad { + controlX: 0 + controlY: root.height + x: root.cornerRadius + y: root.height + } + PathLine { + x: root.width - root.cornerRadius + y: root.height + } + PathQuad { + controlX: root.width + controlY: root.height + x: root.width + y: root.height - root.cornerRadius + } + PathLine { + x: root.width + y: root.cornerRadius + } + PathQuad { + controlX: root.width + controlY: 0 + x: root.width - root.cornerRadius + y: 0 + } + PathLine { + x: root.cornerRadius + y: 0 + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: root.pressed() + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml b/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml new file mode 100644 index 000000000..a50b3306f --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml @@ -0,0 +1,118 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs +import QtMultimedia + +import Drumpad +import Audio + +Rectangle { + id: root + + property string decodingError: "" + required property int index + property int status: SoundEffect.Null + property bool isLoading: status == SoundEffect.Loading + property bool isError: status == SoundEffect.Error || status == SoundEffect.Null + property bool isReady: status == SoundEffect.Ready + + function play() { + if (root.status == SoundEffect.Ready) { + audioEngine.play(); + } + } + + color: Constants.darkGray + implicitHeight: layout.implicitHeight + 2 * layout.anchors.margins + implicitWidth: layout.implicitWidth + 2 * layout.anchors.margins + radius: 10 + + onDecodingErrorChanged: { + if (status == SoundEffect.Error && root.decodingError) { + errorMessageDialog.text = root.decodingError; + errorMessageDialog.open(); + } + } + + AudioEngine { + id: audioEngine + + file: availableSoundsComboBox.currentFile + volume: volumeSlider.value + + onDecodingStatusChanged: (status, error) => { + root.status = status; + if (status == SoundEffect.Error && error) { + root.decodingError = error; + } else { + root.decodingError = ""; + } + } + } + + MessageDialog { + id: errorMessageDialog + + buttons: MessageDialog.Ok + title: "Error decoding file" + } + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + RowLayout { + spacing: 10 + + Text { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + color: "white" + text: `Player ${root.index + 1}` + } + AvailableSoundsComboBox { + id: availableSoundsComboBox + + Layout.alignment: Qt.AlignCenter + initialIndex: root.index + } + } + + WaveformItem { + id: waveformItem + + file: audioEngine.file + height: 100 + width: 300 + } + + Row { + Layout.alignment: Qt.AlignCenter + spacing: 10 + + PadButton { + id: padRectangle + height: 100 + width: 100 + isPlaying: audioEngine.isPlaying + isError: root.isError + isLoading: root.isLoading + onPressed: root.play() + } + + VolumeSlider { + id: volumeSlider + + height: padRectangle.height + value: 0.75 + width: 16 + } + } + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml b/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml new file mode 100644 index 000000000..de95412bb --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml @@ -0,0 +1,68 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls + +SpinBox { + id: root + + property int innerPadding: 10 + + height: contentItem.implicitHeight + innerPadding + width: contentItem.width + up.indicator.implicitWidth + down.indicator.implicitWidth + + background: Rectangle { + border.color: Constants.secondaryColor + } + + contentItem: Text { + color: "black" + height: parent.height + horizontalAlignment: Text.AlignHCenter + text: root.textFromValue(root.value, root.locale) + verticalAlignment: Text.AlignVCenter + width: implicitWidth + innerPadding * 2 + } + + down.indicator: Rectangle { + border.color: Constants.secondaryColor + color: root.down.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black" + height: parent.height + implicitWidth: downText.implicitWidth + innerPadding * 2 + x: root.mirrored ? parent.width - width : 0 + + Text { + id: downText + + anchors.fill: parent + color: "white" + font.pixelSize: Math.round(root.font.pixelSize * 1.5) + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + text: "-" + verticalAlignment: Text.AlignVCenter + } + } + + up.indicator: Rectangle { + border.color: Constants.secondaryColor + color: root.up.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black" + height: parent.height + implicitWidth: upText.implicitWidth + innerPadding * 2 + x: root.mirrored ? 0 : parent.width - width + + Text { + id: upText + + anchors.centerIn: parent + anchors.fill: parent + color: "white" + font.pixelSize: Math.round(root.font.pixelSize * 1.5) + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + text: "+" + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml b/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml new file mode 100644 index 000000000..102291213 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls + +Slider { + id: root + + orientation: Qt.Vertical + padding: 0 + + background: Rectangle { + color: Constants.mediumGray + implicitHeight: root.height + implicitWidth: root.width + radius: width / 2 + + Rectangle { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: Qt.lighter(Constants.primaryColor, 1 - (root.visualPosition * 0.3)) + height: (1 - root.visualPosition) * parent.height + (root.visualPosition * handle.height) + radius: parent.width / 2 + width: parent.width + } + } + + handle: Rectangle { + border.color: "#b0b0b0" + border.width: 1 + color: root.pressed ? "#e0e0e0" : "#ffffff" + height: root.width + radius: width / 2 + width: root.width + x: root.availableWidth / 2 - height / 2 + y: root.visualPosition * (root.availableHeight - height) + } +} diff --git a/examples/tutorials/drumpad/final_project/Drumpad/qmldir b/examples/tutorials/drumpad/final_project/Drumpad/qmldir new file mode 100644 index 000000000..7dba78c17 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Drumpad/qmldir @@ -0,0 +1,10 @@ +module Drumpad + +AvailableSoundsComboBox 1.0 AvailableSoundsComboBox.qml +SoundEffectPlayer 1.0 SoundEffectPlayer.qml +CenteredFlow 1.0 CenteredFlow.qml +VolumeSlider 1.0 VolumeSlider.qml +StyledSpinBox 1.0 StyledSpinBox.qml +PadButton 1.0 PadButton.qml + +singleton Constants 1.0 Constants.qml diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml b/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml new file mode 100644 index 000000000..773cbbfc8 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml @@ -0,0 +1,21 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import Drumpad 1.0 + +Window { + id: root + + height: 800 + title: "Drumpad" + visible: true + width: 1200 + + MainScreen { + id: mainScreen + + anchors.fill: parent + } +} diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml b/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml new file mode 100644 index 000000000..fdbd7b66d --- /dev/null +++ b/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml @@ -0,0 +1,99 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Drumpad +import Audio + +Rectangle { + id: root + + property QtObject soundEffectPlayer: Qt.createComponent("../Drumpad/SoundEffectPlayer.qml", + Component.PreferSynchronous) + + color: "black" + focus: true + + Component.onCompleted: { + // Initialize the default sound effect players + for (var i = 0; i < audioPlayersSpinBox.value; i++) { + root.soundEffectPlayer.createObject(soundEffectPlayersFlow, { + index: i + }); + } + } + Keys.onPressed: event => { + if (event.key < Qt.Key_1 || event.key > Qt.Key_9) { + // Ignore key out of scope + return; + } + + let digit = event.key - Qt.Key_1; + if (digit < soundEffectPlayersFlow.children.length) { + soundEffectPlayersFlow.children[digit].play(); + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + + Row { + id: audioPlayersCountRow + + Layout.alignment: Qt.AlignHCenter + spacing: 5 + + Text { + anchors.verticalCenter: parent.verticalCenter + color: "white" + text: "Audio players:" + } + + StyledSpinBox { + id: audioPlayersSpinBox + + value: 5 + + onValueModified: { + let soundPlayersCount = soundEffectPlayersFlow.children.length; + if (audioPlayersSpinBox.value < soundPlayersCount) { + // Remove extra sound effect players + soundEffectPlayersFlow.children.length = audioPlayersSpinBox.value; + return; + } + + if (audioPlayersSpinBox.value < soundPlayersCount) { + return; + } + // Create more sound effect players + for (var i = soundPlayersCount; i < audioPlayersSpinBox.value; i++) { + root.soundEffectPlayer.createObject(soundEffectPlayersFlow, { + index: i + }); + } + } + } + } + + ScrollView { + Layout.fillHeight: true + Layout.fillWidth: true + contentWidth: width + + background: Rectangle { + color: "#232323" + } + + CenteredFlow { + id: soundEffectPlayersFlow + + anchors.fill: parent + padding: 10 + spacing: 10 + } + } + } +} diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir b/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir new file mode 100644 index 000000000..f1f34c528 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir @@ -0,0 +1,4 @@ +module DrumpadContent + +App 1.0 App.qml +MainScreen 1.0 MainScreen.qml diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml b/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml new file mode 100644 index 000000000..4bfbc24f3 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml @@ -0,0 +1,27 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtMultimedia + +Item { + id: root + + property double volume + property url file + + MediaPlayer { + id: player + source: file + audioOutput: AudioOutput {} + } + + onVolumeChanged : { + console.log("Mock: VolumeChanaged ", volume ) + } + + function play() { + console.log("Mock: play()") + player.play() + } +} diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml b/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml new file mode 100644 index 000000000..fcb6041bb --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml @@ -0,0 +1,13 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Rectangle { + id: root + width: 1920 + height: 1080 + color: "blue" + property url file +} diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir b/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir new file mode 100644 index 000000000..189d68e6b --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir @@ -0,0 +1,3 @@ +module Audio +AudioEngine 1.0 AudioEngine.qml +WaveformItem 1.0 WaveformItem.qml diff --git a/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml b/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml new file mode 100644 index 000000000..7e4ff22ad --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml @@ -0,0 +1,8 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +Item { + getFiles: function() { + console.log("AudioFilesModel mock: getFiles()") + } +} diff --git a/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir b/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir new file mode 100644 index 000000000..2d8e98995 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir @@ -0,0 +1,2 @@ +module Components +AudioFilesModel 1.0 AudioFilesModel.qml diff --git a/examples/tutorials/drumpad/final_project/Python/audio/__init__.py b/examples/tutorials/drumpad/final_project/Python/audio/__init__.py new file mode 100644 index 000000000..817bc3e1e --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/audio/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from .audio_engine import AudioEngine +from .waveform_item import WaveformItem +from .audio_files_model import AudioFilesModel diff --git a/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py b/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py new file mode 100644 index 000000000..daf2b356c --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py @@ -0,0 +1,65 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtQml import QmlElement +from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl +from PySide6.QtMultimedia import QSoundEffect + +from autogen.settings import project_root + +QML_IMPORT_NAME = "Audio" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class AudioEngine(QObject): + volumeChanged = Signal() + fileChanged = Signal() + isPlayingChanged = Signal() + decodingStatusChanged = Signal(QSoundEffect.Status, str) + + def __init__(self, parent=None): + super().__init__(parent) + self._sound_effect = QSoundEffect() + self._sound_effect.playingChanged.connect(self.isPlayingChanged.emit) # + self._sound_effect.statusChanged.connect(self.reportStatus) + + def reportStatus(self): + if self._sound_effect.status() == QSoundEffect.Status.Error: + self.decodingStatusChanged.emit( + QSoundEffect.Status.Error, + f"Error decoding file: {self._sound_effect.source().path()}", + ) + else: + self.decodingStatusChanged.emit(self._sound_effect.status(), "") + + @Slot(result=None) + def play(self): + self._sound_effect.play() + + def volume(self): + return self._sound_effect.volume() + + def setVolume(self, value): + self._sound_effect.setVolume(value) + self.volumeChanged.emit() + + def file(self): + return self._sound_effect.source() + + def setFile(self, value: QUrl): + if self._sound_effect.source() == value or value.isEmpty(): + return + + if "__compiled__" in globals(): + self._sound_effect.setSource(f"qrc:/{value.toString()}") + else: + self._sound_effect.setSource(f"file:{project_root / value.toString()}") + self.fileChanged.emit() + + def isPlaying(self): + return self._sound_effect.isPlaying() + + volume = Property(float, volume, setVolume, notify=volumeChanged) + file = Property(QUrl, file, setFile, notify=fileChanged) + isPlaying = Property(bool, isPlaying, notify=isPlayingChanged) diff --git a/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py b/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py new file mode 100644 index 000000000..bdf7cd61e --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py @@ -0,0 +1,29 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from pathlib import Path + +from PySide6.QtCore import QObject, Slot, QDirIterator +from PySide6.QtQml import QmlElement + +from autogen.settings import project_root + + +QML_IMPORT_NAME = "Audio" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class AudioFilesModel(QObject): + @Slot(result=list) + def getModel(self): + if "__compiled__" in globals(): + resource_prefix = ":/Sounds/" + iterator = QDirIterator(resource_prefix, QDirIterator.Subdirectories) + audio_files = [] + while iterator.hasNext(): + resource = iterator.next() + audio_files.append(resource.split(resource_prefix)[-1]) + return audio_files + + return list(p.name for p in Path(project_root / ".." / "Sounds").glob("*.wav")) diff --git a/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py b/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py new file mode 100644 index 000000000..d3ce0f430 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py @@ -0,0 +1,113 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import struct + +from PySide6.QtCore import Qt, Property, QUrl, Signal, QFile, QPointF +from PySide6.QtGui import QPen, QPainter +from PySide6.QtMultimedia import QAudioFormat, QAudioDecoder +from PySide6.QtQml import QmlElement +from PySide6.QtQuick import QQuickPaintedItem + +QML_IMPORT_NAME = "Audio" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class WaveformItem(QQuickPaintedItem): + + fileChanged = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self._waveformData = [] + self._background_color = Qt.black + + audio_format = QAudioFormat() + audio_format.setChannelCount(1) + audio_format.setSampleRate(44100) + audio_format.setSampleFormat(QAudioFormat.Float) + + self._file_url: QUrl | None = None + self._audio_file: QFile | None = None + + self._decoder = QAudioDecoder() + self._decoder.setAudioFormat(audio_format) + + self._decoder.bufferReady.connect(self.onBufferReady) + self._decoder.finished.connect(self.decoderFinished) + + def file(self) -> QUrl | None: + return self._file_url + + def setFile(self, value: QUrl): + if self._decoder.source() == value: + return + + if self._audio_file and self._audio_file.isOpen(): + self._audio_file.close() + + self._waveformData = [] + self._decoder.stop() + + self._file_url = value + if "__compiled__" in globals(): + path = self._file_url.toString().replace("qrc:/", ":/") + else: + path = self._file_url.path() + self._audio_file = QFile(path) + self._audio_file.open(QFile.ReadOnly) + self._decoder.setSourceDevice(self._audio_file) + self._decoder.start() + self.fileChanged.emit() + + def paint(self, painter): + # Fill the bounding rectangle with the specified color + painter.fillRect(self.boundingRect(), self._background_color) + + # If no waveform data is available, draw the text + if not self._waveformData: + painter.setPen(Qt.white) + painter.drawText(self.boundingRect(), Qt.AlignCenter, "Waveform not available") + return + + painter.setRenderHint(QPainter.Antialiasing) + + # Set the pen for drawing the waveform + pen = QPen(Qt.blue) + pen.setWidth(1) + painter.setPen(pen) + + # Get container dimensions + rect = self.boundingRect() + data_size = len(self._waveformData) + + # Calculate step size and center line + x_step = rect.width() / data_size + center_y = rect.height() / 2.0 + + # Draw the waveform as connected lines + for i in range(1, data_size): + x1 = (i - 1) * x_step + y1 = center_y - self._waveformData[i - 1] * center_y + x2 = i * x_step + y2 = center_y - self._waveformData[i] * center_y + painter.drawLine(QPointF(x1, y1), QPointF(x2, y2)) + + @staticmethod + def float_buffer_to_list(data): + # Calculate the number of 32-bit floats in the buffer + float_count = len(data) // 4 # Each float32 is 4 bytes + # Unpack the binary data into a list of floats + return list(struct.unpack(f"{float_count}f", data)) + + def onBufferReady(self): + buffer = self._decoder.read() + data = buffer.constData() + self._waveformData.extend(self.float_buffer_to_list(data)) + self.update() + + file: QUrl = Property(QUrl, file, setFile, notify=fileChanged) + + def decoderFinished(self): + self._audio_file.close() diff --git a/examples/tutorials/drumpad/final_project/Python/autogen/settings.py b/examples/tutorials/drumpad/final_project/Python/autogen/settings.py new file mode 100644 index 000000000..39386a277 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/autogen/settings.py @@ -0,0 +1,39 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +# This file is automatically generated by Qt Design Studio. +import os +import sys +from pathlib import Path + +from PySide6.QtQml import QQmlApplicationEngine + +project_root = Path(__file__).parent.parent.parent + + +def setup_qt_environment(qml_engine: QQmlApplicationEngine): + """ + Load the QML application. Import the compiled resources when the application is deployed. + """ + qml_app_url = "DrumpadContent/App.qml" + + if "__compiled__" in globals(): + # Application has been deployed using pyside6-deploy + try: + import autogen.resources # noqa: F401 + except ImportError: + resource_file = Path(__file__).parent / "resources.py" + print( + f"Error: No compiled resources found in {resource_file.absolute()}\n" + f"Please compile the resources using pyside6-rcc or pyside6-project build", + file=sys.stderr, + ) + sys.exit(1) + + qml_engine.addImportPath(":/") + qml_engine.load(f":/{qml_app_url}") + return + + qml_engine.addImportPath(str(project_root.absolute())) + os.environ["QT_QUICK_CONTROLS_CONF"] = str(project_root / "qtquickcontrols2.conf") + qml_engine.load(str(project_root / qml_app_url)) diff --git a/examples/tutorials/drumpad/final_project/Python/main.py b/examples/tutorials/drumpad/final_project/Python/main.py new file mode 100644 index 000000000..166f25144 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/main.py @@ -0,0 +1,28 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine + +from autogen.settings import setup_qt_environment +from audio import * # noqa: F401,F403 + + +def main(): + app = QGuiApplication(sys.argv) + engine = QQmlApplicationEngine() + + setup_qt_environment(engine) + + if not engine.rootObjects(): + sys.exit(-1) + + ex = app.exec() + del engine + return ex + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/tutorials/drumpad/final_project/Python/pyproject.toml b/examples/tutorials/drumpad/final_project/Python/pyproject.toml new file mode 100644 index 000000000..fcb5bbb9e --- /dev/null +++ b/examples/tutorials/drumpad/final_project/Python/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "Drumpad" + +[tool.pyside6-project] +files = ["main.py", "autogen/settings.py", "audio/audio_files_model.py", "audio/audio_engine.py", "audio/waveform_item.py", "../Drumpad.qmlproject", "../Drumpad.qrc", "../qtquickcontrols2.conf", "../Drumpad/AvailableSoundsComboBox.qml", "../Drumpad/CenteredFlow.qml", "../Drumpad/Constants.qml", "../Drumpad/PadButton.qml", "../Drumpad/qmldir", "../Drumpad/SoundEffectPlayer.qml", "../Drumpad/StyledSpinBox.qml", "../Drumpad/VolumeSlider.qml", "../DrumpadContent/App.qml", "../DrumpadContent/MainScreen.qml", "../DrumpadContent/qmldir", "../Mocks/Audio/AudioEngine.qml", "../Mocks/Audio/qmldir", "../Mocks/Audio/WaveformItem.qml", "../Mocks/Components/AudioFilesModel.qml", "../Mocks/Components/qmldir"] diff --git a/examples/tutorials/drumpad/final_project/doc/final_project.md b/examples/tutorials/drumpad/final_project/doc/final_project.md new file mode 100644 index 000000000..877bc0176 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/doc/final_project.md @@ -0,0 +1,12 @@ +# Drumpad example (Qt Design Studio) - Final project + +This example contains the final [Qt Design Studio] project of the [Qt Design Studio integration tutorial]. +It contains all the necessary files to execute the project, including the Python code developed +along the tutorial. + +For more details, see the [Qt Design Studio integration tutorial]. + +To download the initial project source code, visit {ref}`example_tutorials_drumpad_initial_project`. + +[Qt Design Studio]: https://www.qt.io/product/ui-design-tools/ +[Qt Design Studio integration tutorial]: tutorial_qt_design_studio_integration diff --git a/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf b/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf new file mode 100644 index 000000000..87a95d011 --- /dev/null +++ b/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf @@ -0,0 +1,6 @@ +; This file can be edited to change the style of the application +; Read "Qt Quick Controls 2 Configuration File" for details: +; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html + +[Controls] +Style=Basic diff --git a/examples/tutorials/drumpad/initial_project/.gitignore b/examples/tutorials/drumpad/initial_project/.gitignore new file mode 100644 index 000000000..855f31da5 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +.DS_Store +build/ +deployment/ +pysidedeploy.spec +resources.py +*.autosave +*.dist/ +Dependencies/ +*.qtds +.qmlls.ini diff --git a/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject b/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject new file mode 100644 index 000000000..5e5414ae2 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject @@ -0,0 +1,69 @@ +// prop: json-converted +// prop: auto-generated + +import QmlProject + +Project { + mainFile: "DrumpadContent/App.qml" + mainUiFile: "DrumpadContent/MainScreen.qml" + targetDirectory: "/opt/Drumpad" + enableCMakeGeneration: false + enablePythonGeneration: false + widgetApp: true + importPaths: [ "." ] + mockImports: [ "Mocks" ] + + qdsVersion: "4.5" + quickVersion: "6.7" + qt6Project: true + qtForMCUs: false + + multilanguageSupport: true + primaryLanguage: "en" + supportedLanguages: [ "en" ] + + Environment { + QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT: "1" + QT_AUTO_SCREEN_SCALE_FACTOR: "1" + QT_ENABLE_HIGHDPI_SCALING: "0" + QT_LOGGING_RULES: "qt.qml.connections=false" + QT_QUICK_CONTROLS_CONF: "qtquickcontrols2.conf" + } + + QmlFiles { + directory: "Drumpad" + } + + QmlFiles { + directory: "DrumpadContent" + } + + QmlFiles { + directory: "Generated" + } + + Files { + directory: "../Sounds" + filter: "*.mp3;*.wav" + } + + QmlFiles { + directory: "Mocks/Audio" + } + + Files { + files: [ + "qtquickcontrols2.conf" + ] + } + + Files { + directory: "Drumpad" + filter: "qmldir" + } + + Files { + directory: "DrumpadContent" + filter: "*.ttf;*.otf" + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad.qrc b/examples/tutorials/drumpad/initial_project/Drumpad.qrc new file mode 100644 index 000000000..b856a46a9 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad.qrc @@ -0,0 +1,23 @@ +<RCC> + <qresource> + <file>Drumpad.qmlproject</file> + <file>Drumpad/AvailableSoundsComboBox.qml</file> + <file>Drumpad/CenteredFlow.qml</file> + <file>Drumpad/Constants.qml</file> + <file>Drumpad/PadButton.qml</file> + <file>Drumpad/qmldir</file> + <file>Drumpad/SoundEffectPlayer.qml</file> + <file>Drumpad/StyledSpinBox.qml</file> + <file>Drumpad/VolumeSlider.qml</file> + <file>DrumpadContent/App.qml</file> + <file>DrumpadContent/MainScreen.qml</file> + <file>DrumpadContent/qmldir</file> + <file>qtquickcontrols2.conf</file> + <file>../Sounds/Bongo Loop 125bpm.wav</file> + <file>../Sounds/Clap.wav</file> + <file>../Sounds/Closed Hat.wav</file> + <file>../Sounds/Kick Drum.wav</file> + <file>../Sounds/Open Hat.wav</file> + <file>../Sounds/Sine Bass Ebm.wav</file> + </qresource> +</RCC> diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml b/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml new file mode 100644 index 000000000..e105e2226 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml @@ -0,0 +1,111 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Audio + +ComboBox { + id: root + + property string currentFile: currentText ? `../Sounds/${currentText}` : "" + required property int initialIndex + + model: audioFilesModel.getModel() + + background: Rectangle { + border.color: root.pressed ? Constants.primaryColor : Constants.secondaryColor + border.width: root.visualFocus ? 3 : 2 + color: root.pressed ? Constants.secondaryColor : "black" + implicitHeight: 30 + radius: 2 + } + contentItem: Text { + color: "white" + elide: Text.ElideRight + leftPadding: 10 + rightPadding: root.indicator.width + 10 + text: root.displayText + verticalAlignment: Text.AlignVCenter + } + delegate: ItemDelegate { + id: delegate + + required property int index + + highlighted: root.highlightedIndex === index + + background: Rectangle { + color: delegate.highlighted ? Constants.darkGray : "black" + implicitWidth: delegate.contentItem.implicitWidth + width: popup.width + } + contentItem: Text { + anchors.fill: parent + color: delegate.highlighted ? "#ff0000" : "white" + elide: Text.ElideRight + leftPadding: 10 + text: root.model[delegate.index] + verticalAlignment: Text.AlignVCenter + } + } + indicator: Canvas { + id: canvas + + contextType: "2d" + height: 8 + width: 12 + x: root.width - canvas.width - root.rightPadding + y: root.topPadding + (root.availableHeight - canvas.height) / 2 + + onPaint: { + let margin = 2; + context.reset(); + context.lineWidth = 2; + context.strokeStyle = "white"; + context.lineCap = "round"; + context.beginPath(); + context.moveTo(margin, margin); + context.lineTo(width / 2, height - margin); + context.lineTo(width - margin, margin); + context.stroke(); + } + + Connections { + function onPressedChanged() { + canvas.requestPaint(); + } + + target: root + } + } + popup: Popup { + id: popup + + implicitHeight: contentItem.implicitHeight + implicitWidth: 200 + padding: 2 + y: root.height + 2 + + background: Rectangle { + border.color: Constants.primaryColor + border.width: 2 + color: "black" + } + contentItem: ListView { + clip: true + currentIndex: root.highlightedIndex + implicitHeight: Math.min(contentHeight, 200) + model: popup.visible ? root.delegateModel : null + } + } + + Component.onCompleted: { + currentIndex = root.initialIndex % model.length; + } + + AudioFilesModel { + id: audioFilesModel + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml b/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml new file mode 100644 index 000000000..a5e9fe2cc --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +// A Flow layout that centers its children horizontally +// Note that the implementation adds unnecessary spacing in rows that are not full +Flow { + property int customMargin: (children.length && (children[0].width + spacing <= parentWidth)) + ? (parentWidth - rowWidth) / 2 + padding + : padding + property int parentWidth: parent.width - 2 * padding + property int rowCount: children.length ? parentWidth / (children[0].width + spacing) : 0 + property int rowWidth: children.length + ? rowCount * children[0].width + (rowCount - 1) * spacing + 2 * padding + : 0 + + anchors { + leftMargin: customMargin + rightMargin: customMargin + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml b/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml new file mode 100644 index 000000000..73058229c --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml @@ -0,0 +1,12 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma Singleton +import QtQuick + +QtObject { + readonly property string darkGray: "#333333" + readonly property string mediumGray: "#9B9B9B" + readonly property string primaryColor: "#FF0000" + readonly property string secondaryColor: "#8C0000" +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml b/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml new file mode 100644 index 000000000..e00d77db6 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml @@ -0,0 +1,110 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Shapes + +Rectangle { + id: root + + property bool isPlaying: false + property bool isError: false + property bool isLoading: false + property int cornerRadius: 10 + signal pressed() + + color: "transparent" + + Shape { + anchors.fill: parent + + ShapePath { + strokeColor: "black" + strokeWidth: 2 + + fillGradient: RadialGradient { + centerRadius: root.height + centerX: root.width / 2 + centerY: root.height / 2 + focalX: centerX + focalY: centerY + + GradientStop { + position: 0 + color: { + if (isError) + return "black"; + if (isLoading) + return "yellow"; + if (isPlaying) + return Qt.darker(Constants.primaryColor, 1.25); + return Qt.darker(Constants.secondaryColor, 1.25); + } + } + GradientStop { + position: 0.5 + color: { + if (isError) + return Constants.darkGray; + if (isLoading) + return "orange"; + if (isPlaying) + return Constants.primaryColor; + return Constants.secondaryColor; + } + } + } + + // Rounded shape path + PathMove { + x: root.cornerRadius + y: 0 + } + PathQuad { + controlX: 0 + controlY: 0 + x: 0 + y: root.cornerRadius + } + PathLine { + x: 0 + y: root.height - root.cornerRadius + } + PathQuad { + controlX: 0 + controlY: root.height + x: root.cornerRadius + y: root.height + } + PathLine { + x: root.width - root.cornerRadius + y: root.height + } + PathQuad { + controlX: root.width + controlY: root.height + x: root.width + y: root.height - root.cornerRadius + } + PathLine { + x: root.width + y: root.cornerRadius + } + PathQuad { + controlX: root.width + controlY: 0 + x: root.width - root.cornerRadius + y: 0 + } + PathLine { + x: root.cornerRadius + y: 0 + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: root.pressed() + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml b/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml new file mode 100644 index 000000000..7232b9667 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml @@ -0,0 +1,118 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs +import QtMultimedia + +import Drumpad +import Audio + +Rectangle { + id: root + + property string decodingError: "" + required property int index + property int status: SoundEffect.Null + property bool isLoading: status == SoundEffect.Loading + property bool isError: status == SoundEffect.Error || status == SoundEffect.Null + property bool isReady: status == SoundEffect.Ready + + function play() { + if (root.status == SoundEffect.Ready) { + audioEngine.play(); + } + } + + color: Constants.darkGray + implicitHeight: layout.implicitHeight + 2 * layout.anchors.margins + implicitWidth: layout.implicitWidth + 2 * layout.anchors.margins + radius: 10 + + onDecodingErrorChanged: { + if (status == SoundEffect.Error && root.decodingError) { + errorMessageDialog.text = root.decodingError; + errorMessageDialog.open(); + } + } + + AudioEngine { + id: audioEngine + + file: availableSoundsComboBox.currentFile + volume: volumeSlider.value + + onDecodingStatusChanged: (status, error) => { + root.status = status; + if (status == SoundEffect.Error && error) { + root.decodingError = error; + } else { + root.decodingError = ""; + } + } + } + + MessageDialog { + id: errorMessageDialog + + buttons: MessageDialog.Ok + title: "Error decoding file" + } + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + RowLayout { + spacing: 10 + + Text { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + color: "white" + text: `Player ${root.index + 1}` + } + AvailableSoundsComboBox { + id: availableSoundsComboBox + + Layout.alignment: Qt.AlignCenter + initialIndex: root.index + } + } + + WaveformItem { + id: waveformItem + + file: audioEngine.file + height: 100 + width: 300 + } + + Row { + Layout.alignment: Qt.AlignCenter + spacing: 10 + + PadButton { + id: padRectangle + height: 100 + width: 100 + isPlaying: audioEngine.isPlaying + isError: root.isError + isLoading: root.isLoading + onPressed: root.play() + } + + VolumeSlider { + id: volumeSlider + + height: padRectangle.height + value: 0.75 + width: 16 + } + } + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml b/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml new file mode 100644 index 000000000..c403be0d8 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml @@ -0,0 +1,68 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +SpinBox { + id: root + + property int innerPadding: 10 + + height: contentItem.implicitHeight + innerPadding + width: contentItem.width + up.indicator.implicitWidth + down.indicator.implicitWidth + + background: Rectangle { + border.color: Constants.secondaryColor + } + + contentItem: Text { + color: "black" + height: parent.height + horizontalAlignment: Text.AlignHCenter + text: root.textFromValue(root.value, root.locale) + verticalAlignment: Text.AlignVCenter + width: implicitWidth + innerPadding * 2 + } + + down.indicator: Rectangle { + border.color: Constants.secondaryColor + color: root.down.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black" + height: parent.height + implicitWidth: downText.implicitWidth + innerPadding * 2 + x: root.mirrored ? parent.width - width : 0 + + Text { + id: downText + + anchors.fill: parent + color: "white" + font.pixelSize: Math.round(root.font.pixelSize * 1.5) + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + text: "-" + verticalAlignment: Text.AlignVCenter + } + } + + up.indicator: Rectangle { + border.color: Constants.secondaryColor + color: root.up.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black" + height: parent.height + implicitWidth: upText.implicitWidth + innerPadding * 2 + x: root.mirrored ? 0 : parent.width - width + + Text { + id: upText + + anchors.centerIn: parent + anchors.fill: parent + color: "white" + font.pixelSize: Math.round(root.font.pixelSize * 1.5) + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + text: "+" + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml b/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml new file mode 100644 index 000000000..0fd1eea45 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Slider { + id: root + + orientation: Qt.Vertical + padding: 0 + + background: Rectangle { + color: Constants.mediumGray + implicitHeight: root.height + implicitWidth: root.width + radius: width / 2 + + Rectangle { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: Qt.lighter(Constants.primaryColor, 1 - (root.visualPosition * 0.3)) + height: (1 - root.visualPosition) * parent.height + (root.visualPosition * handle.height) + radius: parent.width / 2 + width: parent.width + } + } + + handle: Rectangle { + border.color: "#b0b0b0" + border.width: 1 + color: root.pressed ? "#e0e0e0" : "#ffffff" + height: root.width + radius: width / 2 + width: root.width + x: root.availableWidth / 2 - height / 2 + y: root.visualPosition * (root.availableHeight - height) + } +} diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/qmldir b/examples/tutorials/drumpad/initial_project/Drumpad/qmldir new file mode 100644 index 000000000..7dba78c17 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Drumpad/qmldir @@ -0,0 +1,10 @@ +module Drumpad + +AvailableSoundsComboBox 1.0 AvailableSoundsComboBox.qml +SoundEffectPlayer 1.0 SoundEffectPlayer.qml +CenteredFlow 1.0 CenteredFlow.qml +VolumeSlider 1.0 VolumeSlider.qml +StyledSpinBox 1.0 StyledSpinBox.qml +PadButton 1.0 PadButton.qml + +singleton Constants 1.0 Constants.qml diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml b/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml new file mode 100644 index 000000000..e1e4b4d3c --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml @@ -0,0 +1,21 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import Drumpad 1.0 + +Window { + id: root + + height: 800 + title: "Drumpad" + visible: true + width: 1200 + + MainScreen { + id: mainScreen + + anchors.fill: parent + } +} diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml b/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml new file mode 100644 index 000000000..2754c4c93 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml @@ -0,0 +1,99 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Drumpad +import Audio + +Rectangle { + id: root + + property QtObject soundEffectPlayer: Qt.createComponent("../Drumpad/SoundEffectPlayer.qml", + Component.PreferSynchronous) + + color: "black" + focus: true + + Component.onCompleted: { + // Initialize the default sound effect players + for (var i = 0; i < audioPlayersSpinBox.value; i++) { + root.soundEffectPlayer.createObject(soundEffectPlayersFlow, { + index: i + }); + } + } + Keys.onPressed: event => { + if (event.key < Qt.Key_1 || event.key > Qt.Key_9) { + // Ignore key out of scope + return; + } + + let digit = event.key - Qt.Key_1; + if (digit < soundEffectPlayersFlow.children.length) { + soundEffectPlayersFlow.children[digit].play(); + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + + Row { + id: audioPlayersCountRow + + Layout.alignment: Qt.AlignHCenter + spacing: 5 + + Text { + anchors.verticalCenter: parent.verticalCenter + color: "white" + text: "Audio players:" + } + + StyledSpinBox { + id: audioPlayersSpinBox + + value: 5 + + onValueModified: { + let soundPlayersCount = soundEffectPlayersFlow.children.length; + if (audioPlayersSpinBox.value < soundPlayersCount) { + // Remove extra sound effect players + soundEffectPlayersFlow.children.length = audioPlayersSpinBox.value; + return; + } + + if (audioPlayersSpinBox.value < soundPlayersCount) { + return; + } + // Create more sound effect players + for (var i = soundPlayersCount; i < audioPlayersSpinBox.value; i++) { + root.soundEffectPlayer.createObject(soundEffectPlayersFlow, { + index: i + }); + } + } + } + } + + ScrollView { + Layout.fillHeight: true + Layout.fillWidth: true + contentWidth: width + + background: Rectangle { + color: "#232323" + } + + CenteredFlow { + id: soundEffectPlayersFlow + + anchors.fill: parent + padding: 10 + spacing: 10 + } + } + } +} diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir b/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir new file mode 100644 index 000000000..f1f34c528 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir @@ -0,0 +1,4 @@ +module DrumpadContent + +App 1.0 App.qml +MainScreen 1.0 MainScreen.qml diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml b/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml new file mode 100644 index 000000000..4bfbc24f3 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml @@ -0,0 +1,27 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtMultimedia + +Item { + id: root + + property double volume + property url file + + MediaPlayer { + id: player + source: file + audioOutput: AudioOutput {} + } + + onVolumeChanged : { + console.log("Mock: VolumeChanaged ", volume ) + } + + function play() { + console.log("Mock: play()") + player.play() + } +} diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml b/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml new file mode 100644 index 000000000..fcb6041bb --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml @@ -0,0 +1,13 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Rectangle { + id: root + width: 1920 + height: 1080 + color: "blue" + property url file +} diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir b/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir new file mode 100644 index 000000000..189d68e6b --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir @@ -0,0 +1,3 @@ +module Audio +AudioEngine 1.0 AudioEngine.qml +WaveformItem 1.0 WaveformItem.qml diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml b/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml new file mode 100644 index 000000000..b06a1e17a --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml @@ -0,0 +1,8 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +Item { + getFiles: function() { + console.log("AudioFilesModel mock: getFiles()") + } +} diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir b/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir new file mode 100644 index 000000000..2d8e98995 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir @@ -0,0 +1,2 @@ +module Components +AudioFilesModel 1.0 AudioFilesModel.qml diff --git a/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject b/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject new file mode 100644 index 000000000..1841cd88e --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject @@ -0,0 +1,21 @@ +{ + "files": ["../Drumpad.qmlproject", + "../Drumpad.qrc", + "../qtquickcontrols2.conf", + "../Drumpad/AvailableSoundsComboBox.qml", + "../Drumpad/CenteredFlow.qml", + "../Drumpad/Constants.qml", + "../Drumpad/PadButton.qml", + "../Drumpad/qmldir", + "../Drumpad/SoundEffectPlayer.qml", + "../Drumpad/StyledSpinBox.qml", + "../Drumpad/VolumeSlider.qml", + "../DrumpadContent/App.qml", + "../DrumpadContent/MainScreen.qml", + "../DrumpadContent/qmldir", + "../Mocks/Audio/AudioEngine.qml", + "../Mocks/Audio/qmldir", + "../Mocks/Audio/WaveformItem.qml", + "../Mocks/Components/AudioFilesModel.qml", + "../Mocks/Components/qmldir"] +} diff --git a/examples/tutorials/drumpad/initial_project/doc/initial_project.md b/examples/tutorials/drumpad/initial_project/doc/initial_project.md new file mode 100644 index 000000000..b874d85e2 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/doc/initial_project.md @@ -0,0 +1,12 @@ +# Drumpad example (Qt Design Studio) - Initial project + +This example contains the initial [Qt Design Studio] project to be used as a starting point for the +[Qt Design Studio integration tutorial]. **It is not an executable project as is**, since it does +**not** contain the required Python code developed along the tutorial. + +For more details, see the [Qt Design Studio integration tutorial]. + +To download the final project source code, visit {ref}`example_tutorials_drumpad_final_project`. + +[Qt Design Studio]: https://www.qt.io/product/ui-design-tools/ +[Qt Design Studio integration tutorial]: tutorial_qt_design_studio_integration diff --git a/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf b/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf new file mode 100644 index 000000000..87a95d011 --- /dev/null +++ b/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf @@ -0,0 +1,6 @@ +; This file can be edited to change the style of the application +; Read "Qt Quick Controls 2 Configuration File" for details: +; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html + +[Controls] +Style=Basic |
