1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

displays: break monolith config down and allow floats/fix integer

writing (niri)
This commit is contained in:
bbedward
2025-12-16 13:36:00 -05:00
parent e84210e962
commit 55fe463405
10 changed files with 2348 additions and 2150 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: root
width: parent.width
height: warningContent.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
readonly property bool showError: DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included
readonly property bool showSetup: !DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included
color: (showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent"
border.color: (showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent"
border.width: 1
visible: (showError || showSetup) && DisplayConfigState.hasOutputBackend && !DisplayConfigState.checkingInclude
Column {
id: warningContent
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "warning"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: {
if (root.showSetup)
return I18n.tr("First Time Setup");
if (root.showError)
return I18n.tr("Outputs Include Missing");
return "";
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
}
StyledText {
text: {
if (root.showSetup)
return I18n.tr("Click 'Setup' to create the outputs config and add include to your compositor config.");
if (root.showError)
return I18n.tr("dms/outputs config exists but is not included in your compositor config. Display changes won't persist.");
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankButton {
id: fixButton
visible: root.showError || root.showSetup
text: {
if (DisplayConfigState.fixingInclude)
return I18n.tr("Fixing...");
if (root.showSetup)
return I18n.tr("Setup");
return I18n.tr("Fix Now");
}
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: !DisplayConfigState.fixingInclude
anchors.verticalCenter: parent.verticalCenter
onClicked: DisplayConfigState.fixOutputsInclude()
}
}
}
}

View File

@@ -0,0 +1,49 @@
import QtQuick
import qs.Common
Rectangle {
id: root
width: parent.width
height: 280
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
border.color: Theme.outline
border.width: 1
Item {
id: canvas
anchors.fill: parent
anchors.margins: Theme.spacingL
property var bounds: DisplayConfigState.getOutputBounds()
property real scaleFactor: {
if (bounds.width === 0 || bounds.height === 0)
return 0.1;
const padding = Theme.spacingL * 2;
const scaleX = (width - padding) / bounds.width;
const scaleY = (height - padding) / bounds.height;
return Math.min(scaleX, scaleY);
}
property point offset: Qt.point((width - bounds.width * scaleFactor) / 2 - bounds.minX * scaleFactor, (height - bounds.height * scaleFactor) / 2 - bounds.minY * scaleFactor)
Connections {
target: DisplayConfigState
function onAllOutputsChanged() {
canvas.bounds = DisplayConfigState.getOutputBounds();
}
}
Repeater {
model: DisplayConfigState.allOutputs ? Object.keys(DisplayConfigState.allOutputs) : []
delegate: MonitorRect {
required property string modelData
outputName: modelData
outputData: DisplayConfigState.allOutputs[modelData]
canvasScaleFactor: canvas.scaleFactor
canvasOffset: canvas.offset
}
}
}
}

View File

@@ -0,0 +1,158 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
required property string outputName
required property var outputData
required property real canvasScaleFactor
required property point canvasOffset
property bool isConnected: outputData?.connected ?? false
property bool isDragging: false
property point originalLogical: Qt.point(0, 0)
property point snappedLogical: Qt.point(0, 0)
property bool isValidPosition: true
property var physSize: DisplayConfigState.getPhysicalSize(outputData)
property var logicalSize: DisplayConfigState.getLogicalSize(outputData)
x: isDragging ? x : (outputData?.logical?.x ?? 0) * canvasScaleFactor + canvasOffset.x
y: isDragging ? y : (outputData?.logical?.y ?? 0) * canvasScaleFactor + canvasOffset.y
width: logicalSize.w * canvasScaleFactor
height: logicalSize.h * canvasScaleFactor
radius: Theme.cornerRadius
opacity: isConnected ? 1.0 : 0.5
color: {
if (!isConnected)
return Theme.surfaceContainerHighest;
if (!isValidPosition)
return Theme.withAlpha(Theme.error, 0.3);
if (isDragging)
return Theme.withAlpha(Theme.primary, 0.4);
if (dragArea.containsMouse)
return Theme.withAlpha(Theme.primary, 0.2);
return Theme.surfaceContainerHigh;
}
border.color: {
if (!isConnected)
return Theme.outline;
if (!isValidPosition)
return Theme.error;
if (isDragging)
return Theme.primary;
if (CompositorService.getFocusedScreen()?.name === outputName)
return Theme.primary;
return Theme.outline;
}
border.width: isDragging ? 3 : 2
z: isDragging ? 100 : (isConnected ? 1 : 0)
Rectangle {
id: snapPreview
visible: root.isDragging && root.isValidPosition
x: root.snappedLogical.x * root.canvasScaleFactor + root.canvasOffset.x - root.x
y: root.snappedLogical.y * root.canvasScaleFactor + root.canvasOffset.y - root.y
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: "transparent"
border.color: Theme.primary
border.width: 2
opacity: 0.6
}
Column {
anchors.centerIn: parent
spacing: 2
DankIcon {
name: root.isConnected ? "desktop_windows" : "desktop_access_disabled"
size: Math.min(24, Math.min(root.width * 0.3, root.height * 0.25))
color: root.isConnected ? (root.isValidPosition ? Theme.primary : Theme.error) : Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: DisplayConfigState.getOutputDisplayName(root.outputData, root.outputName)
font.pixelSize: Math.max(10, Math.min(14, root.width * 0.12))
font.weight: Font.Medium
color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
elide: Text.ElideMiddle
width: Math.min(implicitWidth, root.width - 8)
}
StyledText {
text: root.isConnected ? (root.physSize.w + "x" + root.physSize.h) : I18n.tr("Disconnected")
font.pixelSize: Math.max(8, Math.min(11, root.width * 0.09))
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
enabled: root.isConnected
cursorShape: !root.isConnected ? Qt.ArrowCursor : (root.isDragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor)
drag.target: root.isConnected ? root : null
drag.axis: Drag.XAndYAxis
drag.threshold: 0
onPressed: mouse => {
if (!root.isConnected)
return;
root.isDragging = true;
root.originalLogical = Qt.point(root.outputData?.logical?.x ?? 0, root.outputData?.logical?.y ?? 0);
root.snappedLogical = root.originalLogical;
root.isValidPosition = true;
}
onPositionChanged: mouse => {
if (!root.isDragging || !root.isConnected)
return;
let posX = Math.round((root.x - root.canvasOffset.x) / root.canvasScaleFactor);
let posY = Math.round((root.y - root.canvasOffset.y) / root.canvasScaleFactor);
const size = DisplayConfigState.getLogicalSize(root.outputData);
const snapped = DisplayConfigState.snapToEdges(root.outputName, posX, posY, size.w, size.h);
root.snappedLogical = snapped;
root.isValidPosition = !DisplayConfigState.checkOverlap(root.outputName, snapped.x, snapped.y, size.w, size.h);
}
onReleased: {
if (!root.isDragging || !root.isConnected)
return;
root.isDragging = false;
const size = DisplayConfigState.getLogicalSize(root.outputData);
const finalX = root.snappedLogical.x;
const finalY = root.snappedLogical.y;
if (DisplayConfigState.checkOverlap(root.outputName, finalX, finalY, size.w, size.h)) {
root.isValidPosition = true;
return;
}
if (finalX === root.originalLogical.x && finalY === root.originalLogical.y)
return;
DisplayConfigState.initOriginalOutputs();
DisplayConfigState.backendUpdateOutputPosition(root.outputName, finalX, finalY);
DisplayConfigState.setPendingChange(root.outputName, "position", {
"x": finalX,
"y": finalY
});
}
}
Drag.active: dragArea.drag.active && root.isConnected
}

View File

@@ -0,0 +1,368 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string outputName: ""
property var outputData: null
property bool expanded: false
width: parent.width
spacing: 0
Rectangle {
width: parent.width
height: headerRow.implicitHeight + Theme.spacingS * 2
color: headerMouse.containsMouse ? Theme.withAlpha(Theme.primary, 0.1) : "transparent"
radius: Theme.cornerRadius / 2
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.expanded ? "expand_more" : "chevron_right"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Compositor Settings")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: headerMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: root.expanded
topPadding: Theme.spacingS
DankToggle {
width: parent.width
text: I18n.tr("Disable Output")
checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "disabled", false)
onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "disabled", checked)
}
DankToggle {
width: parent.width
text: I18n.tr("Focus at Startup")
checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "focusAtStartup", false)
onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "focusAtStartup", checked)
}
DankDropdown {
width: parent.width
text: I18n.tr("Hot Corners")
addHorizontalPadding: true
property var hotCornersData: {
void (DisplayConfigState.pendingNiriChanges);
return DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "hotCorners", null);
}
currentValue: {
if (!hotCornersData)
return I18n.tr("Inherit");
if (hotCornersData.off)
return I18n.tr("Off");
const corners = hotCornersData.corners || [];
if (corners.length === 0)
return I18n.tr("Inherit");
if (corners.length === 4)
return I18n.tr("All");
return I18n.tr("Select...");
}
options: [I18n.tr("Inherit"), I18n.tr("Off"), I18n.tr("All"), I18n.tr("Select...")]
onValueChanged: value => {
switch (value) {
case I18n.tr("Inherit"):
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", null);
break;
case I18n.tr("Off"):
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", {
"off": true
});
break;
case I18n.tr("All"):
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", {
"corners": ["top-left", "top-right", "bottom-left", "bottom-right"]
});
break;
case I18n.tr("Select..."):
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", {
"corners": []
});
break;
}
}
}
Item {
width: parent.width
height: hotCornersGroup.implicitHeight
clip: true
property var hotCornersData: {
void (DisplayConfigState.pendingNiriChanges);
return DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "hotCorners", null);
}
visible: hotCornersData && !hotCornersData.off && hotCornersData.corners !== undefined
DankButtonGroup {
id: hotCornersGroup
anchors.horizontalCenter: parent.horizontalCenter
selectionMode: "multi"
checkEnabled: false
buttonHeight: 32
buttonPadding: parent.width < 400 ? Theme.spacingXS : Theme.spacingM
minButtonWidth: parent.width < 400 ? 28 : 56
textSize: parent.width < 400 ? 11 : Theme.fontSizeMedium
model: [I18n.tr("Top Left"), I18n.tr("Top Right"), I18n.tr("Bottom Left"), I18n.tr("Bottom Right")]
property var cornerKeys: ["top-left", "top-right", "bottom-left", "bottom-right"]
currentSelection: {
const hcData = parent.hotCornersData;
if (!hcData?.corners)
return [];
return hcData.corners.map(key => {
const idx = cornerKeys.indexOf(key);
return idx >= 0 ? model[idx] : null;
}).filter(v => v !== null);
}
onSelectionChanged: (index, selected) => {
const corners = currentSelection.map(label => {
const idx = model.indexOf(label);
return idx >= 0 ? cornerKeys[idx] : null;
}).filter(v => v !== null);
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", {
"corners": corners
});
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.withAlpha(Theme.outline, 0.15)
}
Item {
width: parent.width
height: layoutColumn.implicitHeight
Column {
id: layoutColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Layout Overrides")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("Override global layout settings for this output")
font.pixelSize: Theme.fontSizeSmall
color: Theme.withAlpha(Theme.surfaceVariantText, 0.7)
wrapMode: Text.WordWrap
width: parent.width
}
}
Row {
width: parent.width
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Window Gaps (px)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: parent.width
height: 40
placeholderText: I18n.tr("Inherit")
text: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
if (layout?.gaps === undefined)
return "";
return layout.gaps.toString();
}
onEditingFinished: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {};
const trimmed = text.trim();
if (!trimmed) {
delete layout.gaps;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
return;
}
const val = parseInt(trimmed);
if (isNaN(val) || val < 0)
return;
layout.gaps = val;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout);
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Default Width (%)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: parent.width
height: 40
placeholderText: I18n.tr("Inherit")
text: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
if (!layout?.defaultColumnWidth)
return "";
if (layout.defaultColumnWidth.type !== "proportion")
return "";
const percent = layout.defaultColumnWidth.value * 100;
return parseFloat(percent.toFixed(4)).toString();
}
onEditingFinished: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {};
const trimmed = text.trim().replace("%", "");
if (!trimmed) {
delete layout.defaultColumnWidth;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
return;
}
const val = parseFloat(trimmed);
if (isNaN(val) || val <= 0 || val > 100)
return;
layout.defaultColumnWidth = {
"type": "proportion",
"value": parseFloat((val / 100).toFixed(6))
};
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Preset Widths (%)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "e.g. 33.33, 50, 66.67"
font.pixelSize: Theme.fontSizeSmall
color: Theme.withAlpha(Theme.surfaceVariantText, 0.7)
}
DankTextField {
width: parent.width
height: 40
placeholderText: I18n.tr("Inherit")
text: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
const presets = layout?.presetColumnWidths || [];
if (presets.length === 0)
return "";
return presets.filter(p => p.type === "proportion").map(p => parseFloat((p.value * 100).toFixed(4))).join(", ");
}
onEditingFinished: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {};
const trimmed = text.trim();
if (!trimmed) {
delete layout.presetColumnWidths;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
return;
}
const parts = trimmed.split(/[,\s]+/).filter(s => s);
const presets = [];
for (const part of parts) {
const val = parseFloat(part.replace("%", ""));
if (!isNaN(val) && val > 0 && val <= 100)
presets.push({
"type": "proportion",
"value": parseFloat((val / 100).toFixed(6))
});
}
if (presets.length === 0) {
delete layout.presetColumnWidths;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
return;
}
presets.sort((a, b) => a.value - b.value);
layout.presetColumnWidths = presets;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout);
}
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Center Single Column")
property var layoutData: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null)
checked: layoutData?.alwaysCenterSingleColumn ?? false
onToggled: checked => {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {};
if (checked) {
layout.alwaysCenterSingleColumn = true;
} else {
delete layout.alwaysCenterSingleColumn;
}
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
}
}
}
}

View File

@@ -0,0 +1,54 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: root
width: parent.width
height: messageContent.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: messageContent
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Monitor Configuration")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Display configuration is not available. WLR output management protocol not supported.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
}

View File

@@ -0,0 +1,278 @@
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 "";
default:
return "";
}
}
onLoaded: {
item.outputName = root.outputName;
item.outputData = root.outputData;
}
}
}
}

File diff suppressed because it is too large Load Diff