diff --git a/qml/ConsoleSection.qml b/qml/ConsoleSection.qml new file mode 100644 index 0000000..508f821 --- /dev/null +++ b/qml/ConsoleSection.qml @@ -0,0 +1,179 @@ +import QtCore +import QtQuick.Controls.Universal +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Score.UI as UI +import "./Engine.js" as Engine + +GroupBox { + property alias messageMonitor: messageMonitor + SplitView.preferredHeight: 200 + SplitView.minimumHeight: 80 + visible: appSettings.monitorVisible + title: "Message Monitor" + topPadding: label.height + + background: Rectangle { + color: "#2a2a2a" + border.color: "#3a3a3a" + radius: 4 + } + + label: Label { + text: parent.title + color: "#ffffff" + font.bold: true + font.pointSize: skin.fontLarge + padding: 5 + } + + ColumnLayout { + anchors.fill: parent + spacing: 5 + + RowLayout { + Layout.fillWidth: true + spacing: 15 + + CheckBox { + id: logReceivedCheckbox + checked: appSettings.logReceivedMessages + onToggled: appSettings.logReceivedMessages = checked + + indicator: Rectangle { + x: 0 + anchors.verticalCenter: parent.contentItem.verticalCenter + implicitWidth: 18 + implicitHeight: 18 + color: parent.checked ? "#4a8a4a" : "#3a3a3a" + border.color: "#5a5a5a" + radius: 2 + + Label { + anchors.centerIn: parent + text: "✓" + color: "#ffffff" + visible: parent.parent.checked + font.pointSize: 10 + } + } + + contentItem: Label { + text: "Log Received" + color: "#ffffff" + leftPadding: logReceivedCheckbox.indicator.width + 6 + font.pointSize: skin.fontSmall + } + } + + CheckBox { + id: logSentCheckbox + checked: appSettings.logSentMessages + onToggled: appSettings.logSentMessages = checked + + indicator: Rectangle { + x: 0 + anchors.verticalCenter: parent.contentItem.verticalCenter + implicitWidth: 18 + implicitHeight: 18 + color: parent.checked ? "#4a8a4a" : "#3a3a3a" + border.color: "#5a5a5a" + radius: 2 + + Label { + anchors.centerIn: parent + text: "✓" + color: "#ffffff" + visible: parent.parent.checked + font.pointSize: 10 + } + } + + contentItem: Label { + text: "Log Sent" + color: "#ffffff" + leftPadding: logSentCheckbox.indicator.width + 6 + font.pointSize: skin.fontSmall + } + } + + Label { + text: "Rate limit (milliseconds):" + color: "#ffffff" + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + + TextField { + id: rateLimitField + Layout.preferredWidth: 80 + text: 1000. * appSettings.monitorInterval + color: acceptableInput ? "#fff" : "#f00" + font.pointSize: skin.fontMedium + + background: Rectangle { + color: "#3a3a3a" + border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" + radius: 2 + } + validator: IntValidator { + bottom: 0 + top: 1000 + } + onTextChanged: appSettings.monitorInterval = parseFloat(rateLimitField.text) / 1000. + } + + Item { + Layout.fillWidth: true + } + + Button { + text: "Clear" + Layout.preferredWidth: 60 + Layout.preferredHeight: 22 + onClicked: messageMonitor.clear() + + background: Rectangle { + color: parent.hovered ? "#5a5a5a" : "#4a4a4a" + border.color: "#6a6a6a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontSmall + } + } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + background: Rectangle { + color: "#1a1a1a" + radius: 2 + } + + TextArea { + id: messageMonitor + readOnly: true + selectByMouse: true + color: "#00ff00" + font.family: skin.fontMonospace + font.pointSize: skin.fontSmall + wrapMode: TextArea.Wrap + padding: 8 + + background: Rectangle { + color: "transparent" + } + } + } + } +} diff --git a/qml/Engine.js b/qml/Engine.js new file mode 100644 index 0000000..9c9af58 --- /dev/null +++ b/qml/Engine.js @@ -0,0 +1,421 @@ +function restoreSavedSettings() { + inputPortField.text = appSettings.listenPort; + + // Restore saved output devices + try { + const savedOutputs = JSON.parse(appSettings.savedOutputDevices); + for (let output of savedOutputs) { + createOutputDevice(output.name, output.host, output.port, output.type); + // Restore active state after creation + if (output.active === false) { + outputDevices[outputDevices.length - 1].active = false; + updateOutputList(); + } + } + } catch (e) { + console.log("Could not restore saved outputs:", e); + } +} + +function saveOutputDevices() { + const toSave = outputDevices.map(function (d) { + return { + name: d.name, + host: d.host, + port: d.port, + type: d.type, + active: d.active + }; + }); + appSettings.savedOutputDevices = JSON.stringify(toSave); +} + +var g_lastMessageTimestamp = 0; +function rateLimitLog() +{ + const ts = Util.timestamp(); + if((ts - g_lastMessageTimestamp) < appSettings.monitorInterval) + return false; + g_lastMessageTimestamp = ts; + return true; +} + +function logMessage(message) { + messageMonitor.append(message); + // Update monitor + if (messageMonitor.lineCount > 15) { + messageMonitor.remove(0, messageMonitor.text.indexOf('\n') + 1); + } +} + +function onInputValueReceived(address, value) { + if (appSettings.logReceivedMessages && messageMonitor.visible && rateLimitLog()) { + logMessage(`IN: ${address} = ${JSON.stringify(value)}`); + } + + // Only process /spat/serv messages from ControlGRIS + if (address !== "/spat/serv") { + return; + } + + // Parse ControlGRIS message + if (value.length < 2) { + return; + } + + const command = value[0]; + const sourceIndex = value[1]; + + // Route to all active outputs + for (let output of outputDevices) { + if (output.active) { + const mapped = mapControlGRISMessage(command, sourceIndex, value, output.type); + if (mapped && mapped.length > 0) { + // Send each mapped message to output device + for (let msg of mapped) { + let full_address = `${output.name}:${msg.address}`; + Device.write(full_address, msg.value); + if (appSettings.logSentMessages && messageMonitor.visible && rateLimitLog()) { + logMessage(`OUT: ${full_address} = ${JSON.stringify(msg.value)}`); + } + } + } + } + } +} + +function mapControlGRISMessage(command, sourceIndex, value, outputType) { + const messages = []; + + switch (command) { + case "pol": // Polar coordinates in radians + if (value.length >= 7) { + const azimuth = value[2]; + const elevation = value[3]; + const radius = value[4]; + const hspan = value[5]; + const vspan = value[6]; + + messages.push(...mapPolarToOutput(sourceIndex, azimuth, elevation, radius, hspan, vspan, outputType, false)); + } + break; + case "deg": // Polar coordinates in degrees + if (value.length >= 7) { + const azimuth = value[2] * Math.PI / 180.0; // Convert to radians for internal processing + const elevation = value[3] * Math.PI / 180.0; + const radius = value[4]; + const hspan = value[5]; + const vspan = value[6]; + + messages.push(...mapPolarToOutput(sourceIndex, azimuth, elevation, radius, hspan, vspan, outputType, true)); + } + break; + case "car": // Cartesian coordinates + if (value.length >= 7) { + const x = value[2]; + const y = value[3]; + const z = value[4]; + const hspan = value[5]; + const vspan = value[6]; + + messages.push(...mapCartesianToOutput(sourceIndex, x, y, z, hspan, vspan, outputType)); + } + break; + case "clr": // Clear source position + messages.push(...mapClearToOutput(sourceIndex, outputType)); + break; + case "alg": // Algorithm selection (hybrid mode) + if (value.length >= 3) { + const algorithm = value[2]; + messages.push(...mapAlgorithmToOutput(sourceIndex, algorithm, outputType)); + } + break; + } + + return messages; +} + +function mapPolarToOutput(sourceIndex, azimuth, elevation, radius, hspan, vspan, outputType, isDegrees) { + const messages = []; + + switch (outputType) { + case "SpatGRIS": + // SpatGRIS score implementation expects individual position values + messages.push({ + address: `/${sourceIndex}/azimuth`, + value: azimuth + }); + messages.push({ + address: `/${sourceIndex}/elevation`, + value: elevation + }); + messages.push({ + address: `/${sourceIndex}/distance`, + value: radius + }); + if (hspan !== undefined && vspan !== undefined) { + messages.push({ + address: `/${sourceIndex}/hspan`, + value: hspan + }); + messages.push({ + address: `/${sourceIndex}/vspan`, + value: vspan + }); + } + break; + case "ADM-OSC": + // ADM-OSC uses spherical coordinates in degrees + const admAzimuth = azimuth * 180.0 / Math.PI; + const admElevation = elevation * 180.0 / Math.PI; + + messages.push({ + address: `/adm/obj/${sourceIndex}/azim`, + value: admAzimuth + }); + messages.push({ + address: `/adm/obj/${sourceIndex}/elev`, + value: admElevation + }); + messages.push({ + address: `/adm/obj/${sourceIndex}/dist`, + value: radius + }); + if (hspan !== undefined) { + messages.push({ + address: `/adm/obj/${sourceIndex}/w`, + value: hspan * 360 // Convert to degrees + }); + } + if (vspan !== undefined) { + messages.push({ + address: `/adm/obj/${sourceIndex}/h`, + value: vspan * 180 // Convert to degrees + }); + } + break; + case "SPAT Revolution": + // SPAT uses /source/N/aed format with degrees + messages.push({ + address: `/source/${sourceIndex}/aed`, + value: [azimuth * 180.0 / Math.PI // Convert to degrees + , elevation * 180.0 / Math.PI, radius * 100 // SPAT uses percentage (0-100) + ] + }); + if (hspan !== undefined && vspan !== undefined) { + messages.push({ + address: `/source/${sourceIndex}/spread`, + value: (hspan + vspan) / 2 * 100 // Average spread as percentage + }); + } + break; + } + + return messages; +} + +function mapCartesianToOutput(sourceIndex, x, y, z, hspan, vspan, outputType) { + const messages = []; + + switch (outputType) { + case "SpatGRIS": + // SpatGRIS score implementation expects individual coordinates + messages.push({ + address: `/${sourceIndex}/position`, + value: [x, y, z] + }); + if (hspan !== undefined && vspan !== undefined) { + messages.push({ + address: `/${sourceIndex}/hspan`, + value: hspan + }); + messages.push({ + address: `/${sourceIndex}/vspan`, + value: vspan + }); + } + break; + case "ADM-OSC": + // ADM-OSC uses cartesian coordinates + messages.push({ + address: `/adm/obj/${sourceIndex}/xyz`, + value: [x, y, z] + }); + if (hspan !== undefined) { + messages.push({ + address: `/adm/obj/${sourceIndex}/w`, + value: hspan * 360 // Convert to degrees + }); + } + if (vspan !== undefined) { + messages.push({ + address: `/adm/obj/${sourceIndex}/h`, + value: vspan * 180 // Convert to degrees + }); + } + break; + case "SPAT Revolution": + // SPAT uses /source/N/xyz format + messages.push({ + address: `/source/${sourceIndex}/xyz`, + value: [x, y, z] + }); + if (hspan !== undefined && vspan !== undefined) { + messages.push({ + address: `/source/${sourceIndex}/spread`, + value: (hspan + vspan) / 2 * 100 // Average spread as percentage + }); + } + break; + } + + return messages; +} + +function mapClearToOutput(sourceIndex, outputType) { + const messages = []; + + switch (outputType) { + case "SpatGRIS": + messages.push({ + address: `/${sourceIndex}/x`, + value: 0 + }); + messages.push({ + address: `/${sourceIndex}/y`, + value: 0 + }); + messages.push({ + address: `/${sourceIndex}/z`, + value: 0 + }); + break; + case "ADM-OSC": + messages.push({ + address: `/adm/obj/${sourceIndex}/x`, + value: 0 + }); + messages.push({ + address: `/adm/obj/${sourceIndex}/y`, + value: 0 + }); + messages.push({ + address: `/adm/obj/${sourceIndex}/z`, + value: 0 + }); + break; + case "SPAT Revolution": + messages.push({ + address: `/source/${sourceIndex}/xyz`, + value: [0, 0, 0] + }); + break; + } + + return messages; +} + +function mapAlgorithmToOutput(sourceIndex, algorithm, outputType) { + const messages = []; + + switch (outputType) { + case "SpatGRIS": + // SpatGRIS might use a different format for algorithm selection + messages.push({ + address: `/${sourceIndex}/algorithm`, + value: algorithm + }); + break; + case "ADM-OSC": + // ADM doesn't typically have algorithm selection + break; + case "SPAT Revolution": + // SPAT has different spatialization modes + const spatMode = algorithm === "dome" ? "dome" : "panning"; + messages.push({ + address: `/source/${sourceIndex}/mode`, + value: spatMode + }); + break; + } + + return messages; +} + +function typeToFormat(type) { + switch (type) { + case "SpatGRIS": + return 0; + case "ADM-OSC": + return 1; + case "SPAT Revolution": + return 2; + default: + return 1; + } +} + +function createOutputDevice(name, host, port, type) { + Score.removeDevice(name); + Score.createDevice(name, "b96e0e26-c932-40a4-9640-782bf357840e", { + "Host": host, + "Port": port, + "InputPort": 0, + "Sources": 128, + "Format": typeToFormat(type), + "Programs": 1 + }); + + outputDevices.push({ + name: name, + host: host, + port: port, + type: type, + active: true + }); + + updateOutputList(); + saveOutputDevices(); +} + +function removeOutputDevice(index) { + if (index >= 0 && index < outputDevices.length) { + Score.removeDevice(outputDevices[index].name); + outputDevices.splice(index, 1); + updateOutputList(); + saveOutputDevices(); + } +} + +function updateOutputList() { + outputListModel.clear(); + for (let output of outputDevices) { + outputListModel.append(output); + } +} + +function createInputDevice(inputPort) { + console.log("Cleared old device"); + if (udpInput) + udpInput.close(); + udpInput = null; + oscInput = null; + console.log("Creating new device", inputPort); + + Qt.callLater(function () { + oscInput = Protocols.osc({ + onOsc: function (a, v) { + onInputValueReceived(a, v); + } + }); + udpInput = Protocols.inboundUDP({ + Transport: { + Bind: "0.0.0.0", + Port: inputPort + }, + onMessage: function (bytes) { + oscInput.processMessage(bytes); + } + }); + }); +} diff --git a/qml/InputSection.qml b/qml/InputSection.qml new file mode 100644 index 0000000..4c7734a --- /dev/null +++ b/qml/InputSection.qml @@ -0,0 +1,86 @@ +import QtCore +import QtQuick.Controls.Universal +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Score.UI as UI +import "./Engine.js" as Engine + +GroupBox { + property alias inputPortField: inputPortField + SplitView.fillWidth: true + SplitView.fillHeight: false + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 100 + title: "Input Configuration (from ControlGRIS)" + topPadding: label.height + + background: Rectangle { + color: "#2a2a2a" + border.color: "#3a3a3a" + radius: 4 + } + + label: Label { + text: parent.title + color: "#ffffff" + font.bold: true + font.pointSize: skin.fontLarge + padding: 5 + } + + RowLayout { + anchors.top: parent.top + anchors.left: parent.left + + Label { + text: "Listen Port:" + color: "#ffffff" + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + + TextField { + id: inputPortField + Layout.preferredWidth: 80 + text: "18032" + color: acceptableInput ? "#fff" : "#f00" + font.pointSize: skin.fontMedium + + background: Rectangle { + color: "#3a3a3a" + border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" + radius: 2 + } + validator: IntValidator { + bottom: 1 + top: 65535 + } + onTextChanged: appSettings.listenPort = inputPortField.text + } + + Button { + text: "Apply" + Layout.preferredWidth: Math.min(80, window.width * 0.1) + onClicked: Engine.createInputDevice(parseInt(inputPortField.text)) + + background: Rectangle { + color: parent.hovered ? "#5a5a5a" : "#4a4a4a" + border.color: "#6a6a6a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + } + + Item { + Layout.fillWidth: true + } + } +} diff --git a/qml/Main.qml b/qml/Main.qml index 6f2ea19..35a101e 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -1,9 +1,10 @@ import QtCore import QtQuick.Controls.Universal -import QtQuick -import QtQuick.Layouts +import QtQuick +import QtQuick.Layouts import QtQuick.Controls import Score.UI as UI +import "./Engine.js" as Engine ApplicationWindow { id: window @@ -12,1034 +13,74 @@ ApplicationWindow { height: 600 title: "OSC Spatialization Router" color: "#1e1e1e" - + property alias skin: style + Settings { id: appSettings category: "OSCRouter" property int listenPort: 18032 property bool logReceivedMessages: true - property bool logSentMessages: true + property bool logSentMessages: false + property bool monitorVisible: false + property real monitorInterval: 0.015 // in seconds property string savedOutputDevices: "[]" } + + Style { + id: style + } + property var inputDevice: null property var outputDevices: [] property var addressMappings: new Map() property var oscInput - property var udpInput - - // Input configuration - Component.onCompleted: { - restoreSavedSettings(); - - // Create input device for receiving from ControlGRIS - createInputDevice(); - } - - function restoreSavedSettings() { - inputPortField.text = appSettings.listenPort - - // Restore saved output devices - try { - const savedOutputs = JSON.parse(appSettings.savedOutputDevices) - for (let output of savedOutputs) { - createOutputDevice(output.name, output.host, output.port, output.type) - // Restore active state after creation - if (output.active === false) { - outputDevices[outputDevices.length - 1].active = false - updateOutputList() - } - } - } catch (e) { - console.log("Could not restore saved outputs:", e) - } - } - - function saveOutputDevices() { - const toSave = outputDevices.map(function(d) { - return { name: d.name, host: d.host, port: d.port, type: d.type, active: d.active } - }) - appSettings.savedOutputDevices = JSON.stringify(toSave) - } - - function createInputDevice() { - console.log("Cleared old device"); - if(udpInput) - udpInput.close(); - udpInput = null; - oscInput = null; - console.log("Creating new device", parseInt(inputPortField.text)); - - Qt.callLater(function() { - oscInput = Protocols.osc({ onOsc:function (a,v) { onInputValueReceived(a,v);} }); - udpInput = Protocols.inboundUDP({ - Transport: { Bind: "0.0.0.0", Port: parseInt(inputPortField.text) } - , onMessage: function(bytes) { oscInput.processMessage(bytes); } - }); - }); - } - - function logMessage(message) { - messageMonitor.append(message) - // Update monitor - if (messageMonitor.lineCount > 15) { - messageMonitor.remove(0, messageMonitor.text.indexOf('\n') + 1) - } - } + property var udpInput - function onInputValueReceived(address, value) { - if (appSettings.logReceivedMessages) { - logMessage(`IN: ${address} = ${JSON.stringify(value)}`) - } - - // Only process /spat/serv messages from ControlGRIS - - if (address !== "/spat/serv") { - return - } - - // Parse ControlGRIS message - if (value.length < 2) { - return - } - - const command = value[0] - const sourceIndex = value[1] + property alias messageMonitor: console_section.messageMonitor + property alias inputPortField: input_section.inputPortField + property alias outputListModel: output_section.outputListModel - // Route to all active outputs - for (let output of outputDevices) { - if (output.active) { - const mapped = mapControlGRISMessage(command, sourceIndex, value, output.type) - if (mapped && mapped.length > 0) { - // Send each mapped message to output device - for (let msg of mapped) { - let full_address = `${output.name}:${msg.address}` - Device.write(full_address, msg.value) - if (appSettings.logSentMessages) { - logMessage(`OUT: ${full_address} = ${JSON.stringify(msg.value)}`) - } - } - } - } - } - } - - function mapControlGRISMessage(command, sourceIndex, value, outputType) { - const messages = [] - - switch(command) { - case "pol": // Polar coordinates in radians - if (value.length >= 7) { - const azimuth = value[2] - const elevation = value[3] - const radius = value[4] - const hspan = value[5] - const vspan = value[6] - - messages.push(...mapPolarToOutput(sourceIndex, azimuth, elevation, radius, hspan, vspan, outputType, false)) - } - break - - case "deg": // Polar coordinates in degrees - if (value.length >= 7) { - const azimuth = value[2] * Math.PI / 180.0 // Convert to radians for internal processing - const elevation = value[3] * Math.PI / 180.0 - const radius = value[4] - const hspan = value[5] - const vspan = value[6] - - messages.push(...mapPolarToOutput(sourceIndex, azimuth, elevation, radius, hspan, vspan, outputType, true)) - } - break - - case "car": // Cartesian coordinates - if (value.length >= 7) { - const x = value[2] - const y = value[3] - const z = value[4] - const hspan = value[5] - const vspan = value[6] - - messages.push(...mapCartesianToOutput(sourceIndex, x, y, z, hspan, vspan, outputType)) - } - break - - case "clr": // Clear source position - messages.push(...mapClearToOutput(sourceIndex, outputType)) - break - - case "alg": // Algorithm selection (hybrid mode) - if (value.length >= 3) { - const algorithm = value[2] - messages.push(...mapAlgorithmToOutput(sourceIndex, algorithm, outputType)) - } - break - } - - return messages - } - - function mapPolarToOutput(sourceIndex, azimuth, elevation, radius, hspan, vspan, outputType, isDegrees) { - const messages = [] - - switch(outputType) { - case "SpatGRIS": - // SpatGRIS score implementation expects individual position values - messages.push({ - address: `/${sourceIndex}/azimuth`, - value: azimuth - }) - messages.push({ - address: `/${sourceIndex}/elevation`, - value: elevation - }) - messages.push({ - address: `/${sourceIndex}/distance`, - value: radius - }) - if (hspan !== undefined && vspan !== undefined) { - messages.push({ - address: `/${sourceIndex}/hspan`, - value: hspan - }) - messages.push({ - address: `/${sourceIndex}/vspan`, - value: vspan - }) - } - break - - case "ADM-OSC": - // ADM-OSC uses spherical coordinates in degrees - const admAzimuth = azimuth * 180.0 / Math.PI - const admElevation = elevation * 180.0 / Math.PI - - messages.push({ - address: `/adm/obj/${sourceIndex}/azim`, - value: admAzimuth - }) - messages.push({ - address: `/adm/obj/${sourceIndex}/elev`, - value: admElevation - }) - messages.push({ - address: `/adm/obj/${sourceIndex}/dist`, - value: radius - }) - if (hspan !== undefined) { - messages.push({ - address: `/adm/obj/${sourceIndex}/w`, - value: hspan * 360 // Convert to degrees - }) - } - if (vspan !== undefined) { - messages.push({ - address: `/adm/obj/${sourceIndex}/h`, - value: vspan * 180 // Convert to degrees - }) - } - break - - case "SPAT Revolution": - // SPAT uses /source/N/aed format with degrees - messages.push({ - address: `/source/${sourceIndex}/aed`, - value: [ - azimuth * 180.0 / Math.PI, // Convert to degrees - elevation * 180.0 / Math.PI, - radius * 100 // SPAT uses percentage (0-100) - ] - }) - if (hspan !== undefined && vspan !== undefined) { - messages.push({ - address: `/source/${sourceIndex}/spread`, - value: (hspan + vspan) / 2 * 100 // Average spread as percentage - }) - } - break - } - - return messages - } - - function mapCartesianToOutput(sourceIndex, x, y, z, hspan, vspan, outputType) { - const messages = [] - - switch(outputType) { - case "SpatGRIS": - // SpatGRIS score implementation expects individual coordinates - messages.push({ - address: `/${sourceIndex}/position`, - value: [x,y,z] - }) - if (hspan !== undefined && vspan !== undefined) { - messages.push({ - address: `/${sourceIndex}/hspan`, - value: hspan - }) - messages.push({ - address: `/${sourceIndex}/vspan`, - value: vspan - }) - } - break - - case "ADM-OSC": - // ADM-OSC uses cartesian coordinates - messages.push({ - address: `/adm/obj/${sourceIndex}/xyz`, - value: [x,y,z] - }) - if (hspan !== undefined) { - messages.push({ - address: `/adm/obj/${sourceIndex}/w`, - value: hspan * 360 // Convert to degrees - }) - } - if (vspan !== undefined) { - messages.push({ - address: `/adm/obj/${sourceIndex}/h`, - value: vspan * 180 // Convert to degrees - }) - } - break - - case "SPAT Revolution": - // SPAT uses /source/N/xyz format - messages.push({ - address: `/source/${sourceIndex}/xyz`, - value: [x, y, z] - }) - if (hspan !== undefined && vspan !== undefined) { - messages.push({ - address: `/source/${sourceIndex}/spread`, - value: (hspan + vspan) / 2 * 100 // Average spread as percentage - }) - } - break - } - - return messages - } - - function mapClearToOutput(sourceIndex, outputType) { - const messages = [] - - switch(outputType) { - case "SpatGRIS": - messages.push({ - address: `/${sourceIndex}/x`, - value: 0 - }) - messages.push({ - address: `/${sourceIndex}/y`, - value: 0 - }) - messages.push({ - address: `/${sourceIndex}/z`, - value: 0 - }) - break - - case "ADM-OSC": - messages.push({ - address: `/adm/obj/${sourceIndex}/x`, - value: 0 - }) - messages.push({ - address: `/adm/obj/${sourceIndex}/y`, - value: 0 - }) - messages.push({ - address: `/adm/obj/${sourceIndex}/z`, - value: 0 - }) - break - - case "SPAT Revolution": - messages.push({ - address: `/source/${sourceIndex}/xyz`, - value: [0, 0, 0] - }) - break - } - - return messages - } - - function mapAlgorithmToOutput(sourceIndex, algorithm, outputType) { - const messages = [] - - switch(outputType) { - case "SpatGRIS": - // SpatGRIS might use a different format for algorithm selection - messages.push({ - address: `/${sourceIndex}/algorithm`, - value: algorithm - }) - break - - case "ADM-OSC": - // ADM doesn't typically have algorithm selection - break - - case "SPAT Revolution": - // SPAT has different spatialization modes - const spatMode = algorithm === "dome" ? "dome" : "panning" - messages.push({ - address: `/source/${sourceIndex}/mode`, - value: spatMode - }) - break - } - - return messages - } - - function typeToFormat(type) { - switch(type) { - case "SpatGRIS": - return 0; - case "ADM-OSC": - return 1; - case "SPAT Revolution": - return 2; - default: - return 1; - } - } - - function createOutputDevice(name, host, port, type) { - Score.removeDevice(name) - Score.createDevice(name - , "b96e0e26-c932-40a4-9640-782bf357840e" - , { - "Host": host, - "Port": port, - "InputPort": 0, - "Sources": 128, - "Format": typeToFormat(type), - "Programs": 1 - } - ); - - outputDevices.push({ - name: name, - host: host, - port: port, - type: type, - active: true - }) + Component.onCompleted: { + Engine.restoreSavedSettings(); - updateOutputList() - saveOutputDevices() + Engine.createInputDevice(appSettings.listenPort); } - function removeOutputDevice(index) { - if (index >= 0 && index < outputDevices.length) { - Score.removeDevice(outputDevices[index].name) - outputDevices.splice(index, 1) - updateOutputList() - saveOutputDevices() - } + header: Item { + width: 1 + height: 5 } - function updateOutputList() { - outputListModel.clear() - for (let output of outputDevices) { - outputListModel.append(output) - } + menuBar: TopMenu { } - ColumnLayout { + SplitView { + Layout.margins: 10 anchors.fill: parent - anchors.margins: 10 - spacing: 10 - - - // Input Configuration - GroupBox { - Layout.fillWidth: true - Layout.minimumHeight: 100 - Layout.preferredHeight: 100 - title: "Input Configuration (from ControlGRIS)" - - background: Rectangle { - color: "#2a2a2a" - border.color: "#3a3a3a" - radius: 4 - } - - label: Label { - text: parent.title - color: "#ffffff" - font.bold: true - font.pixelSize: Math.min(14, window.height * 0.025) - padding: 5 - } + orientation: Qt.Vertical - RowLayout { - anchors.fill: parent - spacing: 10 + handle: Rectangle { + implicitHeight: 6 + color: SplitHandle.pressed ? "#5a5a5a" : SplitHandle.hovered ? "#4a4a4a" : "#3a3a3a" - Label { - text: "Listen Port:" - color: "#ffffff" - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - - TextField { - id: inputPortField - Layout.preferredWidth: Math.min(80, window.width * 0.1) - text: "18032" - color: acceptableInput? "#fff" : "#f00" - font.pixelSize: Math.min(12, window.height * 0.02) - - background: Rectangle { - color: "#3a3a3a" - border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" - radius: 2 - } - validator: IntValidator { bottom: 1; top: 65535; } - onTextChanged: appSettings.listenPort = inputPortField.text - } - - Button { - text: "Apply" - Layout.preferredWidth: Math.min(80, window.width * 0.1) - onClicked: createInputDevice() - - background: Rectangle { - color: parent.hovered ? "#5a5a5a" : "#4a4a4a" - border.color: "#6a6a6a" - radius: 2 - } - - contentItem: Label { - text: parent.text - color: "#ffffff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - } - - Item { Layout.fillWidth: true } + Rectangle { + width: 40 + height: 2 + radius: 1 + color: "#6a6a6a" + anchors.centerIn: parent } } - // Output Configuration - GroupBox { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumHeight: 200 - title: "Output Devices" - - background: Rectangle { - color: "#2a2a2a" - border.color: "#3a3a3a" - radius: 4 - } - - label: Label { - text: parent.title - color: "#ffffff" - font.bold: true - font.pixelSize: Math.min(14, window.height * 0.025) - padding: 5 - } - - ColumnLayout { - anchors.fill: parent - spacing: 10 - - - Item { width: 1; height: 30 } - // Add Output Form - RowLayout { - Layout.fillWidth: true - spacing: 5 - - TextField { - id: outputNameField - Layout.preferredWidth: Math.max(100, Math.min(150, window.width * 0.15)) - color: "#ffffff" - font.pixelSize: Math.min(12, window.height * 0.02) - placeholderText: "(Name)" - placeholderTextColor: "#888" - - background: Rectangle { - color: "#3a3a3a" - border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" - radius: 2 - } - } - - TextField { - id: outputHostField - Layout.preferredWidth: Math.max(100, Math.min(150, window.width * 0.15)) - text: "127.0.0.1" - placeholderText: "IP Address" - color: "#ffffff" - font.pixelSize: Math.min(12, window.height * 0.02) - - background: Rectangle { - color: "#3a3a3a" - border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" - radius: 2 - } - } - - TextField { - id: outputPortField - Layout.preferredWidth: Math.max(60, Math.min(100, window.width * 0.1)) - text: "8000" - placeholderText: "Port" - color: "#ffffff" - font.pixelSize: Math.min(12, window.height * 0.02) - - background: Rectangle { - color: "#3a3a3a" - border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" - radius: 2 - } - } - - ComboBox { - id: outputTypeCombo - Layout.preferredWidth: Math.max(120, Math.min(180, window.width * 0.18)) - model: ["SpatGRIS", "ADM-OSC", "SPAT Revolution"] - currentIndex: 0 - - background: Rectangle { - y: 3 - color: "#3a3a3a" - border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" - radius: 2 - height: outputPortField.height - } - - contentItem: Label { - text: parent.displayText - height: 10 - color: "#ffffff" - verticalAlignment: Text.AlignVCenter - leftPadding: 10 - font.pixelSize: 12 - } - delegate: ItemDelegate { - width: parent.width - - background: Rectangle { - color: parent.hovered ? "#4a4a4a" : "#3a3a3a" - } - - contentItem: Label { - text: modelData - color: "#ffffff" - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - } - /* - indicator: Canvas { - x: parent.width - width - 10 - y: parent.topPadding + (parent.availableHeight - height) / 2 - width: 12 - height: 12 - contextType: "2d" - - onPaint: { - context.reset() - context.moveTo(0, 0) - context.lineTo(width, 0) - context.lineTo(width / 2, height) - context.closePath() - context.fillStyle = "#888888" - context.fill() - } - }*/ - - } - - Button { - text: "Add" - Layout.preferredWidth: Math.max(50, Math.min(80, window.width * 0.08)) - - onClicked: { - console.log(outputNameField.text, outputHostField.text,outputPortField.text) - if (outputNameField.text && outputHostField.text && outputPortField.text) { - createOutputDevice( - outputNameField.text, - outputHostField.text, - parseInt(outputPortField.text), - outputTypeCombo.currentText - ) - outputNameField.clear() - outputPortField.text = "8000" - } - } - - background: Rectangle { - color: parent.hovered ? "#5a9a5a" : "#4a8a4a" - border.color: "#6aaa6a" - radius: 2 - } - - contentItem: Label { - text: parent.text - color: "#ffffff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - } - - Item { Layout.fillWidth: true } - } - - // Output List - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - - background: Rectangle { - color: "#1a1a1a" - radius: 2 - } - - ListView { - model: ListModel { id: outputListModel } - spacing: 2 - - delegate: Rectangle { - width: ListView.view.width - 10 - height: 40 - color: model.active ? "#3a3a3a" : "#2a2a2a" - border.color: "#4a4a4a" - radius: 2 - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 10 - - CheckBox { - Layout.preferredWidth: 20 - Layout.preferredHeight: 20 - checked: model.active - - onToggled: { - outputDevices[index].active = checked - updateOutputList() - saveOutputDevices() - } - - indicator: Rectangle { - implicitWidth: 20 - implicitHeight: 20 - color: parent.checked ? "#4a8a4a" : "#3a3a3a" - border.color: "#5a5a5a" - radius: 2 - - Label { - anchors.centerIn: parent - text: "✓" - color: "#ffffff" - visible: parent.parent.checked - font.pixelSize: Math.min(12, window.height * 0.02) - } - } - } - - Label { - text: model.name - color: "#ffffff" - Layout.preferredWidth: Math.max(80, window.width * 0.15) - elide: Text.ElideRight - font.pixelSize: Math.min(12, window.height * 0.02) - } - - Label { - text: `${model.type} - ${model.host}:${model.port}` - color: "#aaaaaa" - Layout.fillWidth: true - elide: Text.ElideRight - font.pixelSize: Math.min(11, window.height * 0.018) - } - - Button { - Layout.preferredWidth: Math.max(60, Math.min(80, window.width * 0.08)) - Layout.preferredHeight: 25 - text: "Remove" - - onClicked: removeOutputDevice(index) - - background: Rectangle { - color: parent.hovered ? "#9a4a4a" : "#8a3a3a" - border.color: "#aa5a5a" - radius: 2 - } - - contentItem: Label { - text: parent.text - color: "#ffffff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(11, window.height * 0.018) - } - } - } - } - } - } - } + InputSection { + id: input_section } - - // Message Monitor - GroupBox { - Layout.fillWidth: true - Layout.preferredHeight: Math.max(100, Math.min(200, window.height * 0.25)) - Layout.minimumHeight: 80 - title: "Message Monitor" - - background: Rectangle { - color: "#2a2a2a" - border.color: "#3a3a3a" - radius: 4 - } - - label: Label { - text: parent.title - color: "#ffffff" - font.bold: true - font.pixelSize: Math.min(14, window.height * 0.025) - padding: 5 - } - - ColumnLayout { - anchors.fill: parent - spacing: 5 - - RowLayout { - Layout.fillWidth: true - spacing: 15 - - CheckBox { - id: logReceivedCheckbox - checked: appSettings.logReceivedMessages - onToggled: appSettings.logReceivedMessages = checked - - indicator: Rectangle { - x: 0 - anchors.verticalCenter: parent.contentItem.verticalCenter - implicitWidth: 18 - implicitHeight: 18 - color: parent.checked ? "#4a8a4a" : "#3a3a3a" - border.color: "#5a5a5a" - radius: 2 - - Label { - anchors.centerIn: parent - text: "✓" - color: "#ffffff" - visible: parent.parent.checked - font.pixelSize: 10 - } - } - - contentItem: Label { - text: "Log Received" - color: "#ffffff" - leftPadding: logReceivedCheckbox.indicator.width + 6 - font.pixelSize: Math.min(11, window.height * 0.018) - } - } - - CheckBox { - id: logSentCheckbox - checked: appSettings.logSentMessages - onToggled: appSettings.logSentMessages = checked - - indicator: Rectangle { - x: 0 - anchors.verticalCenter: parent.contentItem.verticalCenter - implicitWidth: 18 - implicitHeight: 18 - color: parent.checked ? "#4a8a4a" : "#3a3a3a" - border.color: "#5a5a5a" - radius: 2 - - Label { - anchors.centerIn: parent - text: "✓" - color: "#ffffff" - visible: parent.parent.checked - font.pixelSize: 10 - } - } - - contentItem: Label { - text: "Log Sent" - color: "#ffffff" - leftPadding: logSentCheckbox.indicator.width + 6 - font.pixelSize: Math.min(11, window.height * 0.018) - } - } - - Item { Layout.fillWidth: true } - - Button { - text: "Clear" - Layout.preferredWidth: 60 - Layout.preferredHeight: 22 - onClicked: messageMonitor.clear() - - background: Rectangle { - color: parent.hovered ? "#5a5a5a" : "#4a4a4a" - border.color: "#6a6a6a" - radius: 2 - } - - contentItem: Label { - text: parent.text - color: "#ffffff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(10, window.height * 0.016) - } - } - } - - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - - background: Rectangle { - color: "#1a1a1a" - radius: 2 - } - - TextArea { - id: messageMonitor - readOnly: true - selectByMouse: true - color: "#00ff00" - font.family: "Consolas, Monaco, monospace" - font.pixelSize: Math.min(11, window.height * 0.018) - wrapMode: TextArea.Wrap - padding: 8 - - background: Rectangle { - color: "transparent" - } - } - } - } + OutputSection { + id: output_section } - - // Presets and controls - RowLayout { - Layout.fillWidth: true - Layout.minimumHeight: 30 - spacing: 10 - - Label { - text: "Quick Setup:" - color: "#ffffff" - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - - Button { - Layout.preferredWidth: Math.max(80, Math.min(120, window.width * 0.12)) - text: "SpatGRIS" - onClicked: createOutputDevice("SpatGRIS_1", "127.0.0.1", 18042, "SpatGRIS") - - background: Rectangle { - color: parent.hovered ? "#5a5a5a" : "#4a4a4a" - border.color: "#6a6a6a" - radius: 2 - } - - contentItem: Label { - text: parent.text - color: "#ffffff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - } - - Button { - Layout.preferredWidth: Math.max(80, Math.min(120, window.width * 0.12)) - text: "ADM-OSC" - onClicked: createOutputDevice("ADM_1", "127.0.0.1", 9000, "ADM-OSC") - - background: Rectangle { - color: parent.hovered ? "#5a5a5a" : "#4a4a4a" - border.color: "#6a6a6a" - radius: 2 - } - - contentItem: Label { - text: parent.text - color: "#ffffff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - } - - Button { - Layout.preferredWidth: Math.max(80, Math.min(120, window.width * 0.12)) - text: "SPAT Rev" - onClicked: createOutputDevice("SPAT_1", "127.0.0.1", 8088, "SPAT Revolution") - - background: Rectangle { - color: parent.hovered ? "#5a5a5a" : "#4a4a4a" - border.color: "#6a6a6a" - radius: 2 - } - - contentItem: Label { - text: parent.text - color: "#ffffff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - } - - Item { Layout.fillWidth: true } - - Button { - Layout.preferredWidth: Math.max(70, Math.min(100, window.width * 0.1)) - text: "Clear All" - - onClicked: { - while (outputDevices.length > 0) { - removeOutputDevice(0) - } - } - - background: Rectangle { - color: parent.hovered ? "#9a4a4a" : "#8a3a3a" - border.color: "#aa5a5a" - radius: 2 - } - - contentItem: Label { - text: parent.text - color: "#ffffff" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Math.min(12, window.height * 0.02) - } - } + ConsoleSection { + id: console_section } } } diff --git a/qml/OutputSection.qml b/qml/OutputSection.qml new file mode 100644 index 0000000..5f03c3a --- /dev/null +++ b/qml/OutputSection.qml @@ -0,0 +1,275 @@ +import QtCore +import QtQuick.Controls.Universal +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Score.UI as UI +import "./Engine.js" as Engine + +GroupBox { + property alias outputListModel: outputListModel + SplitView.fillHeight: true + SplitView.minimumHeight: 200 + title: "Output Devices" + topPadding: label.height + + background: Rectangle { + color: "#2a2a2a" + border.color: "#3a3a3a" + radius: 4 + } + + label: Label { + text: parent.title + color: "#ffffff" + font.bold: true + font.pointSize: skin.fontLarge + padding: 5 + } + + ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: 10 + + // Add Output Form + RowLayout { + Layout.fillWidth: true + spacing: 5 + + TextField { + id: outputNameField + Layout.preferredWidth: Math.max(100, Math.min(150, window.width * 0.15)) + color: "#ffffff" + font.pointSize: skin.fontMedium + placeholderText: "(Name)" + placeholderTextColor: "#888" + + background: Rectangle { + color: "#3a3a3a" + border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" + radius: 2 + } + } + + TextField { + id: outputHostField + Layout.preferredWidth: Math.max(100, Math.min(150, window.width * 0.15)) + text: "127.0.0.1" + placeholderText: "IP Address" + color: "#ffffff" + font.pointSize: skin.fontMedium + + background: Rectangle { + color: "#3a3a3a" + border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" + radius: 2 + } + } + + TextField { + id: outputPortField + Layout.preferredWidth: Math.max(60, Math.min(100, window.width * 0.1)) + text: "8000" + placeholderText: "Port" + color: "#ffffff" + font.pointSize: skin.fontMedium + + background: Rectangle { + color: "#3a3a3a" + border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" + radius: 2 + } + } + + ComboBox { + id: outputTypeCombo + Layout.preferredWidth: Math.max(120, Math.min(180, window.width * 0.18)) + model: ["SpatGRIS", "ADM-OSC", "SPAT Revolution"] + currentIndex: 0 + + background: Rectangle { + y: 3 + color: "#3a3a3a" + border.color: parent.focus ? "#5a5a5a" : "#4a4a4a" + radius: 2 + height: outputPortField.height + } + + contentItem: Label { + text: parent.displayText + height: 10 + color: "#ffffff" + verticalAlignment: Text.AlignVCenter + leftPadding: 10 + font.pointSize: skin.fontMedium + } + delegate: ItemDelegate { + width: parent.width + + background: Rectangle { + color: parent.hovered ? "#4a4a4a" : "#3a3a3a" + } + + contentItem: Label { + text: modelData + color: "#ffffff" + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + } + /* + indicator: Canvas { + x: parent.width - width - 10 + y: parent.topPadding + (parent.availableHeight - height) / 2 + width: 12 + height: 12 + contextType: "2d" + + onPaint: { + context.reset() + context.moveTo(0, 0) + context.lineTo(width, 0) + context.lineTo(width / 2, height) + context.closePath() + context.fillStyle = "#888888" + context.fill() + } + }*/ + + } + + Button { + text: "Add" + Layout.preferredWidth: Math.max(50, Math.min(80, window.width * 0.08)) + + onClicked: { + console.log(outputNameField.text, outputHostField.text, outputPortField.text); + if (outputNameField.text && outputHostField.text && outputPortField.text) { + Engine.createOutputDevice(outputNameField.text, outputHostField.text, parseInt(outputPortField.text), outputTypeCombo.currentText); + outputNameField.clear(); + outputPortField.text = "8000"; + } + } + + background: Rectangle { + color: parent.hovered ? "#5a9a5a" : "#4a8a4a" + border.color: "#6aaa6a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + } + + Item { + Layout.fillWidth: true + } + } + + // Output List + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + background: Rectangle { + color: "#1a1a1a" + radius: 2 + } + + ListView { + model: ListModel { + id: outputListModel + } + spacing: 2 + + delegate: Rectangle { + width: ListView.view.width - 10 + height: 40 + color: model.active ? "#3a3a3a" : "#2a2a2a" + border.color: "#4a4a4a" + radius: 2 + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 10 + + CheckBox { + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + checked: model.active + + onToggled: { + outputDevices[index].active = checked; + Engine.updateOutputList(); + Engine.saveOutputDevices(); + } + + indicator: Rectangle { + implicitWidth: 20 + implicitHeight: 20 + color: parent.checked ? "#4a8a4a" : "#3a3a3a" + border.color: "#5a5a5a" + radius: 2 + + Label { + anchors.centerIn: parent + text: "✓" + color: "#ffffff" + visible: parent.parent.checked + font.pointSize: skin.fontMedium + } + } + } + + Label { + text: model.name + color: "#ffffff" + Layout.preferredWidth: Math.max(80, window.width * 0.15) + elide: Text.ElideRight + font.pointSize: skin.fontMedium + } + + Label { + text: `${model.type} - ${model.host}:${model.port}` + color: "#aaaaaa" + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: skin.fontSmall + } + + Button { + Layout.preferredWidth: Math.max(60, Math.min(80, window.width * 0.08)) + Layout.preferredHeight: 25 + text: "Remove" + + onClicked: Engine.removeOutputDevice(index) + + background: Rectangle { + color: parent.hovered ? "#9a4a4a" : "#8a3a3a" + border.color: "#aa5a5a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontSmall + } + } + } + } + } + } + } +} diff --git a/qml/Style.qml b/qml/Style.qml new file mode 100644 index 0000000..b16e7e6 --- /dev/null +++ b/qml/Style.qml @@ -0,0 +1,15 @@ +import QtQml +import QtQuick + +QtObject { + // Font sizes - responsive with maximum caps + // Large: Section titles (bold) + readonly property real fontLarge: 10 + // Medium: Regular text, labels, buttons, inputs + readonly property real fontMedium: 8 + // Small: Secondary text, small buttons, checkbox labels + readonly property real fontSmall: 8 + + // Font families + readonly property string fontMonospace: "Consolas, Monaco, monospace" +} diff --git a/qml/TopMenu.qml b/qml/TopMenu.qml new file mode 100644 index 0000000..f19d51a --- /dev/null +++ b/qml/TopMenu.qml @@ -0,0 +1,129 @@ +import QtCore +import QtQuick.Controls.Universal +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Score.UI as UI +import "./Engine.js" as Engine + +RowLayout { + property Style style + spacing: 10 + + Label { + text: "Quick Setup:" + color: "#ffffff" + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + + Button { + Layout.preferredWidth: Math.max(80, Math.min(120, window.width * 0.12)) + text: "SpatGRIS" + onClicked: Engine.createOutputDevice("SpatGRIS_1", "127.0.0.1", 18042, "SpatGRIS") + + background: Rectangle { + color: parent.hovered ? "#5a5a5a" : "#4a4a4a" + border.color: "#6a6a6a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + } + + Button { + Layout.preferredWidth: Math.max(80, Math.min(120, window.width * 0.12)) + text: "ADM-OSC" + onClicked: Engine.createOutputDevice("ADM_1", "127.0.0.1", 9000, "ADM-OSC") + + background: Rectangle { + color: parent.hovered ? "#5a5a5a" : "#4a4a4a" + border.color: "#6a6a6a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + } + + Button { + Layout.preferredWidth: Math.max(80, Math.min(120, window.width * 0.12)) + text: "SPAT Rev" + onClicked: Engine.createOutputDevice("SPAT_1", "127.0.0.1", 8088, "SPAT Revolution") + + background: Rectangle { + color: parent.hovered ? "#5a5a5a" : "#4a4a4a" + border.color: "#6a6a6a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + } + + Item { + Layout.fillWidth: true + } + + Button { + Layout.preferredWidth: Math.max(70, Math.min(100, window.width * 0.1)) + text: appSettings.monitorVisible ? "Hide Log" : "Show Log" + + onClicked: appSettings.monitorVisible = !appSettings.monitorVisible + + background: Rectangle { + color: appSettings.monitorVisible ? (parent.hovered ? "#5a8a5a" : "#4a7a4a") : (parent.hovered ? "#5a5a5a" : "#4a4a4a") + border.color: appSettings.monitorVisible ? "#6aaa6a" : "#6a6a6a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + } + + Button { + Layout.preferredWidth: Math.max(70, Math.min(100, window.width * 0.1)) + text: "Clear All" + + onClicked: { + while (outputDevices.length > 0) { + Engine.removeOutputDevice(0); + } + } + + background: Rectangle { + color: parent.hovered ? "#9a4a4a" : "#8a3a3a" + border.color: "#aa5a5a" + radius: 2 + } + + contentItem: Label { + text: parent.text + color: "#ffffff" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: skin.fontMedium + } + } +}