mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
1 parent
219a95d869
commit
db903801c3
24 files changed
+983
-90
No files matched your search
+1
-1
Submodule dank-qml-common updated: 2d206c20d1...fc42c683c7.
Generated
+3
-3
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -9,6 +9,8 @@ import qs.Widgets
|
||||
DankOSD {
|
||||
id: root
|
||||
|
||||
osdKind: "workspace"
|
||||
|
||||
property string workspaceLabel: ""
|
||||
property var lastActiveWorkspaceId: null
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user