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:
1247
quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml
Normal file
1247
quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
quickshell/Modules/Settings/DisplayConfig/MonitorCanvas.qml
Normal file
49
quickshell/Modules/Settings/DisplayConfig/MonitorCanvas.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
quickshell/Modules/Settings/DisplayConfig/MonitorRect.qml
Normal file
158
quickshell/Modules/Settings/DisplayConfig/MonitorRect.qml
Normal 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
|
||||
}
|
||||
368
quickshell/Modules/Settings/DisplayConfig/NiriOutputSettings.qml
Normal file
368
quickshell/Modules/Settings/DisplayConfig/NiriOutputSettings.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
278
quickshell/Modules/Settings/DisplayConfig/OutputCard.qml
Normal file
278
quickshell/Modules/Settings/DisplayConfig/OutputCard.qml
Normal 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
@@ -234,23 +234,82 @@ PanelWindow {
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
id: detailsText
|
||||
text: ToastService.currentDetails
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
switch (ToastService.currentLevel) {
|
||||
case ToastService.levelError:
|
||||
case ToastService.levelWarn:
|
||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||
default:
|
||||
return Theme.surfaceText;
|
||||
Item {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: detailsText.implicitHeight
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: ToastService.currentDetails.length > 0
|
||||
|
||||
StyledText {
|
||||
id: detailsText
|
||||
text: ToastService.currentDetails
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
switch (ToastService.currentLevel) {
|
||||
case ToastService.levelError:
|
||||
case ToastService.levelWarn:
|
||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||
default:
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
anchors.left: parent.left
|
||||
anchors.right: copyDetailsButton.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: copyDetailsButton
|
||||
iconName: "content_copy"
|
||||
iconSize: Theme.iconSizeSmall
|
||||
iconColor: {
|
||||
switch (ToastService.currentLevel) {
|
||||
case ToastService.levelError:
|
||||
case ToastService.levelWarn:
|
||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||
default:
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
buttonSize: Theme.iconSizeSmall + 8
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
property bool showTooltip: false
|
||||
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["dms", "cl", "copy", ToastService.currentDetails]);
|
||||
showTooltip = true;
|
||||
detailsTooltipTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: detailsTooltipTimer
|
||||
interval: 1500
|
||||
onTriggered: copyDetailsButton.showTooltip = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: copyDetailsButton.showTooltip
|
||||
width: detailsTooltipLabel.implicitWidth + 16
|
||||
height: detailsTooltipLabel.implicitHeight + 8
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
y: -height - 4
|
||||
x: -width / 2 + copyDetailsButton.width / 2
|
||||
|
||||
StyledText {
|
||||
id: detailsTooltipLabel
|
||||
anchors.centerIn: parent
|
||||
text: root.copiedText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
visible: ToastService.currentDetails.length > 0
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -1236,13 +1236,19 @@ Singleton {
|
||||
let block = ` layout {\n`;
|
||||
if (layout.gaps !== undefined)
|
||||
block += ` gaps ${layout.gaps}\n`;
|
||||
if (layout.defaultColumnWidth?.type === "proportion")
|
||||
block += ` default-column-width { proportion ${layout.defaultColumnWidth.value}; }\n`;
|
||||
if (layout.defaultColumnWidth?.type === "proportion") {
|
||||
const val = layout.defaultColumnWidth.value;
|
||||
const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString();
|
||||
block += ` default-column-width { proportion ${formatted}; }\n`;
|
||||
}
|
||||
if (layout.presetColumnWidths && layout.presetColumnWidths.length > 0) {
|
||||
block += ` preset-column-widths {\n`;
|
||||
for (const preset of layout.presetColumnWidths) {
|
||||
if (preset.type === "proportion")
|
||||
block += ` proportion ${preset.value}\n`;
|
||||
if (preset.type === "proportion") {
|
||||
const val = preset.value;
|
||||
const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString();
|
||||
block += ` proportion ${formatted}\n`;
|
||||
}
|
||||
}
|
||||
block += ` }\n`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user