Files
DankMaterialShell/quickshell/Modules/Settings/DisplayConfigTab.qml
T
bbedward a48f41b749 theme: switch to dankgo material utilities fork
settings: label some non-obvious thing swith more details

fixes #3555
fixes #3553
2026-09-25 21:31:34 -04:00

468 lines
19 KiB
QML

import QtQuick
import qs.Common
import qs.Modals
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
import qs.Modules.Settings.DisplayConfig
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var parentModal: null
property string pendingICCOutput: ""
property string selectedProfileId: {
const id = SessionData.activeDisplayProfile[CompositorService.compositor] || "";
if (!SettingsData.displayProfileAutoSelect) {
const profile = DisplayConfigState.validatedProfiles[id];
if (profile && profile.name === "")
return "";
}
return id;
}
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() {
return Object.values(DisplayConfigState.validatedProfiles).filter(p => p.name !== "").map(p => p.name);
}
function getProfileIds() {
return Object.keys(DisplayConfigState.validatedProfiles);
}
function getProfileIdByName(name) {
const profiles = DisplayConfigState.validatedProfiles;
for (const id in profiles) {
if (profiles[id].name === name)
return id;
}
return "";
}
function getProfileNameById(id) {
const profiles = DisplayConfigState.validatedProfiles;
return profiles[id]?.name || "";
}
function openEditMonitorsDialog() {
if (!root.selectedProfileId)
return;
editMonitorSelection = DisplayConfigState.getProfileMonitorInclusion(root.selectedProfileId);
showEditMonitorsDialog = true;
}
Connections {
target: DisplayConfigState
function onChangesApplied(changeDescriptions) {
confirmationModal.changes = changeDescriptions;
confirmationModal.open();
}
function onChangesConfirmed() {
}
function onChangesReverted() {
}
function onProfileActivated(profileId, profileName) {
ToastService.showInfo(I18n.tr("Profile activated: %1", "toast after switching display profile, %1 is the profile name").arg(profileName));
}
function onProfileSaved(profileId, profileName) {
ToastService.showInfo(I18n.tr("Profile saved: %1", "toast after saving a display profile, %1 is the profile name").arg(profileName));
}
function onProfileDeleted(profileId) {
ToastService.showInfo(I18n.tr("Profile deleted"));
}
function onProfileError(message) {
ToastService.showError(I18n.tr("Profile error"), message);
}
}
SettingsPage {
id: mainColumn
IncludeSetupBanner {
include: DisplayConfigState.include
visibleCondition: DisplayConfigState.hasOutputBackend
}
SettingsCard {
width: parent.width
visible: CompositorService.isAqueous && DisplayConfigState.validationError !== ""
SettingsRow {
subtitle: DisplayConfigState.validationError
subtitleColor: Theme.error
DankButton {
text: I18n.tr("Discard draft and reload", "Discard unsaved Aqueous display settings and refresh the current display state")
enabled: !DisplayConfigState.validatingConfig
onClicked: DisplayConfigState.discardAqueousPreview()
}
}
}
SettingsCard {
width: parent.width
iconName: "tune"
title: I18n.tr("Profiles", "card title for display configuration profiles")
visible: DisplayConfigState.hasOutputBackend
SettingsToggleRow {
settingKey: "displayProfileAutoSelect"
text: I18n.tr("Auto")
description: I18n.tr("Applies the saved profile that matches the connected displays", "display profile auto select toggle description")
checked: SettingsData.displayProfileAutoSelect
onToggled: checked => {
SettingsData.displayProfileAutoSelect = checked;
if (!checked)
SessionData.setActiveDisplayProfile(CompositorService.compositor, "");
SettingsData.saveSettings();
if (checked)
DisplayConfigState.applyAutoConfig();
}
}
SettingsRow {
visible: !root.showNewProfileDialog && !root.showDeleteConfirmDialog && !root.showRenameDialog && !root.showEditMonitorsDialog
body: Row {
width: parent.width
spacing: Theme.spacingS
opacity: SettingsData.displayProfileAutoSelect ? 0.4 : 1.0
DankDropdown {
id: profileDropdown
width: parent.width - newButton.width - editMonitorsButton.width - deleteButton.width - Theme.spacingS * 3
compactMode: true
dropdownWidth: width
options: root.getProfileOptions()
emptyText: I18n.tr("No profiles")
enabled: !SettingsData.displayProfileAutoSelect
onValueChanged: value => {
const profileId = root.getProfileIdByName(value);
if (profileId && profileId !== root.selectedProfileId)
DisplayConfigState.activateProfile(profileId);
}
}
Binding {
target: profileDropdown
property: "currentValue"
value: SettingsData.displayProfileAutoSelect ? I18n.tr("Auto") : root.getProfileNameById(root.selectedProfileId)
}
DankButton {
id: newButton
tooltipText: I18n.tr("New profile")
iconName: "add"
text: ""
horizontalPadding: Theme.spacingM
backgroundColor: Theme.chipSurface
textColor: Theme.surfaceText
enabled: !SettingsData.displayProfileAutoSelect
onClicked: {
root.newProfileName = "";
root.showNewProfileDialog = true;
}
}
DankButton {
id: editMonitorsButton
tooltipText: I18n.tr("Edit monitors")
iconName: "edit"
text: ""
horizontalPadding: Theme.spacingM
backgroundColor: Theme.chipSurface
textColor: Theme.surfaceText
enabled: root.selectedProfileId !== "" && !SettingsData.displayProfileAutoSelect
onClicked: root.openEditMonitorsDialog()
}
DankButton {
id: deleteButton
Accessible.name: I18n.tr("Delete profile")
iconName: "delete"
text: ""
horizontalPadding: Theme.spacingM
backgroundColor: Theme.chipSurface
textColor: Theme.error
enabled: root.selectedProfileId !== "" && !SettingsData.displayProfileAutoSelect
onClicked: root.showDeleteConfirmDialog = true
}
}
}
SettingsRow {
visible: root.showNewProfileDialog
body: Row {
width: parent.width
spacing: Theme.spacingS
DankTextField {
id: newProfileField
outlined: true
leftIconName: "badge"
labelText: I18n.tr("Profile name")
width: parent.width - createButton.width - cancelNewButton.width - Theme.spacingS * 2
text: root.newProfileName
onTextChanged: root.newProfileName = text
onAccepted: {
if (text.trim())
DisplayConfigState.createProfile(text.trim());
root.showNewProfileDialog = false;
}
Component.onCompleted: forceActiveFocus()
}
DankButton {
id: createButton
text: I18n.tr("Create")
enabled: root.newProfileName.trim() !== ""
onClicked: {
DisplayConfigState.createProfile(root.newProfileName.trim());
root.showNewProfileDialog = false;
}
}
DankButton {
id: cancelNewButton
text: I18n.tr("Cancel")
backgroundColor: "transparent"
textColor: Theme.surfaceText
onClicked: root.showNewProfileDialog = false
}
}
}
SettingsRow {
visible: root.showDeleteConfirmDialog
title: I18n.tr("Delete profile \"%1\"?", "delete confirmation, %1 is the display profile name").arg(root.getProfileNameById(root.selectedProfileId))
DankButton {
text: I18n.tr("Delete")
backgroundColor: Theme.error
textColor: Theme.primaryText
onClicked: {
DisplayConfigState.deleteProfile(root.selectedProfileId);
root.showDeleteConfirmDialog = false;
}
}
DankButton {
text: I18n.tr("Cancel")
backgroundColor: "transparent"
textColor: Theme.surfaceText
onClicked: root.showDeleteConfirmDialog = false
}
}
SettingsRow {
visible: root.showEditMonitorsDialog
title: I18n.tr("Displays in \"%1\"", "monitor list heading, %1 is the display profile name").arg(root.getProfileNameById(root.selectedProfileId)) + ":"
}
Repeater {
model: Object.keys(DisplayConfigState.allOutputs || {})
delegate: SettingsToggleRow {
required property string modelData
visible: root.showEditMonitorsDialog
text: {
const od = DisplayConfigState.allOutputs[modelData];
return DisplayConfigState.getOutputDisplayName(od, modelData);
}
description: DisplayConfigState.allOutputs[modelData]?.connected ? I18n.tr("Connected") : I18n.tr("Disconnected")
descriptionColor: DisplayConfigState.allOutputs[modelData]?.connected ? Theme.success : Theme.surfaceVariantText
checked: root.editMonitorSelection[modelData] ?? false
onToggled: checked => {
const sel = Object.assign({}, root.editMonitorSelection);
sel[modelData] = checked;
root.editMonitorSelection = sel;
}
}
}
SettingsRow {
visible: root.showEditMonitorsDialog
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
}
}
}
SettingsCard {
width: parent.width
iconName: "monitor"
title: I18n.tr("Arrangement", "card title for monitor arrangement")
visible: DisplayConfigState.hasOutputBackend
SettingsToggleRow {
settingKey: "displaySnapToEdge"
visible: monitorCanvas.filteredOutputs.length > 1
text: I18n.tr("Snap", "verb, toggle to snap monitors to edges when arranging")
description: I18n.tr("Dragged displays line up with neighboring edges without overlapping", "display arrangement snap toggle description")
checked: SettingsData.displaySnapToEdge
onToggled: checked => SettingsData.set("displaySnapToEdge", checked)
}
SettingsButtonGroupRow {
id: displayFormatGroup
settingKey: "displayNameMode"
resetKeys: []
visible: !CompositorService.isMango
text: I18n.tr("Name format")
model: [I18n.tr("Name"), I18n.tr("Model")]
currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
const newMode = index === 1 ? "model" : "system";
DisplayConfigState.setOriginalDisplayNameMode(SettingsData.displayNameMode);
SettingsData.displayNameMode = newMode;
}
Connections {
target: SettingsData
function onDisplayNameModeChanged() {
displayFormatGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0;
}
}
}
MonitorCanvas {
id: monitorCanvas
width: parent.width
}
SettingsRow {
visible: {
const all = DisplayConfigState.allOutputs || {};
const disconnected = Object.keys(all).filter(k => !all[k]?.connected);
return disconnected.length > 0;
}
title: {
const all = DisplayConfigState.allOutputs || {};
const disconnected = Object.keys(all).filter(k => !all[k]?.connected);
if (SettingsData.displayShowDisconnected)
return I18n.tr("%1 disconnected", "displays row title, %1 is a count of disconnected monitors").arg(disconnected.length);
return I18n.tr("%1 disconnected (hidden)", "displays row title, %1 is a count of disconnected monitors").arg(disconnected.length);
}
DankButton {
text: SettingsData.displayShowDisconnected ? I18n.tr("Hide") : I18n.tr("Show")
backgroundColor: "transparent"
textColor: Theme.primary
onClicked: {
SettingsData.displayShowDisconnected = !SettingsData.displayShowDisconnected;
SettingsData.saveSettings();
}
}
}
}
Repeater {
model: {
if (!DisplayConfigState.hasOutputBackend)
return [];
const keys = Object.keys(DisplayConfigState.allOutputs || {});
if (SettingsData.displayShowDisconnected)
return keys;
return keys.filter(k => DisplayConfigState.allOutputs[k]?.connected);
}
delegate: OutputCard {
required property string modelData
outputName: modelData
outputData: DisplayConfigState.allOutputs[modelData]
onRequestICCBrowse: name => {
root.pendingICCOutput = name;
iccFileBrowser.open();
}
onRequestICCInfo: name => iccInfoModal.showProfile(name)
}
}
NoBackendMessage {
width: parent.width
visible: !DisplayConfigState.hasOutputBackend
}
SettingsFabBar {
id: pendingChangesBar
shown: DisplayConfigState.hasOutputBackend && DisplayConfigState.hasPendingChanges
DankFab {
text: I18n.tr("Discard", "verb, button to discard pending changes")
iconName: "undo"
colorRole: "secondaryContainer"
onClicked: DisplayConfigState.discardChanges()
}
DankFab {
text: I18n.tr("Apply changes")
iconName: "check"
colorRole: "primary"
onClicked: DisplayConfigState.applyChanges()
}
}
}
ICCProfileInfoModal {
id: iccInfoModal
}
FileBrowserModal {
id: iccFileBrowser
parentModal: root.parentModal || null
browserTitle: I18n.tr("Select ICC Profile", "ICC profile file browser title")
bucket: "icc"
filters: ["*.icc", "*.icm"]
onAccepted: paths => {
if (pendingICCOutput)
ICCService.applyICC(pendingICCOutput, paths[0]);
}
}
DisplayConfirmationModal {
id: confirmationModal
onConfirmed: DisplayConfigState.confirmChanges(root.selectedProfileId)
onReverted: DisplayConfigState.revertChanges()
}
readonly property bool identifyConfigured: {
if (!DisplayConfigState.hasOutputBackend || DisplayConfigState.readOnly)
return false;
if (!["niri", "hyprland", "mango"].includes(CompositorService.compositor))
return true;
return DisplayConfigState.includeStatus.included;
}
Loader {
active: root.visible && root.identifyConfigured && monitorCanvas.identifyActive
sourceComponent: MonitorIdentifyOverlay {}
}
}