-
Notifications
You must be signed in to change notification settings - Fork 247
QML client rewrite #3457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
QML client rewrite #3457
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import QtQuick 2.15 | ||
| import QtQuick.Controls 2.15 | ||
| import QtQuick.Layouts 1.15 | ||
|
|
||
| ApplicationWindow { | ||
| visible: true | ||
| minimumWidth: 600 | ||
| minimumHeight: 490 | ||
| title: qsTr("Jamulus") | ||
|
|
||
| ColumnLayout { | ||
| anchors.fill: parent | ||
|
|
||
| // Tab Bar | ||
| TabBar { | ||
| id: tabBar | ||
| Layout.fillWidth: true | ||
|
|
||
| TabButton { | ||
| text: qsTr("Home") | ||
| checked: stackLayout.currentIndex === 0 | ||
| onClicked: stackLayout.currentIndex = 0 | ||
| } | ||
|
|
||
| TabButton { | ||
| text: qsTr("Settings") | ||
| checked: stackLayout.currentIndex === 1 | ||
| onClicked: stackLayout.currentIndex = 1 | ||
| } | ||
| } | ||
|
|
||
| // Stack Layout for Tab Content | ||
| StackLayout { | ||
| id: stackLayout | ||
| Layout.fillWidth: true | ||
| Layout.fillHeight: true | ||
|
|
||
| // Home Tab Content | ||
| Item { | ||
| id: homeTab | ||
| Layout.fillWidth: true | ||
| Layout.fillHeight: true | ||
|
|
||
| MainView { | ||
| anchors.fill: parent | ||
| } | ||
| } | ||
|
|
||
| // Settings Tab Content | ||
| Item { | ||
| id: settingsTab | ||
| Layout.fillWidth: true | ||
| Layout.fillHeight: true | ||
|
|
||
| SettingsView { | ||
| // anchors.fill: parent | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Popup { | ||
| id: userPopup | ||
| x: (parent.width - width) / 2 | ||
| y: (parent.height - height) / 2 | ||
| width: 300 | ||
| height: 150 | ||
| visible: _main.userMsg !== "" // Show the popup when there's a message | ||
| closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside | ||
|
|
||
| Rectangle { | ||
| anchors.fill: parent | ||
| color: "white" | ||
|
|
||
| Label { | ||
| id: userMessage | ||
| text: _main.userMsg // Display the message from _main | ||
| anchors.centerIn: parent | ||
| wrapMode: Text.WordWrap | ||
| horizontalAlignment: Text.AlignHCenter | ||
| width: parent.width - 20 // Ensure some padding from the edges | ||
| } | ||
| } | ||
|
|
||
| onVisibleChanged: { | ||
| if (!visible) { | ||
| _main.userMsg = "" | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // React to changes in _client.userMsg | ||
| function onUserMsgChanged(newMsg) { | ||
| if (newMsg !== "") { | ||
| userPopup.open() | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| import QtQuick 2.15 | ||
| import QtQuick.Controls 2.15 | ||
| import QtQuick.Layouts 1.15 | ||
|
|
||
| Rectangle { | ||
| id: channelFader | ||
| border.width: 2 | ||
| radius: 5 | ||
| border.color: "#d9d9d9" | ||
|
|
||
| property var channelModel | ||
| // use ? and ?? operators to suppress errors when channelModel is null | ||
| property string channelUserName: channelModel?.channelUserName ?? "" // channelUserNameText.text | ||
| property double channelLevel: channelModel?.channelMeter.doubleVal ?? 0 | ||
| property bool channelClipStatus: channelModel?.channelMeter.clipStatus ?? false | ||
| property double faderLevel: channelModel?.faderLevel ?? 0 // volumeFader.value | ||
| property double panLevel: channelModel?.panLevel ?? 0 // panKnob.value | ||
| property bool isMuted: channelModel?.isMuted ?? 0 // muteButton.checked | ||
| property bool isSolo: channelModel?.isSolo ?? 0 // soloButton.checked | ||
| property int groupID: channelModel?.groupID ?? 0 // groupId | ||
|
|
||
| ColumnLayout { | ||
| Layout.fillHeight: true | ||
| Layout.fillWidth: true | ||
| anchors.centerIn: parent | ||
| spacing: 3 | ||
|
|
||
| Text { | ||
| id: panKnobLabel | ||
| text: "PAN" | ||
| } | ||
|
|
||
| // Pan Knob | ||
| Dial { | ||
| id: panKnob | ||
| from: 0 | ||
| to: 100 | ||
| value: panLevel // default - set to AUD_MIX_PAN_MAX / 2 | ||
| stepSize: 1 | ||
| Layout.alignment: Qt.AlignHCenter | ||
| Layout.preferredWidth: 32 | ||
| Layout.preferredHeight: 32 | ||
| Layout.bottomMargin: 4 | ||
| background: Rectangle { | ||
| // color: "white" | ||
| border.width: 1 | ||
| border.color: "#4e4e4e" | ||
| radius: width / 2 | ||
| } | ||
|
|
||
| onMoved: { | ||
| channelModel.setPanLevel(value) | ||
| } | ||
| } | ||
|
|
||
| RowLayout { | ||
| spacing: 5 | ||
| Layout.alignment: Qt.AlignHCenter | ||
| Layout.preferredHeight: 200 | ||
| Layout.preferredWidth: 15 | ||
|
|
||
| // Level Meter | ||
| SingleLevelMeter { | ||
| id: levelMeterRectangleUser | ||
| heightPercentage: channelLevel | ||
| chanClipStatus: channelClipStatus | ||
| } | ||
|
|
||
| // Fader | ||
| Slider { | ||
| id: volumeFader | ||
| orientation: Qt.Vertical | ||
| from: 0.0 | ||
| to: 100.0 | ||
| value: faderLevel // FIXME - set to AUD_MIX_FADER_MAX | ||
| Layout.alignment: Qt.AlignHCenter | ||
| Layout.preferredHeight: parent.height | ||
|
|
||
| onValueChanged: { | ||
| channelModel.setFaderLevel(value) | ||
| } | ||
|
Comment on lines
+79
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Use
A user drag also assigns 🐛 Proposed fix- onValueChanged: {
+ onMoved: {
channelModel.setFaderLevel(value)
}🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| // GRPMTSOLO box | ||
| ColumnLayout { | ||
| Layout.alignment: Qt.AlignHCenter | ||
| spacing: 5 | ||
|
|
||
| RowLayout { | ||
| spacing: 5 | ||
| Layout.alignment: Qt.AlignHCenter | ||
|
|
||
| Button { | ||
| id: muteButton | ||
| checkable: true | ||
| text: "M" | ||
| Layout.preferredWidth: 30 | ||
| Layout.preferredHeight: 30 | ||
| font.bold: true | ||
| checked: isMuted | ||
| onClicked: { | ||
| channelModel.setIsMuted(!isMuted) | ||
| } | ||
|
Comment on lines
+101
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Do not combine a A click assigns Drive the visual state from the model only, and send the toggle request from the click. 🐛 Proposed fix Button {
id: muteButton
- checkable: true
text: "M"
Layout.preferredWidth: 30
Layout.preferredHeight: 30
font.bold: true
- checked: isMuted
+ highlighted: isMuted
onClicked: {
channelModel.setIsMuted(!isMuted)
}
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| Button { | ||
| id: soloButton | ||
| checkable: true | ||
| text: "S" | ||
| Layout.preferredWidth: 30 | ||
| Layout.preferredHeight: 30 | ||
| font.bold: true | ||
| checked: isSolo | ||
| onClicked: { | ||
| channelModel.setIsSolo(!isSolo) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Button { | ||
| id: groupChooser | ||
| text: groupID > 0 ? groupID.toString() : "GRP" | ||
| Layout.preferredWidth: 50 | ||
| Layout.preferredHeight: 25 | ||
| Layout.alignment: Qt.AlignHCenter | ||
| font.bold: true | ||
| onClicked: menu.open() | ||
|
|
||
| Menu { | ||
| id: menu | ||
| y: groupChooser.height | ||
|
|
||
| Repeater { | ||
| model: 9 // Total number of items | ||
| delegate: MenuItem { | ||
| property int groupId: index | ||
| text: index === 0 ? "No group" : "Group " + index | ||
| onTriggered: channelModel.setGroupID(groupId) | ||
|
Comment on lines
+135
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Map the menu entries to the C++ group values. The C++ side uses Map index 0 to 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| // Username label | ||
| Rectangle { | ||
| Layout.fillWidth: true | ||
| Layout.preferredHeight: 50 | ||
| Layout.alignment: Qt.AlignHCenter | ||
| border.color: "#d9d9d9" | ||
| border.width: 1 | ||
| radius: 3 | ||
|
|
||
| Label { | ||
| id: channelUserNameText | ||
| anchors.fill: parent | ||
| text: channelUserName | ||
| font.bold: true | ||
| wrapMode: Text.WordWrap | ||
| horizontalAlignment: Text.AlignHCenter | ||
| verticalAlignment: Text.AlignVCenter | ||
| } | ||
| } | ||
|
|
||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import QtQuick 2.15 | ||
| import QtQuick.Controls 2.15 | ||
| import QtQuick.Layouts 1.15 | ||
|
|
||
| Item { | ||
| id: chatBox | ||
| width: 400 | ||
| height: 300 | ||
|
|
||
| ColumnLayout { | ||
| anchors.fill: parent | ||
| spacing: 6 | ||
|
|
||
| ScrollView { | ||
| Layout.fillWidth: true | ||
| Layout.fillHeight: true | ||
|
|
||
| TextArea { | ||
| id: chatArea | ||
| Layout.fillWidth: true | ||
| Layout.fillHeight: true | ||
| readOnly: true | ||
| wrapMode: TextEdit.Wrap | ||
| textFormat: TextEdit.RichText // Enable HTML rendering | ||
| text: _chatBox.chatHistory | ||
| font.pixelSize: 12 | ||
|
|
||
| // Add padding for better text display | ||
| leftPadding: 10 | ||
| rightPadding: 10 | ||
| topPadding: 10 | ||
| bottomPadding: 10 | ||
|
|
||
| // Auto-scroll to bottom when new messages arrive | ||
| onTextChanged: { | ||
| cursorPosition = length | ||
| if (length > 0) { | ||
| // ensure cursor is visible | ||
| chatArea.flickableItem.contentY = chatArea.flickableItem.contentHeight - chatArea.flickableItem.height | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| RowLayout { | ||
| Layout.fillWidth: true | ||
| spacing: 4 | ||
|
|
||
| TextField { | ||
| id: chatInput | ||
| Layout.fillWidth: true | ||
| placeholderText: qsTr("Type a message…") | ||
| onAccepted: { | ||
| _chatBox.sendMessage(text) | ||
| text = "" | ||
| } | ||
| } | ||
|
|
||
| Button { | ||
| text: qsTr("Send") | ||
| onClicked: { | ||
| _chatBox.sendMessage(chatInput.text) | ||
| chatInput.text = "" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Button { | ||
| text: qsTr("Clear Chat") | ||
| onClicked: { | ||
| _chatBox.clearChat() | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🌐 Web query:
Qt QML auto-connect on<Signal> handler for context property object signal💡 Result:
In QML, you cannot use an on handler directly on a context property object [1][2]. Instead, you must use a Connections object to connect to signals emitted by an object exposed via a context property [1][3]. When you expose a C++ object as a context property (e.g., using setContextProperty), it becomes accessible globally by name in QML, allowing you to call its methods, read its properties, or access its signals [1][4]. However, because the context property object is not a nested child of the QML component where you want to handle the signal, the QML engine does not automatically create an on handler scope for it [1][5]. To handle a signal from a context property object, define a Connections element and set its target to the context property name [1][2]: Connections { target: contextPropertyName onSignalName: { // Handle the signal here console.log("Signal received") } } In this syntax, replace contextPropertyName with the name used in setContextProperty and onSignalName with the CamelCase version of your C++ signal name [1][5]. This approach explicitly tells the QML engine to monitor the specified object for signal emissions [1][6].
Citations:
🤖 get_repo_knowledge executed:
get_repo_knowledge jamulussoftware/jamulus /tmp/coderabbit-repo-knowledge/jamulussoftware-jamulus-e32ae91cLength of output: 2628
🏁 Script executed:
Repository: jamulussoftware/jamulus
Length of output: 5611
🌐 Web query:
site:doc.qt.io QML context property signal handler Connections element onSignal function context property💡 Result:
In QML, the Connections element is the recommended way to connect to signals from objects, including those injected via context properties [1][2]. To handle a signal within a Connections element, you should use the function syntax rather than the older signal handler (onSignal) syntax [3][4]. Using the function syntax is preferred because defining signal handlers directly (e.g., onDataChanged) within a Connections element is deprecated and may trigger warnings [4]. Example using the function syntax: Connections { target: contextPropertyObject function onDataChanged { console.log("The data changed!") } } Key points for context properties and Connections: 1. Target assignment: Set the target property of the Connections element to the context property (e.g., target: applicationData) [1][3]. 2. Function syntax: Use function onSignalName {} to define the handler [3][4]. This ensures better scoping and avoids issues associated with deprecated handler styles [4]. 3. Scope: When using a function, you can access the context property directly inside the function body, as it remains available within the scope of the QML file [4]. 4. Deprecation: Specifying signal handlers without the function keyword (e.g., onDataChanged:...) is discouraged for backward compatibility only and should be avoided in modern QML code [3][4]. If you mix both styles, the function definitions will be ignored in favor of the older handler style [3].
Citations:
Remove the unused
onUserMsgChangedfunction._mainis a context property backed byCClient, so itsuserMsgChanged()signal does not connect to this root-level function. The signal emits no argument, and thePopup.visiblebinding already reacts to_main.userMsg. Remove the function and its stale comment. NoConnectionselement is needed.🤖 Prompt for AI Agents