summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml352
1 files changed, 352 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml
new file mode 100644
index 0000000..0da1175
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml
@@ -0,0 +1,352 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+StyledRect {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ 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 + deleteButton.width + Theme.spacingS * 2 : 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
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: (root.outputData?.model ?? "") + (root.outputData?.make ? " - " + root.outputData.make : "")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ 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
+ }
+ }
+
+ Rectangle {
+ id: deleteButton
+ visible: !root.isConnected
+ width: 28
+ height: 28
+ radius: Theme.cornerRadius
+ color: deleteArea.containsMouse ? Theme.errorHover : "transparent"
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "delete"
+ size: 18
+ color: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: deleteArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: DisplayConfigState.deleteDisconnectedOutput(root.outputName)
+ }
+ }
+ }
+
+ 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
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ 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
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ 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
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ 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 && !CompositorService.isHyprland && !CompositorService.isNiri && (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)
+ }
+
+ DankDropdown {
+ width: parent.width
+ text: I18n.tr("Variable Refresh Rate")
+ visible: root.isConnected && CompositorService.isHyprland && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
+ options: [I18n.tr("Off"), I18n.tr("On"), I18n.tr("Fullscreen Only")]
+ currentValue: {
+ DisplayConfigState.pendingHyprlandChanges;
+ if (DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "vrrFullscreenOnly", false))
+ return I18n.tr("Fullscreen Only");
+ const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr");
+ const vrrEnabled = pendingVrr !== undefined ? pendingVrr : (DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false);
+ if (vrrEnabled)
+ return I18n.tr("On");
+ return I18n.tr("Off");
+ }
+ onValueChanged: value => {
+ const off = I18n.tr("Off");
+ const fullscreen = I18n.tr("Fullscreen Only");
+ DisplayConfigState.setPendingChange(root.outputName, "vrr", value !== off);
+ DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "vrrFullscreenOnly", value === fullscreen || null);
+ }
+ }
+
+ DankDropdown {
+ width: parent.width
+ text: I18n.tr("Variable Refresh Rate")
+ visible: root.isConnected && CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
+ options: [I18n.tr("Off"), I18n.tr("On"), I18n.tr("On-Demand")]
+ currentValue: {
+ DisplayConfigState.pendingNiriChanges;
+ if (DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "vrrOnDemand", false))
+ return I18n.tr("On-Demand");
+ const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr");
+ const vrrEnabled = pendingVrr !== undefined ? pendingVrr : (DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false);
+ if (!vrrEnabled)
+ return I18n.tr("Off");
+ return I18n.tr("On");
+ }
+ onValueChanged: value => {
+ const off = I18n.tr("Off");
+ const onDemand = I18n.tr("On-Demand");
+ DisplayConfigState.setPendingChange(root.outputName, "vrr", value !== off);
+ DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "vrrOnDemand", value === onDemand || null);
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+}