1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-12 23:32:50 -04:00

feat(display): Fix and implement display auto-switching with JSON profile storage (#2275)

* feat(display): fix monitor auto config and add output disable guard

* feat(display): fixed some race conditions and sole display getting disabled.

Co-authored-by: Copilot <copilot@github.com>

* feat(display): changes console log to use new log service

* feat(display): fix trailing spaces

* prek run

* add migration, fix missing hyprland HDR parameters, use FileView >
python

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: bbedward <bbedward@gmail.com>
This commit is contained in:
DK
2026-05-05 02:07:24 +09:00
committed by GitHub
parent 7c991bc4e3
commit cfe6e6867e
9 changed files with 907 additions and 259 deletions
+3
View File
@@ -24,6 +24,7 @@ import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts import qs.Modules.DankBar.Popouts
import qs.Modules.Frame import qs.Modules.Frame
import qs.Modules.WorkspaceOverlays import qs.Modules.WorkspaceOverlays
import qs.Modules.Settings.DisplayConfig
import qs.Services import qs.Services
Item { Item {
@@ -304,6 +305,8 @@ Item {
dockRecreateDebounce.start(); dockRecreateDebounce.start();
// Force PolkitService singleton to initialize // Force PolkitService singleton to initialize
PolkitService.polkitAvailable; PolkitService.polkitAvailable;
// Force DisplayConfigState singleton to initialize so auto-config runs at startup
DisplayConfigState.hasOutputBackend;
loginSoundTimer.start(); loginSoundTimer.start();
} }
+9 -4
View File
@@ -1622,13 +1622,15 @@ Item {
for (const id in profiles) { for (const id in profiles) {
const p = profiles[id]; const p = profiles[id];
if (!p.name)
continue;
const flags = []; const flags = [];
if (id === activeId) if (id === activeId)
flags.push("active"); flags.push("active");
if (id === matchedId) if (id === matchedId)
flags.push("matched"); flags.push("matched");
const flagStr = flags.length > 0 ? " [" + flags.join(",") + "]" : ""; const flagStr = flags.length > 0 ? " [" + flags.join(",") + "]" : "";
lines.push(p.name + flagStr + " -> " + JSON.stringify(p.outputSet)); lines.push(p.name + flagStr + " -> " + JSON.stringify(Object.keys(p.outputs)));
} }
if (lines.length === 0) if (lines.length === 0)
@@ -1660,13 +1662,16 @@ Item {
return `PROFILE_SET_SUCCESS: ${profileName}`; return `PROFILE_SET_SUCCESS: ${profileName}`;
} }
// ! TODO - auto profile switching is buggy on niri and other compositors
function toggleAuto(): string { function toggleAuto(): string {
return "ERROR: Auto profile selection is temporarily disabled due to compositor bugs"; SettingsData.displayProfileAutoSelect = !SettingsData.displayProfileAutoSelect;
SettingsData.saveSettings();
if (SettingsData.displayProfileAutoSelect)
DisplayConfigState.applyAutoConfig();
return `Auto profile selection: ${SettingsData.displayProfileAutoSelect ? "enabled" : "disabled"}`;
} }
function status(): string { function status(): string {
const auto = "off"; // disabled for now const auto = SettingsData.displayProfileAutoSelect ? "on" : "off";
const activeId = SettingsData.getActiveDisplayProfile(CompositorService.compositor); const activeId = SettingsData.getActiveDisplayProfile(CompositorService.compositor);
const matchedId = DisplayConfigState.matchedProfile; const matchedId = DisplayConfigState.matchedProfile;
const profiles = DisplayConfigState.validatedProfiles; const profiles = DisplayConfigState.validatedProfiles;
File diff suppressed because it is too large Load Diff
@@ -74,6 +74,8 @@ Column {
DankToggle { DankToggle {
width: parent.width width: parent.width
text: I18n.tr("Disable Output") text: I18n.tr("Disable Output")
enabled: checked || DisplayConfigState.canDisableOutput()
description: (!checked && !DisplayConfigState.canDisableOutput()) ? (Object.keys(DisplayConfigState.outputs).length <= 1 ? I18n.tr("Cannot disable the only output") : I18n.tr("At least one output must remain enabled")) : ""
checked: DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "disabled", false) checked: DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "disabled", false)
onToggled: checked => DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "disabled", checked) onToggled: checked => DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "disabled", checked)
} }
@@ -61,6 +61,8 @@ Column {
DankToggle { DankToggle {
width: parent.width width: parent.width
text: I18n.tr("Disable Output") text: I18n.tr("Disable Output")
enabled: checked || DisplayConfigState.canDisableOutput()
description: (!checked && !DisplayConfigState.canDisableOutput()) ? (Object.keys(DisplayConfigState.outputs).length <= 1 ? I18n.tr("Cannot disable the only output") : I18n.tr("At least one output must remain enabled")) : ""
checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "disabled", false) checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "disabled", false)
onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "disabled", checked) onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "disabled", checked)
} }
@@ -168,7 +168,7 @@ StyledRect {
const pendingScale = DisplayConfigState.getPendingValue(root.outputName, "scale"); const pendingScale = DisplayConfigState.getPendingValue(root.outputName, "scale");
if (pendingScale !== undefined) if (pendingScale !== undefined)
return parseFloat(pendingScale.toFixed(2)).toString(); return parseFloat(pendingScale.toFixed(2)).toString();
const scale = DisplayConfigState.outputs[root.outputName]?.logical?.scale ?? 1.0; const scale = root.outputData?.logical?.scale || 1.0;
return parseFloat(scale.toFixed(2)).toString(); return parseFloat(scale.toFixed(2)).toString();
} }
@@ -251,8 +251,7 @@ StyledRect {
const pendingTransform = DisplayConfigState.getPendingValue(root.outputName, "transform"); const pendingTransform = DisplayConfigState.getPendingValue(root.outputName, "transform");
if (pendingTransform) if (pendingTransform)
return DisplayConfigState.getTransformLabel(pendingTransform); return DisplayConfigState.getTransformLabel(pendingTransform);
const data = DisplayConfigState.outputs[root.outputName]; return DisplayConfigState.getTransformLabel(root.outputData?.logical?.transform ?? "Normal");
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°")] 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)) onValueChanged: value => DisplayConfigState.setPendingChange(root.outputName, "transform", DisplayConfigState.getTransformValue(value))
+120 -30
View File
@@ -15,15 +15,13 @@ Item {
property bool showNewProfileDialog: false property bool showNewProfileDialog: false
property bool showDeleteConfirmDialog: false property bool showDeleteConfirmDialog: false
property bool showRenameDialog: false property bool showRenameDialog: false
property bool showEditMonitorsDialog: false
property string newProfileName: "" property string newProfileName: ""
property string renameProfileName: "" property string renameProfileName: ""
property var editMonitorSelection: ({})
function getProfileOptions() { function getProfileOptions() {
const profiles = DisplayConfigState.validatedProfiles; return Object.values(DisplayConfigState.validatedProfiles).filter(p => p.name !== "").map(p => p.name);
const options = [];
for (const id in profiles)
options.push(profiles[id].name);
return options;
} }
function getProfileIds() { function getProfileIds() {
@@ -44,6 +42,13 @@ Item {
return profiles[id]?.name || ""; return profiles[id]?.name || "";
} }
function openEditMonitorsDialog() {
if (!root.selectedProfileId)
return;
editMonitorSelection = DisplayConfigState.getProfileMonitorInclusion(root.selectedProfileId);
showEditMonitorsDialog = true;
}
Connections { Connections {
target: DisplayConfigState target: DisplayConfigState
function onChangesApplied(changeDescriptions) { function onChangesApplied(changeDescriptions) {
@@ -139,10 +144,9 @@ Item {
} }
} }
// ! TODO - auto profile switching is buggy on niri and other compositors
Column { Column {
id: autoSelectColumn id: autoSelectColumn
visible: false // disabled for now visible: true
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -156,12 +160,12 @@ Item {
DankToggle { DankToggle {
id: autoSelectToggle id: autoSelectToggle
checked: false // disabled for now checked: SettingsData.displayProfileAutoSelect
enabled: false
onToggled: checked => { onToggled: checked => {
// disabled for now SettingsData.displayProfileAutoSelect = checked;
// SettingsData.displayProfileAutoSelect = checked; SettingsData.saveSettings();
// SettingsData.saveSettings(); if (checked)
DisplayConfigState.applyAutoConfig();
} }
} }
} }
@@ -170,16 +174,17 @@ Item {
Row { Row {
width: parent.width width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingS
visible: !root.showNewProfileDialog && !root.showDeleteConfirmDialog && !root.showRenameDialog visible: !root.showNewProfileDialog && !root.showDeleteConfirmDialog && !root.showRenameDialog && !root.showEditMonitorsDialog
opacity: SettingsData.displayProfileAutoSelect ? 0.4 : 1.0
DankDropdown { DankDropdown {
id: profileDropdown id: profileDropdown
width: parent.width - newButton.width - deleteButton.width - Theme.spacingS * 2 width: parent.width - newButton.width - editMonitorsButton.width - deleteButton.width - Theme.spacingS * 3
compactMode: true compactMode: true
dropdownWidth: width dropdownWidth: width
options: root.getProfileOptions() options: root.getProfileOptions()
currentValue: root.getProfileNameById(root.selectedProfileId)
emptyText: I18n.tr("No profiles") emptyText: I18n.tr("No profiles")
enabled: !SettingsData.displayProfileAutoSelect
onValueChanged: value => { onValueChanged: value => {
const profileId = root.getProfileIdByName(value); const profileId = root.getProfileIdByName(value);
if (profileId && profileId !== root.selectedProfileId) if (profileId && profileId !== root.selectedProfileId)
@@ -187,6 +192,12 @@ Item {
} }
} }
Binding {
target: profileDropdown
property: "currentValue"
value: SettingsData.displayProfileAutoSelect ? I18n.tr("Auto") : root.getProfileNameById(root.selectedProfileId)
}
DankButton { DankButton {
id: newButton id: newButton
iconName: "add" iconName: "add"
@@ -195,12 +206,25 @@ Item {
horizontalPadding: Theme.spacingM horizontalPadding: Theme.spacingM
backgroundColor: Theme.surfaceContainer backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText textColor: Theme.surfaceText
enabled: !SettingsData.displayProfileAutoSelect
onClicked: { onClicked: {
root.newProfileName = ""; root.newProfileName = "";
root.showNewProfileDialog = true; root.showNewProfileDialog = true;
} }
} }
DankButton {
id: editMonitorsButton
iconName: "edit"
text: ""
buttonHeight: 40
horizontalPadding: Theme.spacingM
backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText
enabled: root.selectedProfileId !== "" && !SettingsData.displayProfileAutoSelect
onClicked: root.openEditMonitorsDialog()
}
DankButton { DankButton {
id: deleteButton id: deleteButton
iconName: "delete" iconName: "delete"
@@ -209,7 +233,7 @@ Item {
horizontalPadding: Theme.spacingM horizontalPadding: Theme.spacingM
backgroundColor: Theme.surfaceContainer backgroundColor: Theme.surfaceContainer
textColor: Theme.error textColor: Theme.error
enabled: root.selectedProfileId !== "" enabled: root.selectedProfileId !== "" && !SettingsData.displayProfileAutoSelect
onClicked: root.showDeleteConfirmDialog = true onClicked: root.showDeleteConfirmDialog = true
} }
} }
@@ -307,23 +331,89 @@ Item {
} }
} }
Row { Rectangle {
width: parent.width width: parent.width
spacing: Theme.spacingS height: editMonitorsColumn.height + Theme.spacingM * 2
visible: DisplayConfigState.matchedProfile !== "" radius: Theme.cornerRadius
color: Theme.surfaceContainer
visible: root.showEditMonitorsDialog
DankIcon { Column {
name: "check_circle" id: editMonitorsColumn
size: 16 anchors.centerIn: parent
color: Theme.success width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingS
}
StyledText { StyledText {
text: I18n.tr("Matches profile: %1").arg(root.getProfileNameById(DisplayConfigState.matchedProfile)) text: I18n.tr("Monitors in \"%1\":").arg(root.getProfileNameById(root.selectedProfileId))
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeMedium
color: Theme.success color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter width: parent.width
}
Repeater {
model: Object.keys(DisplayConfigState.allOutputs || {})
delegate: Row {
required property string modelData
width: parent.width
spacing: Theme.spacingM
DankToggle {
id: monitorToggle
checked: root.editMonitorSelection[modelData] ?? false
anchors.verticalCenter: parent.verticalCenter
onToggled: checked => {
const sel = Object.assign({}, root.editMonitorSelection);
sel[modelData] = checked;
root.editMonitorSelection = sel;
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: {
const od = DisplayConfigState.allOutputs[modelData];
return DisplayConfigState.getOutputDisplayName(od, modelData);
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
StyledText {
text: DisplayConfigState.allOutputs[modelData]?.connected
? I18n.tr("Connected") : I18n.tr("Disconnected")
font.pixelSize: Theme.fontSizeSmall
color: DisplayConfigState.allOutputs[modelData]?.connected
? Theme.success : Theme.surfaceVariantText
}
}
}
}
Row {
spacing: Theme.spacingS
anchors.right: parent.right
DankButton {
text: I18n.tr("Save")
enabled: Object.values(root.editMonitorSelection).some(v => v)
onClicked: {
const enabled = Object.keys(root.editMonitorSelection).filter(k => root.editMonitorSelection[k]);
DisplayConfigState.updateProfileMonitors(root.selectedProfileId, enabled);
root.showEditMonitorsDialog = false;
}
}
DankButton {
text: I18n.tr("Cancel")
backgroundColor: "transparent"
textColor: Theme.surfaceText
onClicked: root.showEditMonitorsDialog = false
}
}
} }
} }
} }
+9 -2
View File
@@ -297,9 +297,12 @@ Singleton {
return Array.from(visibleTags).sort((a, b) => a - b); return Array.from(visibleTags).sort((a, b) => a - b);
} }
function generateOutputsConfig(outputsData) { function generateOutputsConfig(outputsData, callback) {
if (!outputsData || Object.keys(outputsData).length === 0) if (!outputsData || Object.keys(outputsData).length === 0) {
if (callback)
callback(false);
return; return;
}
let lines = ["# Auto-generated by DMS - do not edit manually", ""]; let lines = ["# Auto-generated by DMS - do not edit manually", ""];
for (const outputName in outputsData) { for (const outputName in outputsData) {
@@ -336,11 +339,15 @@ Singleton {
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => { Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) { if (exitCode !== 0) {
log.warn("Failed to write outputs config:", output); log.warn("Failed to write outputs config:", output);
if (callback)
callback(false);
return; return;
} }
log.info("Generated outputs config at", outputsPath); log.info("Generated outputs config at", outputsPath);
if (CompositorService.isDwl) if (CompositorService.isDwl)
reloadConfig(); reloadConfig();
if (callback)
callback(true);
}); });
} }
+9 -2
View File
@@ -62,9 +62,12 @@ Singleton {
return outputName; return outputName;
} }
function generateOutputsConfig(outputsData, hyprlandSettings) { function generateOutputsConfig(outputsData, hyprlandSettings, callback) {
if (!outputsData || Object.keys(outputsData).length === 0) if (!outputsData || Object.keys(outputsData).length === 0) {
if (callback)
callback(false);
return; return;
}
const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings; const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings;
let lines = ["# Auto-generated by DMS - do not edit manually", ""]; let lines = ["# Auto-generated by DMS - do not edit manually", ""];
@@ -162,11 +165,15 @@ Singleton {
Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => { Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) { if (exitCode !== 0) {
log.warn("Failed to write outputs config:", output); log.warn("Failed to write outputs config:", output);
if (callback)
callback(false);
return; return;
} }
log.info("Generated outputs config at", outputsPath); log.info("Generated outputs config at", outputsPath);
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
reloadConfig(); reloadConfig();
if (callback)
callback(true);
}); });
} }