mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-04 19:42:08 -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:
@@ -24,6 +24,7 @@ import qs.Modules.DankBar
|
||||
import qs.Modules.DankBar.Popouts
|
||||
import qs.Modules.Frame
|
||||
import qs.Modules.WorkspaceOverlays
|
||||
import qs.Modules.Settings.DisplayConfig
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
@@ -304,6 +305,8 @@ Item {
|
||||
dockRecreateDebounce.start();
|
||||
// Force PolkitService singleton to initialize
|
||||
PolkitService.polkitAvailable;
|
||||
// Force DisplayConfigState singleton to initialize so auto-config runs at startup
|
||||
DisplayConfigState.hasOutputBackend;
|
||||
loginSoundTimer.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -1622,13 +1622,15 @@ Item {
|
||||
|
||||
for (const id in profiles) {
|
||||
const p = profiles[id];
|
||||
if (!p.name)
|
||||
continue;
|
||||
const flags = [];
|
||||
if (id === activeId)
|
||||
flags.push("active");
|
||||
if (id === matchedId)
|
||||
flags.push("matched");
|
||||
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)
|
||||
@@ -1660,13 +1662,16 @@ Item {
|
||||
return `PROFILE_SET_SUCCESS: ${profileName}`;
|
||||
}
|
||||
|
||||
// ! TODO - auto profile switching is buggy on niri and other compositors
|
||||
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 {
|
||||
const auto = "off"; // disabled for now
|
||||
const auto = SettingsData.displayProfileAutoSelect ? "on" : "off";
|
||||
const activeId = SettingsData.getActiveDisplayProfile(CompositorService.compositor);
|
||||
const matchedId = DisplayConfigState.matchedProfile;
|
||||
const profiles = DisplayConfigState.validatedProfiles;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,6 +74,8 @@ Column {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
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)
|
||||
onToggled: checked => DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "disabled", checked)
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ Column {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
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)
|
||||
onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "disabled", checked)
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ StyledRect {
|
||||
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;
|
||||
const scale = root.outputData?.logical?.scale || 1.0;
|
||||
return parseFloat(scale.toFixed(2)).toString();
|
||||
}
|
||||
|
||||
@@ -251,8 +251,7 @@ StyledRect {
|
||||
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");
|
||||
return DisplayConfigState.getTransformLabel(root.outputData?.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))
|
||||
|
||||
@@ -15,15 +15,13 @@ Item {
|
||||
property bool showNewProfileDialog: false
|
||||
property bool showDeleteConfirmDialog: false
|
||||
property bool showRenameDialog: false
|
||||
property bool showEditMonitorsDialog: false
|
||||
property string newProfileName: ""
|
||||
property string renameProfileName: ""
|
||||
property var editMonitorSelection: ({})
|
||||
|
||||
function getProfileOptions() {
|
||||
const profiles = DisplayConfigState.validatedProfiles;
|
||||
const options = [];
|
||||
for (const id in profiles)
|
||||
options.push(profiles[id].name);
|
||||
return options;
|
||||
return Object.values(DisplayConfigState.validatedProfiles).filter(p => p.name !== "").map(p => p.name);
|
||||
}
|
||||
|
||||
function getProfileIds() {
|
||||
@@ -44,6 +42,13 @@ Item {
|
||||
return profiles[id]?.name || "";
|
||||
}
|
||||
|
||||
function openEditMonitorsDialog() {
|
||||
if (!root.selectedProfileId)
|
||||
return;
|
||||
editMonitorSelection = DisplayConfigState.getProfileMonitorInclusion(root.selectedProfileId);
|
||||
showEditMonitorsDialog = true;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DisplayConfigState
|
||||
function onChangesApplied(changeDescriptions) {
|
||||
@@ -139,10 +144,9 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// ! TODO - auto profile switching is buggy on niri and other compositors
|
||||
Column {
|
||||
id: autoSelectColumn
|
||||
visible: false // disabled for now
|
||||
visible: true
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -156,12 +160,12 @@ Item {
|
||||
|
||||
DankToggle {
|
||||
id: autoSelectToggle
|
||||
checked: false // disabled for now
|
||||
enabled: false
|
||||
checked: SettingsData.displayProfileAutoSelect
|
||||
onToggled: checked => {
|
||||
// disabled for now
|
||||
// SettingsData.displayProfileAutoSelect = checked;
|
||||
// SettingsData.saveSettings();
|
||||
SettingsData.displayProfileAutoSelect = checked;
|
||||
SettingsData.saveSettings();
|
||||
if (checked)
|
||||
DisplayConfigState.applyAutoConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,16 +174,17 @@ Item {
|
||||
Row {
|
||||
width: parent.width
|
||||
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 {
|
||||
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
|
||||
dropdownWidth: width
|
||||
options: root.getProfileOptions()
|
||||
currentValue: root.getProfileNameById(root.selectedProfileId)
|
||||
emptyText: I18n.tr("No profiles")
|
||||
enabled: !SettingsData.displayProfileAutoSelect
|
||||
onValueChanged: value => {
|
||||
const profileId = root.getProfileIdByName(value);
|
||||
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 {
|
||||
id: newButton
|
||||
iconName: "add"
|
||||
@@ -195,12 +206,25 @@ Item {
|
||||
horizontalPadding: Theme.spacingM
|
||||
backgroundColor: Theme.surfaceContainer
|
||||
textColor: Theme.surfaceText
|
||||
enabled: !SettingsData.displayProfileAutoSelect
|
||||
onClicked: {
|
||||
root.newProfileName = "";
|
||||
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 {
|
||||
id: deleteButton
|
||||
iconName: "delete"
|
||||
@@ -209,7 +233,7 @@ Item {
|
||||
horizontalPadding: Theme.spacingM
|
||||
backgroundColor: Theme.surfaceContainer
|
||||
textColor: Theme.error
|
||||
enabled: root.selectedProfileId !== ""
|
||||
enabled: root.selectedProfileId !== "" && !SettingsData.displayProfileAutoSelect
|
||||
onClicked: root.showDeleteConfirmDialog = true
|
||||
}
|
||||
}
|
||||
@@ -307,23 +331,89 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: DisplayConfigState.matchedProfile !== ""
|
||||
height: editMonitorsColumn.height + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
visible: root.showEditMonitorsDialog
|
||||
|
||||
DankIcon {
|
||||
name: "check_circle"
|
||||
size: 16
|
||||
color: Theme.success
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Column {
|
||||
id: editMonitorsColumn
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Matches profile: %1").arg(root.getProfileNameById(DisplayConfigState.matchedProfile))
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.success
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
StyledText {
|
||||
text: I18n.tr("Monitors in \"%1\":").arg(root.getProfileNameById(root.selectedProfileId))
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,9 +297,12 @@ Singleton {
|
||||
return Array.from(visibleTags).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function generateOutputsConfig(outputsData) {
|
||||
if (!outputsData || Object.keys(outputsData).length === 0)
|
||||
function generateOutputsConfig(outputsData, callback) {
|
||||
if (!outputsData || Object.keys(outputsData).length === 0) {
|
||||
if (callback)
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
|
||||
|
||||
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) => {
|
||||
if (exitCode !== 0) {
|
||||
log.warn("Failed to write outputs config:", output);
|
||||
if (callback)
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
log.info("Generated outputs config at", outputsPath);
|
||||
if (CompositorService.isDwl)
|
||||
reloadConfig();
|
||||
if (callback)
|
||||
callback(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,12 @@ Singleton {
|
||||
return outputName;
|
||||
}
|
||||
|
||||
function generateOutputsConfig(outputsData, hyprlandSettings) {
|
||||
if (!outputsData || Object.keys(outputsData).length === 0)
|
||||
function generateOutputsConfig(outputsData, hyprlandSettings, callback) {
|
||||
if (!outputsData || Object.keys(outputsData).length === 0) {
|
||||
if (callback)
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings;
|
||||
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) => {
|
||||
if (exitCode !== 0) {
|
||||
log.warn("Failed to write outputs config:", output);
|
||||
if (callback)
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
log.info("Generated outputs config at", outputsPath);
|
||||
if (CompositorService.isHyprland)
|
||||
reloadConfig();
|
||||
if (callback)
|
||||
callback(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user