osd: more position options, hookup sheets to some

fixes #3629
This commit is contained in:
bbedward committed 2026-09-29 23:31:28 -04:00
1 parent 219a95d869
commit db903801c3
24 files changed
+983 -90

No files matched your search

Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1790715373,
"narHash": "sha256-hiykGTIlWoRLntC4BWTZ/HIfTQ1aABmAvDyu9t3Sb+A=",
"lastModified": 1790735794,
"narHash": "sha256-+N/tmoRkCCQng+JgZ3Qrb/vrjFihjw9kQgLTus7LwoA=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "2d206c20d102fb77edf35a41b098f9cc086d1acf",
"rev": "fc42c683c7fd16e7583536acabae54a5fd678fc3",
"type": "github"
},
"original": {
+36
View File
@@ -1050,6 +1050,7 @@ Singleton {
property bool osdAlwaysShowValue: Spec.SPEC.osdAlwaysShowValue.def
property int osdPosition: SettingsData.Position.BottomCenter
property var osdPositionOverrides: Spec.SPEC.osdPositionOverrides.def
property bool osdVolumeEnabled: Spec.SPEC.osdVolumeEnabled.def
property bool osdMediaVolumeEnabled: Spec.SPEC.osdMediaVolumeEnabled.def
property bool osdMediaPlaybackEnabled: Spec.SPEC.osdMediaPlaybackEnabled.def
@@ -1586,6 +1587,41 @@ Singleton {
}
}
function osdPositionFor(kind) {
const override = osdPositionOverrides?.[kind];
return typeof override === "number" ? override : osdPosition;
}
function hasOsdPositionOverride(kind) {
return typeof osdPositionOverrides?.[kind] === "number";
}
function setOsdPosition(kind, position) {
if (!kind) {
set("osdPosition", position);
return;
}
if (position === osdPosition) {
resetOsdPosition(kind);
return;
}
set("osdPositionOverrides", Object.assign({}, osdPositionOverrides, {
[kind]: position
}));
}
function resetOsdPosition(kind) {
if (!kind) {
resetToDefault(["osdPosition"]);
return;
}
if (!hasOsdPositionOverride(kind))
return;
const next = Object.assign({}, osdPositionOverrides);
delete next[kind];
set("osdPositionOverrides", next);
}
function barConfigDefault(field) {
return Spec.SPEC.barConfigs.def[0][field];
}
@@ -1241,6 +1241,9 @@ var LOCAL_SPEC = {
osdPosition: {
def: 5
},
osdPositionOverrides: {
def: {}
},
osdVolumeEnabled: {
def: true
},
@@ -211,10 +211,10 @@ Item {
anchors.topMargin: PopoutMetrics.contentGap
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottom: keyboardHintsLoader.top
anchors.leftMargin: PopoutMetrics.contentPadding
anchors.rightMargin: PopoutMetrics.contentPadding
anchors.bottomMargin: (keyboardHintsLoader.item ? keyboardHintsLoader.item.implicitHeight + PopoutMetrics.contentPadding * 2 : 0) + Theme.spacingXS
anchors.bottomMargin: keyboardHintsLoader.active ? PopoutMetrics.contentGap : 0
clip: true
DankListView {
@@ -297,7 +297,7 @@ Item {
anchors.right: parent.right
anchors.leftMargin: PopoutMetrics.contentPadding
anchors.rightMargin: PopoutMetrics.contentPadding
anchors.bottomMargin: active ? PopoutMetrics.contentPadding : 0
anchors.bottomMargin: PopoutMetrics.contentPadding
active: modal.showKeyboardHints
height: item ? item.implicitHeight : 0
@@ -255,8 +255,8 @@ Item {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
anchors.margins: PopoutMetrics.contentPadding
spacing: PopoutMetrics.contentGap
Item {
id: editorHeader
@@ -295,7 +295,7 @@ Item {
DankTextEdit {
id: editField
width: parent.width
height: Math.max(Theme.fontSizeMedium * 8, parent.height - editorHeader.height - editorActions.height - Theme.spacingM * 2)
height: Math.max(Theme.fontSizeMedium * 8, parent.height - editorHeader.height - editorActions.height - PopoutMetrics.contentGap * 2)
leftIconName: "edit"
placeholderText: I18n.tr("Edit clipboard text")
backgroundColor: Theme.floatingWindowFieldColor
@@ -6,6 +6,8 @@ import qs.Widgets
DankOSD {
id: root
osdKind: "audioOutput"
property string deviceName: ""
property string deviceIcon: "speaker"
+13
View File
@@ -1,10 +1,13 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter.Details
import qs.Services
LevelOSD {
id: root
osdKind: "brightness"
readonly property var device: BrightnessService.getCurrentDeviceInfo()
property int displayLevel: BrightnessService.brightnessLevel
@@ -19,6 +22,7 @@ LevelOSD {
return BrightnessService.brightnessIconName(device);
}
}
endIconLabel: I18n.tr("Brightness")
iconColor: Theme.onPrimary
value: displayLevel
minimum: BrightnessService.brightnessMinimum(device)
@@ -26,6 +30,15 @@ LevelOSD {
unit: BrightnessService.brightnessUnit(device)
available: BrightnessService.brightnessAvailable
sheet: OsdDetailSheet {
BrightnessDetail {
anchors.fill: parent
initialDeviceName: root.device?.name ?? ""
screenName: root.screen?.name ?? ""
screenModel: root.screen?.model ?? ""
}
}
onLevelRequested: level => {
displayLevel = level;
BrightnessService.setBrightness(level, BrightnessService.lastIpcDevice, true);
+2
View File
@@ -6,6 +6,8 @@ import qs.Widgets
DankOSD {
id: root
osdKind: "capsLock"
osdWidth: Theme.osdHeight
osdHeight: Theme.osdHeight
autoHideInterval: 2000
@@ -6,6 +6,8 @@ import qs.Widgets
DankOSD {
id: root
osdKind: "idleInhibitor"
osdWidth: Theme.osdHeight
osdHeight: Theme.osdHeight
autoHideInterval: 2000
+55
View File
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Widgets
DankOSD {
@@ -10,6 +11,7 @@ DankOSD {
property string iconName: ""
property string insetIconName: ""
property string endIconName: ""
property string endIconLabel: ""
property bool iconInteractive: false
property string iconLabel: ""
property color iconColor: Theme.onPrimary
@@ -26,6 +28,40 @@ DankOSD {
readonly property real osdValueReserve: endIconName.length > 0 || SettingsData.osdAlwaysShowValue ? Theme.buttonHeightM : 0
function showEndIconTooltip(item) {
endIconTooltip.active = true;
const tip = endIconTooltip.item;
if (!tip)
return;
const pos = item.mapToItem(null, 0, 0);
const left = windowX + pos.x;
const top = windowY + pos.y;
const gap = Theme.spacingS;
tip.text = endIconLabel;
switch (alignY) {
case 1:
tip.show(endIconLabel, left + item.width / 2, top - gap - tip.implicitHeight, screen);
return;
case -1:
tip.show(endIconLabel, left + item.width / 2, top + item.height + gap, screen);
return;
}
if (alignX === 1)
tip.show(endIconLabel, left - gap, top + item.height / 2, screen, false, true);
else
tip.show(endIconLabel, left + item.width + gap, top + item.height / 2, screen, true, false);
}
function hideEndIconTooltip() {
endIconTooltip.item?.hide();
endIconTooltip.active = false;
}
onVisibleChanged: {
if (!visible)
hideEndIconTooltip();
}
function requestLevel(level) {
if (!available)
return;
@@ -36,14 +72,23 @@ DankOSD {
osdWidth: isVerticalLayout ? Theme.osdHeight : Math.min(Theme.osdLevelWidth + osdValueReserve, screenWidth - Theme.spacingM * 2)
osdHeight: isVerticalLayout ? Math.min(Theme.osdLevelVerticalHeight, screenHeight - Theme.spacingM * 2) : Theme.buttonHeightS + Theme.spacingS * 2
autoHideInterval: 3000
sheetWidth: CcMetrics.sheetWidthFor(CcMetrics.minimumColumns)
enableMouseInteraction: true
Loader {
id: endIconTooltip
active: false
sourceComponent: DankTooltip {}
}
content: OsdLevelRow {
vertical: root.isVerticalLayout
sliderSize: root.isVerticalLayout ? "m" : "s"
iconName: root.iconName
insetIconName: root.insetIconName
endIconName: root.endIconName
endIconInteractive: root.sheet !== null
endIconLabel: root.endIconLabel
iconInteractive: root.iconInteractive
iconLabel: root.iconLabel
iconColor: root.iconColor
@@ -56,6 +101,16 @@ DankOSD {
displayText: root.displayText
sliderEnabled: root.available
onIconClicked: root.iconClicked()
onEndIconClicked: {
root.hideEndIconTooltip();
root.expand();
}
onEndIconHoveredChanged: {
if (endIconHovered && root.sheet)
root.showEndIconTooltip(endIconItem);
else
root.hideEndIconTooltip();
}
onHoverChanged: hovered => root.setChildHovered(hovered)
onSliderValueChanged: newValue => root.requestLevel(newValue)
}
@@ -9,6 +9,8 @@ import Quickshell.Widgets
DankOSD {
id: root
osdKind: "mediaPlayback"
readonly property bool useVertical: isVerticalLayout
readonly property bool playing: player?.isPlaying ?? false
readonly property var player: MprisController.activePlayer
@@ -5,6 +5,8 @@ import qs.Services
LevelOSD {
id: root
osdKind: "mediaVolume"
readonly property var player: MprisController.activePlayer
readonly property bool volumeSupported: player?.volumeSupported ?? false
readonly property real playerVolume: player?.volume ?? 0
+10
View File
@@ -1,16 +1,20 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter.Details
import qs.Services
LevelOSD {
id: root
osdKind: "mic"
readonly property var audio: AudioService.source?.audio ?? null
readonly property bool muted: audio?.muted ?? false
iconName: muted ? "mic_off" : "mic"
insetIconName: "mic"
endIconName: "graphic_eq"
endIconLabel: I18n.tr("Audio Input")
iconInteractive: true
iconLabel: muted ? I18n.tr("Unmute") : I18n.tr("Mute")
iconColor: muted ? Theme.onErrorContainer : Theme.onPrimary
@@ -19,6 +23,12 @@ LevelOSD {
available: !!audio
displayText: muted ? I18n.tr("Muted") : ""
sheet: OsdDetailSheet {
AudioInputDetail {
anchors.fill: parent
}
}
onIconClicked: AudioService.toggleMicMute()
onLevelRequested: level => {
SessionData.suppressOSDTemporarily();
+104
View File
@@ -0,0 +1,104 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Details
import qs.Services
import qs.Widgets
FocusScope {
id: root
default property alias detail: body.data
readonly property Item page: body.children[0] ?? null
readonly property string settingsTab: page?.headerActions?.settingsTab ?? ""
signal closeRequested
implicitHeight: CcMetrics.detailDialogPadding * 2 + header.height + (page?.implicitHeight ?? 0)
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
function releasePortSelector() {
if (portSelectorLoader.item?.shown)
return;
portSelectorLoader.active = false;
}
Item {
id: header
x: CcMetrics.detailDialogPadding
y: CcMetrics.detailDialogPadding
width: parent.width - CcMetrics.detailDialogPadding * 2
height: CcMetrics.pageHeaderHeight
StyledText {
anchors.left: parent.left
anchors.leftMargin: CcMetrics.rowPaddingH
anchors.right: settingsButton.visible ? settingsButton.left : parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: root.page?.title ?? ""
font.pixelSize: CcMetrics.pageTitleSize
color: Theme.surfaceText
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
}
DankActionButton {
id: settingsButton
anchors.right: parent.right
anchors.rightMargin: CcMetrics.headerEdgeInset
anchors.verticalCenter: parent.verticalCenter
visible: root.settingsTab !== ""
buttonSize: CcMetrics.headerActionSize
iconSize: CcMetrics.headerActionIconSize
iconName: "settings"
iconColor: Theme.surfaceText
Accessible.name: I18n.tr("Settings")
onClicked: {
root.closeRequested();
PopoutService.openSettingsWithTab(root.settingsTab);
}
}
}
Item {
id: body
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.bottom: parent.bottom
anchors.leftMargin: CcMetrics.detailDialogPadding
anchors.rightMargin: CcMetrics.detailDialogPadding
anchors.bottomMargin: CcMetrics.detailDialogPadding
focus: true
}
Connections {
target: root.page
ignoreUnknownSignals: true
function onShowPortSelector(node) {
portSelectorLoader.active = true;
portSelectorLoader.item?.show(node);
if (!portSelectorLoader.item?.shown)
portSelectorLoader.active = false;
}
}
Loader {
id: portSelectorLoader
anchors.fill: parent
z: CcMetrics.overlayZ
active: false
sourceComponent: AudioPortSelector {
onDismissed: Qt.callLater(root.releasePortSelector)
}
}
}
+17 -6
View File
@@ -10,6 +10,8 @@ Item {
property string iconName: "volume_up"
property string insetIconName: ""
property string endIconName: ""
property bool endIconInteractive: false
property string endIconLabel: ""
property string iconLabel: ""
property bool iconInteractive: false
property bool tonalIcon: true
@@ -27,7 +29,9 @@ Item {
property color thumbOutlineColor: Theme.hostSurface
property real horizontalPadding: -1
readonly property bool containsMouse: levelSlider.containsMouse || icon.hovered
readonly property bool containsMouse: levelSlider.containsMouse || icon.hovered || endIcon.hovered
readonly property bool endIconHovered: endIcon.hovered
readonly property Item endIconItem: endIcon
readonly property bool showValueColumn: SettingsData.osdAlwaysShowValue
readonly property bool showEndIcon: endIconName.length > 0
readonly property alias isDragging: levelSlider.isDragging
@@ -41,6 +45,7 @@ Item {
signal sliderValueChanged(int newValue)
signal iconClicked
signal endIconClicked
signal hoverChanged(bool hovered)
onContainsMouseChanged: root.hoverChanged(root.containsMouse)
@@ -126,12 +131,18 @@ Item {
visible: root.showValueColumn
}
DankIcon {
anchors.centerIn: parent
name: root.endIconName
size: Theme.iconSize
color: root.sliderEnabled ? root.fillColor : Theme.onSurface_38
OsdIcon {
id: endIcon
anchors.fill: parent
iconName: root.endIconName
iconColor: root.fillColor
tonal: false
label: root.endIconLabel
interactive: root.endIconInteractive
available: root.sliderEnabled
visible: !root.showValueColumn
onClicked: root.endIconClicked()
}
}
}
@@ -6,6 +6,8 @@ import qs.Widgets
DankOSD {
id: root
osdKind: "powerProfile"
property int currentProfile: 0
property string profileIcon: "settings"
+10
View File
@@ -1,15 +1,19 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter.Details
import qs.Services
LevelOSD {
id: root
osdKind: "volume"
readonly property var audio: AudioService.sink?.audio ?? null
iconName: AudioService.sinkVolumeIconName
insetIconName: "music_note"
endIconName: AudioService.sinkIcon(AudioService.sink)
endIconLabel: I18n.tr("Audio Output")
iconInteractive: true
iconLabel: audio?.muted ? I18n.tr("Unmute", "verb, button to unmute audio or a muted app") : I18n.tr("Mute")
value: AudioService.sinkVolumePercent
@@ -17,6 +21,12 @@ LevelOSD {
available: !!audio
displayText: audio?.muted ? I18n.tr("Muted") : ""
sheet: OsdDetailSheet {
AudioOutputDetail {
anchors.fill: parent
}
}
onIconClicked: AudioService.toggleMute()
onLevelRequested: level => {
SessionData.suppressOSDTemporarily();
+2
View File
@@ -9,6 +9,8 @@ import qs.Widgets
DankOSD {
id: root
osdKind: "workspace"
property string workspaceLabel: ""
property var lastActiveWorkspaceId: null
+97 -42
View File
@@ -6,6 +6,75 @@ import qs.Modules.Settings.Widgets
Item {
id: root
property string positionScope: ""
readonly property var positionScopes: [
{
"value": "",
"label": I18n.tr("Default"),
"shown": true
},
{
"value": "volume",
"label": I18n.tr("Volume"),
"shown": SettingsData.osdVolumeEnabled
},
{
"value": "mediaVolume",
"label": I18n.tr("Media volume"),
"shown": SettingsData.osdMediaVolumeEnabled
},
{
"value": "mediaPlayback",
"label": I18n.tr("Media playback"),
"shown": SettingsData.osdMediaPlaybackEnabled
},
{
"value": "brightness",
"label": I18n.tr("Brightness"),
"shown": SettingsData.osdBrightnessEnabled
},
{
"value": "idleInhibitor",
"label": I18n.tr("Idle inhibitor", "feature that keeps the session from going idle"),
"shown": SettingsData.osdIdleInhibitorEnabled
},
{
"value": "mic",
"label": I18n.tr("Microphone"),
"shown": SettingsData.osdMicMuteEnabled || SettingsData.osdMicVolumeEnabled
},
{
"value": "capsLock",
"label": I18n.tr("Caps lock"),
"shown": SettingsData.osdCapsLockEnabled
},
{
"value": "powerProfile",
"label": I18n.tr("Power profile"),
"shown": SettingsData.osdPowerProfileEnabled
},
{
"value": "audioOutput",
"label": I18n.tr("Audio output switch"),
"shown": SettingsData.osdAudioOutputEnabled
},
{
"value": "workspace",
"label": I18n.tr("Workspace Switch", "toggle label for workspace change OSD"),
"shown": SettingsData.osdWorkspaceEnabled
}
].filter(scope => scope.shown).map(scope => ({
"value": scope.value,
"label": scope.label,
"icon": SettingsData.hasOsdPositionOverride(scope.value) ? "pin_drop" : "",
"tooltip": SettingsData.hasOsdPositionOverride(scope.value) ? I18n.tr("Custom") : ""
}))
onPositionScopesChanged: {
if (!positionScopes.some(scope => scope.value === positionScope))
positionScope = "";
}
SettingsPage {
id: mainColumn
@@ -13,51 +82,37 @@ Item {
width: parent.width
settingKey: "osd"
SettingsDropdownRow {
SettingsRow {
id: positionRow
readonly property bool overridden: root.positionScope !== "" && SettingsData.hasOsdPositionOverride(root.positionScope)
settingKey: "osdPosition"
text: I18n.tr("Position")
currentValue: {
switch (SettingsData.osdPosition) {
case SettingsData.Position.Top:
return I18n.tr("Top Right", "screen position option");
case SettingsData.Position.Left:
return I18n.tr("Top Left", "screen position option");
case SettingsData.Position.TopCenter:
return I18n.tr("Top Center", "screen position option");
case SettingsData.Position.Right:
return I18n.tr("Bottom Right", "screen position option");
case SettingsData.Position.Bottom:
return I18n.tr("Bottom Left", "screen position option");
case SettingsData.Position.BottomCenter:
return I18n.tr("Bottom Center", "screen position option");
case SettingsData.Position.LeftCenter:
return I18n.tr("Left Center", "screen position option");
case SettingsData.Position.RightCenter:
return I18n.tr("Right Center", "screen position option");
default:
return I18n.tr("Bottom Center", "screen position option");
}
title: I18n.tr("Position")
subtitle: {
const label = positionPicker.labelFor(SettingsData.osdPositionFor(root.positionScope));
return root.positionScope !== "" && !overridden ? I18n.tr("Default") + " · " + label : label;
}
options: [I18n.tr("Top Right", "screen position option"), I18n.tr("Top Left", "screen position option"), I18n.tr("Top Center", "screen position option"), I18n.tr("Bottom Right", "screen position option"), I18n.tr("Bottom Left", "screen position option"), I18n.tr("Bottom Center", "screen position option"), I18n.tr("Left Center", "screen position option"), I18n.tr("Right Center", "screen position option")]
onValueChanged: value => {
if (value === I18n.tr("Top Right", "screen position option")) {
SettingsData.set("osdPosition", SettingsData.Position.Top);
} else if (value === I18n.tr("Top Left", "screen position option")) {
SettingsData.set("osdPosition", SettingsData.Position.Left);
} else if (value === I18n.tr("Top Center", "screen position option")) {
SettingsData.set("osdPosition", SettingsData.Position.TopCenter);
} else if (value === I18n.tr("Bottom Right", "screen position option")) {
SettingsData.set("osdPosition", SettingsData.Position.Right);
} else if (value === I18n.tr("Bottom Left", "screen position option")) {
SettingsData.set("osdPosition", SettingsData.Position.Bottom);
} else if (value === I18n.tr("Bottom Center", "screen position option")) {
SettingsData.set("osdPosition", SettingsData.Position.BottomCenter);
} else if (value === I18n.tr("Left Center", "screen position option")) {
SettingsData.set("osdPosition", SettingsData.Position.LeftCenter);
} else if (value === I18n.tr("Right Center", "screen position option")) {
SettingsData.set("osdPosition", SettingsData.Position.RightCenter);
modified: root.positionScope === "" ? !SettingsData.isDefault(["osdPosition"]) : overridden
resetByKeys: false
onResetRequested: SettingsData.resetOsdPosition(root.positionScope)
body: [
DankFilterChips {
model: root.positionScopes
Binding on currentIndex {
value: Math.max(0, root.positionScopes.findIndex(scope => scope.value === root.positionScope))
restoreMode: Binding.RestoreNone
}
showCounts: false
onSelectionChanged: index => root.positionScope = root.positionScopes[index].value
},
SettingsScreenPositionPicker {
id: positionPicker
selected: SettingsData.osdPositionFor(root.positionScope)
onPicked: position => SettingsData.setOsdPosition(root.positionScope, position)
}
}
]
}
SettingsToggleRow {
@@ -31,6 +31,7 @@ Singleton {
readonly property real splitDividerHeight: Theme.iconSize
readonly property real buttonGroupCompactThreshold: 200
readonly property real choiceCardPreviewRatio: 10 / 16
readonly property real positionPickerMaxWidth: 360
readonly property real wallpaperThumbRatio: 10 / 16
readonly property int wallpaperThumbCache: 1024
readonly property real wallpaperHeroStackWidth: 640
@@ -0,0 +1,211 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
property int selected: SettingsData.Position.BottomCenter
readonly property var positions: [SettingsData.Position.Left, SettingsData.Position.TopCenter, SettingsData.Position.Top, SettingsData.Position.LeftCenter, SettingsData.Position.RightCenter, SettingsData.Position.Bottom, SettingsData.Position.BottomCenter, SettingsData.Position.Right]
readonly property real edgeSlotLength: 0.26
readonly property real sideSlotLength: 0.42
readonly property real slotThickness: 0.1
readonly property rect selectedArea: slotRect(selected)
property int placedPosition: -1
signal picked(int position)
width: parent?.width ?? 0
implicitHeight: screenMock.height
height: implicitHeight
LayoutMirroring.enabled: false
LayoutMirroring.childrenInherit: true
onSelectedAreaChanged: placeIndicator()
Component.onCompleted: placeIndicator()
function labelFor(position) {
switch (position) {
case SettingsData.Position.Top:
return I18n.tr("Top Right", "screen position option");
case SettingsData.Position.Left:
return I18n.tr("Top Left", "screen position option");
case SettingsData.Position.TopCenter:
return I18n.tr("Top Center", "screen position option");
case SettingsData.Position.Right:
return I18n.tr("Bottom Right", "screen position option");
case SettingsData.Position.Bottom:
return I18n.tr("Bottom Left", "screen position option");
case SettingsData.Position.LeftCenter:
return I18n.tr("Left Center", "screen position option");
case SettingsData.Position.RightCenter:
return I18n.tr("Right Center", "screen position option");
default:
return I18n.tr("Bottom Center", "screen position option");
}
}
function cellFor(position) {
switch (position) {
case SettingsData.Position.Left:
return Qt.point(0, 0);
case SettingsData.Position.TopCenter:
return Qt.point(1, 0);
case SettingsData.Position.Top:
return Qt.point(2, 0);
case SettingsData.Position.LeftCenter:
return Qt.point(0, 1);
case SettingsData.Position.RightCenter:
return Qt.point(2, 1);
case SettingsData.Position.Bottom:
return Qt.point(0, 2);
case SettingsData.Position.Right:
return Qt.point(2, 2);
default:
return Qt.point(1, 2);
}
}
function slotRect(position) {
const cell = cellFor(position);
const side = cell.y === 1;
const inset = screenMock.inset;
const w = Math.round(side ? screenMock.height * slotThickness : screenMock.width * edgeSlotLength);
const h = Math.round(screenMock.height * (side ? sideSlotLength : slotThickness));
const x = cell.x === 0 ? inset : cell.x === 2 ? screenMock.width - w - inset : Math.round((screenMock.width - w) / 2);
const y = cell.y === 0 ? inset : cell.y === 2 ? screenMock.height - h - inset : Math.round((screenMock.height - h) / 2);
return Qt.rect(x, y, w, h);
}
function neighbor(position, dx, dy) {
const from = cellFor(position);
let best = -1;
let bestCost = Infinity;
for (const candidate of positions) {
const cell = cellFor(candidate);
const along = dx !== 0 ? (cell.x - from.x) * dx : (cell.y - from.y) * dy;
if (along <= 0)
continue;
const cost = along + 2 * (dx !== 0 ? Math.abs(cell.y - from.y) : Math.abs(cell.x - from.x));
if (cost >= bestCost)
continue;
best = candidate;
bestCost = cost;
}
return best;
}
function step(from, dx, dy) {
const next = neighbor(from, dx, dy);
if (next < 0)
return;
picked(next);
slotRepeater.itemAt(positions.indexOf(next))?.forceActiveFocus(Qt.TabFocusReason);
}
function placeIndicator() {
const animate = placedPosition >= 0 && placedPosition !== selected;
placedPosition = selected;
if (animate)
indicatorSpring.retarget(selectedArea);
else
indicatorSpring.snapTo(selectedArea);
}
RectSpringMotion {
id: indicatorSpring
enabled: !Theme.springMotionDisabled
reducedMotion: SettingsData.reduceMotion
stiffness: Theme.springPreset("fast", Theme.expressiveDurations.expressiveFastSpatial).stiffness
damping: Theme.springPreset("fast", Theme.expressiveDurations.expressiveFastSpatial).damping
}
Rectangle {
id: screenMock
readonly property real inset: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(root.width, SettingsMetrics.positionPickerMaxWidth)
height: Math.round(width * SettingsMetrics.choiceCardPreviewRatio)
radius: Theme.cornerRadiusM
color: Theme.surfaceContainerHighest
border.width: Theme.outlineWidth
border.color: Theme.outlineVariant
Repeater {
model: root.positions
Rectangle {
required property int modelData
readonly property rect area: root.slotRect(modelData)
x: area.x
y: area.y
width: area.width
height: area.height
radius: Theme.fullRadius(width, height)
color: root.enabled ? Theme.outlineVariant : Theme.onSurface_12
}
}
Rectangle {
readonly property real inset: screenMock.inset
width: indicatorSpring.value.width
height: indicatorSpring.value.height
x: Math.max(inset, Math.min(indicatorSpring.value.x, screenMock.width - width - inset))
y: Math.max(inset, Math.min(indicatorSpring.value.y, screenMock.height - height - inset))
radius: Theme.fullRadius(width, height)
color: root.enabled ? Theme.primary : Theme.onSurface_38
}
Repeater {
id: slotRepeater
model: root.positions
StyledButton {
id: slot
required property int modelData
readonly property rect area: root.slotRect(modelData)
readonly property bool isSelected: modelData === root.selected
readonly property real touchPad: Math.max(0, (Theme.buttonHeightS - Math.min(area.width, area.height)) / 2)
x: area.x - touchPad
y: area.y - touchPad
width: area.width + touchPad * 2
height: area.height + touchPad * 2
focusPolicy: isSelected || activeFocus ? Qt.StrongFocus : Qt.ClickFocus
Accessible.role: Accessible.RadioButton
Accessible.name: root.labelFor(modelData)
Accessible.checked: isSelected
onClicked: root.picked(modelData)
Keys.onLeftPressed: root.step(modelData, -1, 0)
Keys.onRightPressed: root.step(modelData, 1, 0)
Keys.onUpPressed: root.step(modelData, 0, -1)
Keys.onDownPressed: root.step(modelData, 0, 1)
Rectangle {
anchors.centerIn: parent
width: slot.area.width
height: slot.area.height
radius: Theme.fullRadius(width, height)
color: "transparent"
StateLayer {
control: slot
disabled: !slot.enabled
stateColor: slot.isSelected ? Theme.onPrimary : Theme.onSurface
}
FocusRing {
visible: slot.visualFocus
}
}
}
}
}
}
+86 -32
View File
@@ -14,6 +14,8 @@ PanelWindow {
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property var modelData
property string osdKind: ""
readonly property int osdPosition: SettingsData.osdPositionFor(osdKind)
property bool shouldBeVisible: false
property bool _surfaceFrameReady: false
readonly property bool presented: shouldBeVisible && _surfaceFrameReady
@@ -25,12 +27,15 @@ PanelWindow {
property real surfaceRadius: Theme.fullRadius(alignedWidth, alignedHeight)
property int animationDuration: Theme.mediumDuration
property var animationEasing: Theme.emphasizedEasing
property Component sheet: null
property real sheetWidth: 0
property bool expanded: false
signal osdShown
signal osdHidden
function show() {
if (SessionData.suppressOSD)
if (SessionData.suppressOSD || expanded)
return;
if (shouldBeVisible) {
hideTimer.restart();
@@ -45,10 +50,38 @@ PanelWindow {
}
function hide() {
if (expanded) {
closeSheet();
return;
}
shouldBeVisible = false;
closeTimer.restart();
}
function expand() {
if (!sheet || !shouldBeVisible || expanded)
return;
hideTimer.stop();
expanded = true;
}
function closeSheet() {
if (sheetLoader.item) {
sheetLoader.item.close();
return;
}
finishSheet();
}
function finishSheet() {
expanded = false;
shouldBeVisible = false;
hideTimer.stop();
closeTimer.stop();
visible = false;
osdHidden();
}
function resetHideTimer() {
if (shouldBeVisible) {
hideTimer.restart();
@@ -101,6 +134,7 @@ PanelWindow {
if (Quickshell.screens[i].name === currentScreenName)
return;
}
expanded = false;
shouldBeVisible = false;
visible = false;
hideTimer.stop();
@@ -135,7 +169,7 @@ PanelWindow {
readonly property real alignedWidth: Theme.px(osdWidth, dpr)
readonly property real alignedHeight: Theme.px(osdHeight, dpr)
readonly property bool isVerticalLayout: SettingsData.osdPosition === SettingsData.Position.LeftCenter || SettingsData.osdPosition === SettingsData.Position.RightCenter
readonly property bool isVerticalLayout: osdPosition === SettingsData.Position.LeftCenter || osdPosition === SettingsData.Position.RightCenter
readonly property var barEdgeOffsets: {
const offsets = {
@@ -187,58 +221,65 @@ PanelWindow {
return SettingsData.dockReservationForEdge(screen, side);
}
readonly property real alignedX: {
const margin = Theme.spacingM;
const centerX = (screenWidth - alignedWidth) / 2;
function edgeInset(side) {
return Theme.spacingM + Math.max(barEdgeOffsets[side], dockOffsetForEdge(side));
}
switch (SettingsData.osdPosition) {
case SettingsData.Position.Left:
case SettingsData.Position.Bottom:
case SettingsData.Position.LeftCenter:
const leftDockOffset = root.dockOffsetForEdge("left");
return Theme.snap(margin + Math.max(barEdgeOffsets.left, leftDockOffset), dpr);
case SettingsData.Position.Top:
case SettingsData.Position.Right:
case SettingsData.Position.RightCenter:
const rightDockOffset = root.dockOffsetForEdge("right");
return Theme.snap(screenWidth - alignedWidth - margin - Math.max(barEdgeOffsets.right, rightDockOffset), dpr);
case SettingsData.Position.TopCenter:
case SettingsData.Position.BottomCenter:
function placeAlong(align, extent, size, startSide, endSide) {
switch (align) {
case -1:
return Theme.snap(edgeInset(startSide), dpr);
case 1:
return Theme.snap(extent - size - edgeInset(endSide), dpr);
default:
return Theme.snap(centerX, dpr);
return Theme.snap((extent - size) / 2, dpr);
}
}
readonly property real alignedY: {
const margin = Theme.spacingM;
const centerY = (screenHeight - alignedHeight) / 2;
readonly property int alignX: {
switch (osdPosition) {
case SettingsData.Position.Left:
case SettingsData.Position.Bottom:
case SettingsData.Position.LeftCenter:
return -1;
case SettingsData.Position.Top:
case SettingsData.Position.Right:
case SettingsData.Position.RightCenter:
return 1;
default:
return 0;
}
}
switch (SettingsData.osdPosition) {
readonly property int alignY: {
switch (osdPosition) {
case SettingsData.Position.Top:
case SettingsData.Position.Left:
case SettingsData.Position.TopCenter:
const topDockOffset = root.dockOffsetForEdge("top");
return Theme.snap(margin + Math.max(barEdgeOffsets.top, topDockOffset), dpr);
return -1;
case SettingsData.Position.Right:
case SettingsData.Position.Bottom:
case SettingsData.Position.BottomCenter:
const bottomDockOffset = root.dockOffsetForEdge("bottom");
return Theme.snap(screenHeight - alignedHeight - margin - Math.max(barEdgeOffsets.bottom, bottomDockOffset), dpr);
case SettingsData.Position.LeftCenter:
case SettingsData.Position.RightCenter:
return 1;
default:
return Theme.snap(centerY, dpr);
return 0;
}
}
readonly property real alignedX: placeAlong(alignX, screenWidth, alignedWidth, "left", "right")
readonly property real alignedY: placeAlong(alignY, screenHeight, alignedHeight, "top", "bottom")
anchors {
top: true
left: true
}
readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowBuffer, dpr))
readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowBuffer, dpr))
WlrLayershell.margins {
left: Math.max(0, Theme.snap(alignedX - shadowBuffer, dpr))
top: Math.max(0, Theme.snap(alignedY - shadowBuffer, dpr))
left: root.windowX
top: root.windowY
}
implicitWidth: alignedWidth + (shadowBuffer * 2)
@@ -266,6 +307,8 @@ PanelWindow {
interval: autoHideInterval
repeat: false
onTriggered: {
if (expanded)
return;
if (!enableMouseInteraction || !mouseArea.containsMouse) {
hide();
} else {
@@ -340,4 +383,15 @@ PanelWindow {
mask: Region {
item: bgShadowLayer
}
LazyLoader {
id: sheetLoader
active: root.expanded
DankOSDSheet {
host: root
onPresented: root.visible = false
onDismissed: root.finishSheet()
}
}
}
+316
View File
@@ -0,0 +1,316 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import "../Common/FluidGeometry.js" as FluidGeometry
PanelWindow {
id: root
required property var host
property bool opened: false
property bool closing: false
property real shownSheetHeight: sheetHeight
Behavior on shownSheetHeight {
enabled: root.opened && !root.closing && !morph.running && root.animationDuration > 0 && !SettingsData.reduceMotion
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutResizeCurve
}
}
readonly property real dpr: host.dpr
readonly property real shadowBuffer: host.shadowBuffer
readonly property int alignX: host.alignX
readonly property int alignY: host.alignY
readonly property rect compact: Qt.rect(host.alignedX, host.alignedY, host.alignedWidth, host.alignedHeight)
readonly property real insetTop: host.edgeInset("top")
readonly property real insetBottom: host.edgeInset("bottom")
readonly property real insetLeft: host.edgeInset("left")
readonly property real insetRight: host.edgeInset("right")
readonly property real sheetWidth: Theme.px(Math.min(host.sheetWidth, host.screenWidth - insetLeft - insetRight), dpr)
readonly property real maxSheetHeight: {
switch (alignY) {
case -1:
return host.screenHeight - compact.y - insetBottom;
case 1:
return compact.y + compact.height - insetTop;
default:
return 2 * Math.min(compact.y + compact.height / 2 - insetTop, host.screenHeight - insetBottom - compact.y - compact.height / 2);
}
}
readonly property real sheetHeight: Theme.px(Math.min(maxSheetHeight, sheetBody.item?.implicitHeight ?? compact.height), dpr)
readonly property real sheetX: Math.max(insetLeft, Math.min(alongFrom(alignX, compact.x, compact.width, sheetWidth), host.screenWidth - insetRight - sheetWidth))
readonly property real sheetY: alongFrom(alignY, compact.y, compact.height, shownSheetHeight)
readonly property real bandHeight: Math.max(compact.height, maxSheetHeight)
readonly property real bandX: Math.min(compact.x, sheetX)
readonly property real bandY: alongFrom(alignY, compact.y, compact.height, bandHeight)
readonly property real bandRight: Math.max(compact.x + compact.width, sheetX + sheetWidth)
readonly property real originX: Math.max(0, Theme.snap(bandX - shadowBuffer, dpr))
readonly property real originY: Math.max(0, Theme.snap(bandY - shadowBuffer, dpr))
readonly property rect compactLocal: Qt.rect(compact.x - originX, compact.y - originY, compact.width, compact.height)
readonly property rect sheetLocal: Qt.rect(Theme.snap(sheetX - originX, dpr), Theme.snap(sheetY - originY, dpr), sheetWidth, shownSheetHeight)
readonly property int animationDuration: Theme.popoutAnimationDuration
readonly property var springParams: Theme.springPreset("default", animationDuration)
readonly property real morphTravelPx: Math.max(1, Math.abs(sheetLocal.x - compactLocal.x), Math.abs(sheetLocal.y - compactLocal.y), Math.abs(sheetLocal.width - compactLocal.width), Math.abs(sheetLocal.height - compactLocal.height))
readonly property bool morphSettled: !morph.running || Math.abs(morph.value - morph.target) < Theme.morphLayerEpsilon
// A fully transparent surface renders no frames, so the spring would never advance to settle.
readonly property bool closeFinished: closing && (morphSettled || FluidGeometry.chromeOpacity(morph.value) <= 0)
readonly property real morphProgress: morphSettled ? morph.target : morph.value
readonly property real progress: Math.max(0, Math.min(1, morphProgress))
readonly property rect body: FluidGeometry.limitOvershoot(morphRect(compactLocal, sheetLocal, morphProgress), sheetLocal, Theme.fluidOvershootLimit)
readonly property real surfaceRadius: host.surfaceRadius + (Theme.windowRadius - host.surfaceRadius) * progress
signal presented
signal dismissed
function alongFrom(align, start, size, target) {
switch (align) {
case -1:
return start;
case 1:
return start + size - target;
default:
return start + (size - target) / 2;
}
}
function alignIn(align, outer, inner) {
switch (align) {
case -1:
return 0;
case 1:
return outer - inner;
default:
return (outer - inner) / 2;
}
}
// FluidGeometry only bounds growth; a shrinking axis overshooting its target would crush the fixed-size content.
function morphRect(from, to, value) {
const grown = FluidGeometry.interpolate(from, to, value);
const held = FluidGeometry.interpolate(from, to, Math.min(1, value));
const holdX = to.width < from.width;
const holdY = to.height < from.height;
return Qt.rect(holdX ? held.x : grown.x, holdY ? held.y : grown.y, holdX ? held.width : grown.width, holdY ? held.height : grown.height);
}
function present() {
opened = true;
presented();
morph.retarget(1);
keyScope.forceActiveFocus();
}
function close() {
if (closing)
return;
morph.retarget(0);
closing = true;
}
onCloseFinishedChanged: {
if (closeFinished)
dismissed();
}
screen: host.screen
color: "transparent"
exclusiveZone: -1
WlrLayershell.namespace: host.blurNamespace
WlrLayershell.layer: host.WlrLayershell.layer
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(opened && !closing)
anchors {
top: true
left: true
}
WlrLayershell.margins {
left: root.originX
top: root.originY
}
implicitWidth: Math.min(host.screenWidth - originX, Theme.snap(bandRight + shadowBuffer, dpr) - originX)
implicitHeight: Math.min(host.screenHeight - originY, Theme.snap(bandY + bandHeight + shadowBuffer, dpr) - originY)
SpringMotion {
id: morph
enabled: !Theme.springMotionDisabled
reducedMotion: root.animationDuration <= 0 || SettingsData.reduceMotion
positionEpsilon: Math.max(0.001, 0.25 / root.dpr / root.morphTravelPx)
velocityEpsilon: positionEpsilon * damping / Math.max(0.001, 2 * mass)
stiffness: root.springParams.stiffness
damping: root.springParams.damping
Component.onCompleted: snapTo(0)
}
WindowBlur {
targetWindow: root
surfaceColor: Theme.withAlpha(root.host.surfaceColor, Theme.popupTransparency)
blurX: surface.x
blurY: surface.y
blurWidth: surface.opacity < 1 ? 0 : surface.width
blurHeight: surface.opacity < 1 ? 0 : surface.height
blurRadius: root.surfaceRadius
}
DankFocusGrab {
windows: [root, dismissWindow].concat(KeyboardFocus.barWindows)
wanted: KeyboardFocus.wantsGrab(root.opened && !root.closing)
onCleared: root.close()
}
Connections {
target: surface.Window.window
enabled: !root.opened
function onFrameSwapped() {
root.present();
}
}
FocusScope {
id: keyScope
anchors.fill: parent
Keys.onEscapePressed: root.close()
MouseArea {
anchors.fill: parent
enabled: root.opened
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.close()
}
Item {
id: surface
x: root.body.x
y: root.body.y
width: root.body.width
height: root.body.height
opacity: root.closing ? FluidGeometry.chromeOpacity(morph.value) : 1
ElevationShadow {
anchors.fill: parent
z: -2
level: Theme.elevationLevel2
fallbackOffset: Theme.spacingXS
targetRadius: root.surfaceRadius
targetColor: Theme.withAlpha(root.host.surfaceColor, Theme.popupTransparency)
borderColor: Theme.outlineVariant
borderWidth: 0
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onWheel: wheel => wheel.accepted = true
}
Item {
anchors.fill: parent
clip: true
Loader {
x: root.alignIn(root.alignX, parent.width, width)
y: root.alignIn(root.alignY, parent.height, height)
width: root.compact.width
height: root.compact.height
sourceComponent: root.host.content
opacity: root.closing ? 0 : 1 - FluidGeometry.chromeOpacity(morph.value)
visible: opacity > 0
enabled: !root.opened
}
Loader {
id: sheetBody
x: root.alignIn(root.alignX, parent.width, width)
y: root.alignIn(root.alignY, parent.height, height)
width: root.sheetWidth
height: root.sheetHeight
sourceComponent: root.host.sheet
opacity: FluidGeometry.contentOpacity(root.body, root.sheetLocal)
visible: opacity > 0
focus: true
}
}
}
}
Connections {
target: sheetBody.item
ignoreUnknownSignals: true
function onCloseRequested() {
root.close();
}
}
PanelWindow {
id: dismissWindow
property bool mapped: false
screen: root.screen
visible: root.opened && !root.closing
color: "transparent"
exclusiveZone: -1
// Input only: after the first transparent frame maps it, repainting would damage the whole screen.
updatesEnabled: !mapped
WlrLayershell.namespace: root.host.blurNamespace + ":dismiss"
WlrLayershell.layer: root.WlrLayershell.layer
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
bottom: true
left: true
right: true
}
mask: Region {
width: dismissWindow.width
height: dismissWindow.height
Region {
x: root.originX
y: root.originY
width: root.width
height: root.height
intersection: Intersection.Subtract
}
}
Connections {
target: dismissSurface.Window.window
enabled: !dismissWindow.mapped
function onFrameSwapped() {
dismissWindow.mapped = true;
}
}
MouseArea {
id: dismissSurface
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.close()
}
}
}