import QtQuick import qs.Common import qs.Services import qs.Widgets StyledRect { id: root required property string outputName required property var outputData property bool isConnected: outputData?.connected ?? false width: parent.width height: settingsColumn.implicitHeight + Theme.spacingM * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, isConnected ? 0.5 : 0.3) border.color: Theme.withAlpha(Theme.outline, 0.3) border.width: 1 opacity: isConnected ? 1.0 : 0.7 Column { id: settingsColumn anchors.fill: parent anchors.margins: Theme.spacingM spacing: Theme.spacingS Row { width: parent.width spacing: Theme.spacingM DankIcon { name: root.isConnected ? "desktop_windows" : "desktop_access_disabled" size: Theme.iconSize - 4 color: root.isConnected ? Theme.primary : Theme.surfaceVariantText anchors.verticalCenter: parent.verticalCenter } Column { width: parent.width - Theme.iconSize - Theme.spacingM - (disconnectedBadge.visible ? disconnectedBadge.width + Theme.spacingS : 0) spacing: 2 StyledText { text: DisplayConfigState.getOutputDisplayName(root.outputData, root.outputName) font.pixelSize: Theme.fontSizeMedium font.weight: Font.Medium color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText } StyledText { text: (root.outputData?.model ?? "") + (root.outputData?.make ? " - " + root.outputData.make : "") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText } } Rectangle { id: disconnectedBadge visible: !root.isConnected width: disconnectedText.implicitWidth + Theme.spacingM height: disconnectedText.implicitHeight + Theme.spacingXS radius: height / 2 color: Theme.withAlpha(Theme.outline, 0.3) anchors.verticalCenter: parent.verticalCenter StyledText { id: disconnectedText text: I18n.tr("Disconnected") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText anchors.centerIn: parent } } } DankDropdown { width: parent.width text: I18n.tr("Resolution & Refresh") visible: root.isConnected currentValue: { const pendingMode = DisplayConfigState.getPendingValue(root.outputName, "mode"); if (pendingMode) return pendingMode; const data = DisplayConfigState.outputs[root.outputName]; if (!data?.modes || data?.current_mode === undefined) return "Auto"; const mode = data.modes[data.current_mode]; return mode ? DisplayConfigState.formatMode(mode) : "Auto"; } options: { const data = DisplayConfigState.outputs[root.outputName]; if (!data?.modes) return ["Auto"]; const opts = []; for (var i = 0; i < data.modes.length; i++) { opts.push(DisplayConfigState.formatMode(data.modes[i])); } return opts; } onValueChanged: value => DisplayConfigState.setPendingChange(root.outputName, "mode", value) } StyledText { visible: !root.isConnected text: I18n.tr("Configuration will be preserved when this display reconnects") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap width: parent.width } Row { width: parent.width spacing: Theme.spacingM visible: root.isConnected Column { width: (parent.width - Theme.spacingM) / 2 spacing: Theme.spacingXS StyledText { text: I18n.tr("Scale") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText } Item { id: scaleContainer width: parent.width height: scaleDropdown.visible ? scaleDropdown.height : scaleInput.height property bool customMode: false property string currentScale: { const pendingScale = DisplayConfigState.getPendingValue(root.outputName, "scale"); if (pendingScale !== undefined) return parseFloat(pendingScale.toFixed(2)).toString(); const scale = DisplayConfigState.outputs[root.outputName]?.logical?.scale ?? 1.0; return parseFloat(scale.toFixed(2)).toString(); } DankDropdown { id: scaleDropdown width: parent.width dropdownWidth: parent.width visible: !scaleContainer.customMode currentValue: scaleContainer.currentScale options: { const standard = ["0.5", "0.75", "1", "1.25", "1.5", "1.75", "2", "2.5", "3", I18n.tr("Custom...")]; const current = scaleContainer.currentScale; if (standard.slice(0, -1).includes(current)) return standard; const opts = [...standard.slice(0, -1), current, standard[standard.length - 1]]; return opts.sort((a, b) => { if (a === I18n.tr("Custom...")) return 1; if (b === I18n.tr("Custom...")) return -1; return parseFloat(a) - parseFloat(b); }); } onValueChanged: value => { if (value === I18n.tr("Custom...")) { scaleContainer.customMode = true; scaleInput.text = scaleContainer.currentScale; scaleInput.forceActiveFocus(); scaleInput.selectAll(); return; } DisplayConfigState.setPendingChange(root.outputName, "scale", parseFloat(value)); } } DankTextField { id: scaleInput width: parent.width height: 40 visible: scaleContainer.customMode placeholderText: "0.5 - 4.0" function applyValue() { const val = parseFloat(text); if (isNaN(val) || val < 0.25 || val > 4) { text = scaleContainer.currentScale; scaleContainer.customMode = false; return; } DisplayConfigState.setPendingChange(root.outputName, "scale", parseFloat(val.toFixed(2))); scaleContainer.customMode = false; } onAccepted: applyValue() onEditingFinished: applyValue() Keys.onEscapePressed: { text = scaleContainer.currentScale; scaleContainer.customMode = false; } } } } Column { width: (parent.width - Theme.spacingM) / 2 spacing: Theme.spacingXS StyledText { text: I18n.tr("Transform") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText } DankDropdown { width: parent.width dropdownWidth: parent.width currentValue: { const pendingTransform = DisplayConfigState.getPendingValue(root.outputName, "transform"); if (pendingTransform) return DisplayConfigState.getTransformLabel(pendingTransform); const data = DisplayConfigState.outputs[root.outputName]; return DisplayConfigState.getTransformLabel(data?.logical?.transform ?? "Normal"); } options: [I18n.tr("Normal"), I18n.tr("90°"), I18n.tr("180°"), I18n.tr("270°"), I18n.tr("Flipped"), I18n.tr("Flipped 90°"), I18n.tr("Flipped 180°"), I18n.tr("Flipped 270°")] onValueChanged: value => DisplayConfigState.setPendingChange(root.outputName, "transform", DisplayConfigState.getTransformValue(value)) } } } DankToggle { width: parent.width text: I18n.tr("Variable Refresh Rate") visible: root.isConnected && !CompositorService.isDwl && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) checked: { const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr"); if (pendingVrr !== undefined) return pendingVrr; return DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false; } onToggled: checked => DisplayConfigState.setPendingChange(root.outputName, "vrr", checked) } DankToggle { width: parent.width text: I18n.tr("VRR On-Demand") description: I18n.tr("VRR activates only when applications request it") visible: root.isConnected && CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "vrrOnDemand", false) onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "vrrOnDemand", checked) } Rectangle { width: parent.width height: 1 color: Theme.withAlpha(Theme.outline, 0.2) visible: compositorSettingsLoader.active } Loader { id: compositorSettingsLoader width: parent.width active: root.isConnected && compositorSettingsSource !== "" source: compositorSettingsSource property string compositorSettingsSource: { switch (CompositorService.compositor) { case "niri": return "NiriOutputSettings.qml"; case "hyprland": return "HyprlandOutputSettings.qml"; default: return ""; } } onLoaded: { item.outputName = root.outputName; item.outputData = root.outputData; } } } }