mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-04 03:22:12 -04:00
feat(DMS FrameMode): A New Connected Unified Surface & Animation Overhaul
- Introduces Standalone & Connected Modes - Updated Animations & Motion effects for both modes - Numerous QOL tweaks and updates throughout the system - Highly inspired to the OG Caelestia Shell / @Soramanew
This commit is contained in:
@@ -1,26 +1,13 @@
|
||||
repos:
|
||||
- repo: local
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.10.1
|
||||
hooks:
|
||||
- id: golangci-lint-fmt
|
||||
name: golangci-lint-fmt
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
|
||||
language: system
|
||||
require_serial: true
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
- id: golangci-lint-full
|
||||
name: golangci-lint-full
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
|
||||
language: system
|
||||
require_serial: true
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
- id: golangci-lint-config-verify
|
||||
name: golangci-lint-config-verify
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
|
||||
language: system
|
||||
files: \.golangci\.(?:yml|yaml|toml|json)
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: go-test
|
||||
name: go test
|
||||
entry: go test ./...
|
||||
|
||||
62
quickshell/Common/AnimVariants.qml
Normal file
62
quickshell/Common/AnimVariants.qml
Normal file
@@ -0,0 +1,62 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
// AnimVariants — central tuning for animation variants (Material/Fluent/Dynamic)
|
||||
// and motion effects (Standard/Directional/Depth). Lookups are indexed by enum
|
||||
// value: animationVariant 0=Material, 1=Fluent, 2=Dynamic; motionEffect
|
||||
// 0=Standard, 1=Directional, 2=Depth.
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int _variant: (typeof SettingsData === "undefined") ? 0 : SettingsData.animationVariant
|
||||
readonly property int _effect: (typeof SettingsData === "undefined") ? 0 : SettingsData.motionEffect
|
||||
|
||||
readonly property var _enterCurves: [Anims.expressiveDefaultSpatial, Anims.standardDecel, Anims.expressiveFastSpatial]
|
||||
readonly property var _exitCurves: [Anims.emphasized, Anims.standard, Anims.emphasized]
|
||||
readonly property var _directionalExitCurves: [Anims.emphasized, Anims.emphasizedAccel, Anims.emphasizedAccel]
|
||||
readonly property var _enterDurationFactors: [1.0, 0.9, 1.08]
|
||||
readonly property var _exitDurationFactors: [1.0, 0.85, 0.92]
|
||||
readonly property var _cleanupPaddings: [50, 8, 24]
|
||||
readonly property var _effectScaleCollapsed: [0.96, 1.0, 0.88]
|
||||
readonly property var _effectAnimOffsets: [16, 144, 56]
|
||||
|
||||
readonly property list<real> variantEnterCurve: _enterCurves[_variant] || _enterCurves[0]
|
||||
readonly property list<real> variantExitCurve: _exitCurves[_variant] || _exitCurves[0]
|
||||
|
||||
readonly property list<real> variantModalEnterCurve: isDirectionalEffect && _variant !== 0 ? (_enterCurves[_variant] || _enterCurves[0]) : variantEnterCurve
|
||||
readonly property list<real> variantModalExitCurve: isDirectionalEffect ? (_directionalExitCurves[_variant] || _exitCurves[0]) : variantExitCurve
|
||||
|
||||
readonly property list<real> variantPopoutEnterCurve: isDirectionalEffect ? (_variant === 0 ? Anims.standardDecel : (_enterCurves[_variant] || _enterCurves[0])) : variantEnterCurve
|
||||
readonly property list<real> variantPopoutExitCurve: isDirectionalEffect ? (_directionalExitCurves[_variant] || _exitCurves[0]) : variantExitCurve
|
||||
|
||||
readonly property real variantEnterDurationFactor: _enterDurationFactors[_variant] !== undefined ? _enterDurationFactors[_variant] : 1.0
|
||||
readonly property real variantExitDurationFactor: _exitDurationFactors[_variant] !== undefined ? _exitDurationFactors[_variant] : 1.0
|
||||
|
||||
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
|
||||
readonly property real variantOpacityDurationScale: _variant === 1 ? 0.55 : 1.0
|
||||
|
||||
function variantDuration(baseDuration, entering) {
|
||||
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
|
||||
return Math.max(0, Math.round(baseDuration * factor));
|
||||
}
|
||||
|
||||
function variantExitCleanupPadding() {
|
||||
return _cleanupPaddings[_effect] !== undefined ? _cleanupPaddings[_effect] : 50;
|
||||
}
|
||||
|
||||
function variantCloseInterval(baseDuration) {
|
||||
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
|
||||
}
|
||||
|
||||
readonly property bool isDirectionalEffect: isConnectedEffect || _effect === 1
|
||||
readonly property bool isDepthEffect: _effect === 2
|
||||
readonly property bool isConnectedEffect: (typeof SettingsData !== "undefined") && SettingsData.connectedFrameModeActive
|
||||
|
||||
readonly property real effectScaleCollapsed: _effectScaleCollapsed[_effect] !== undefined ? _effectScaleCollapsed[_effect] : 0.96
|
||||
readonly property real effectAnimOffset: _effectAnimOffsets[_effect] !== undefined ? _effectAnimOffsets[_effect] : 16
|
||||
}
|
||||
@@ -22,4 +22,9 @@ Singleton {
|
||||
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
|
||||
|
||||
// Used by AnimVariants for variant/effect logic
|
||||
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
|
||||
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
481
quickshell/Common/ConnectedModeState.qml
Normal file
481
quickshell/Common/ConnectedModeState.qml
Normal file
@@ -0,0 +1,481 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var emptyDockState: ({
|
||||
"reveal": false,
|
||||
"barSide": "bottom",
|
||||
"bodyX": 0,
|
||||
"bodyY": 0,
|
||||
"bodyW": 0,
|
||||
"bodyH": 0,
|
||||
"slideX": 0,
|
||||
"slideY": 0
|
||||
})
|
||||
|
||||
// Popout state (updated by DankPopout when connectedFrameModeActive)
|
||||
property string popoutOwnerId: ""
|
||||
property bool popoutVisible: false
|
||||
property string popoutBarSide: "top"
|
||||
property real popoutBodyX: 0
|
||||
property real popoutBodyY: 0
|
||||
property real popoutBodyW: 0
|
||||
property real popoutBodyH: 0
|
||||
property real popoutAnimX: 0
|
||||
property real popoutAnimY: 0
|
||||
property string popoutScreen: ""
|
||||
property bool popoutOmitStartConnector: false
|
||||
property bool popoutOmitEndConnector: false
|
||||
|
||||
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
|
||||
property var dockStates: ({})
|
||||
|
||||
// Dock slide offsets — hot-path updates separated from full geometry state
|
||||
property var dockSlides: ({})
|
||||
|
||||
function _cloneDict(src) {
|
||||
const next = {};
|
||||
for (const k in src)
|
||||
next[k] = src[k];
|
||||
return next;
|
||||
}
|
||||
|
||||
function hasPopoutOwner(claimId) {
|
||||
return !!claimId && popoutOwnerId === claimId;
|
||||
}
|
||||
|
||||
function claimPopout(claimId, state) {
|
||||
if (!claimId)
|
||||
return false;
|
||||
|
||||
popoutOwnerId = claimId;
|
||||
return updatePopout(claimId, state);
|
||||
}
|
||||
|
||||
function updatePopout(claimId, state) {
|
||||
if (!hasPopoutOwner(claimId) || !state)
|
||||
return false;
|
||||
|
||||
if (state.visible !== undefined)
|
||||
popoutVisible = !!state.visible;
|
||||
if (state.barSide !== undefined)
|
||||
popoutBarSide = state.barSide || "top";
|
||||
if (state.bodyX !== undefined)
|
||||
popoutBodyX = Number(state.bodyX);
|
||||
if (state.bodyY !== undefined)
|
||||
popoutBodyY = Number(state.bodyY);
|
||||
if (state.bodyW !== undefined)
|
||||
popoutBodyW = Number(state.bodyW);
|
||||
if (state.bodyH !== undefined)
|
||||
popoutBodyH = Number(state.bodyH);
|
||||
if (state.animX !== undefined)
|
||||
popoutAnimX = Number(state.animX);
|
||||
if (state.animY !== undefined)
|
||||
popoutAnimY = Number(state.animY);
|
||||
if (state.screen !== undefined)
|
||||
popoutScreen = state.screen || "";
|
||||
if (state.omitStartConnector !== undefined)
|
||||
popoutOmitStartConnector = !!state.omitStartConnector;
|
||||
if (state.omitEndConnector !== undefined)
|
||||
popoutOmitEndConnector = !!state.omitEndConnector;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function releasePopout(claimId) {
|
||||
if (!hasPopoutOwner(claimId))
|
||||
return false;
|
||||
|
||||
popoutOwnerId = "";
|
||||
popoutVisible = false;
|
||||
popoutBarSide = "top";
|
||||
popoutBodyX = 0;
|
||||
popoutBodyY = 0;
|
||||
popoutBodyW = 0;
|
||||
popoutBodyH = 0;
|
||||
popoutAnimX = 0;
|
||||
popoutAnimY = 0;
|
||||
popoutScreen = "";
|
||||
popoutOmitStartConnector = false;
|
||||
popoutOmitEndConnector = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function setPopoutAnim(claimId, animX, animY) {
|
||||
if (!hasPopoutOwner(claimId))
|
||||
return false;
|
||||
if (animX !== undefined) {
|
||||
const nextX = Number(animX);
|
||||
if (!isNaN(nextX) && popoutAnimX !== nextX)
|
||||
popoutAnimX = nextX;
|
||||
}
|
||||
if (animY !== undefined) {
|
||||
const nextY = Number(animY);
|
||||
if (!isNaN(nextY) && popoutAnimY !== nextY)
|
||||
popoutAnimY = nextY;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function setPopoutBody(claimId, bodyX, bodyY, bodyW, bodyH) {
|
||||
if (!hasPopoutOwner(claimId))
|
||||
return false;
|
||||
if (bodyX !== undefined) {
|
||||
const nextX = Number(bodyX);
|
||||
if (!isNaN(nextX) && popoutBodyX !== nextX)
|
||||
popoutBodyX = nextX;
|
||||
}
|
||||
if (bodyY !== undefined) {
|
||||
const nextY = Number(bodyY);
|
||||
if (!isNaN(nextY) && popoutBodyY !== nextY)
|
||||
popoutBodyY = nextY;
|
||||
}
|
||||
if (bodyW !== undefined) {
|
||||
const nextW = Number(bodyW);
|
||||
if (!isNaN(nextW) && popoutBodyW !== nextW)
|
||||
popoutBodyW = nextW;
|
||||
}
|
||||
if (bodyH !== undefined) {
|
||||
const nextH = Number(bodyH);
|
||||
if (!isNaN(nextH) && popoutBodyH !== nextH)
|
||||
popoutBodyH = nextH;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _normalizeDockState(state) {
|
||||
return {
|
||||
"reveal": !!(state && state.reveal),
|
||||
"barSide": state && state.barSide ? state.barSide : "bottom",
|
||||
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
|
||||
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
|
||||
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
|
||||
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
|
||||
"slideX": Number(state && state.slideX !== undefined ? state.slideX : 0),
|
||||
"slideY": Number(state && state.slideY !== undefined ? state.slideY : 0)
|
||||
};
|
||||
}
|
||||
|
||||
function _sameDockState(a, b) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
return a.reveal === b.reveal && a.barSide === b.barSide && Math.abs(a.bodyX - b.bodyX) < 0.5 && Math.abs(a.bodyY - b.bodyY) < 0.5 && Math.abs(a.bodyW - b.bodyW) < 0.5 && Math.abs(a.bodyH - b.bodyH) < 0.5 && Math.abs(a.slideX - b.slideX) < 0.5 && Math.abs(a.slideY - b.slideY) < 0.5;
|
||||
}
|
||||
|
||||
function setDockState(screenName, state) {
|
||||
if (!screenName || !state)
|
||||
return false;
|
||||
|
||||
const normalized = _normalizeDockState(state);
|
||||
if (_sameDockState(dockStates[screenName], normalized))
|
||||
return true;
|
||||
|
||||
const next = _cloneDict(dockStates);
|
||||
next[screenName] = normalized;
|
||||
dockStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearDockState(screenName) {
|
||||
if (!screenName || !dockStates[screenName])
|
||||
return false;
|
||||
|
||||
const next = _cloneDict(dockStates);
|
||||
delete next[screenName];
|
||||
dockStates = next;
|
||||
|
||||
// Also clear corresponding slide
|
||||
if (dockSlides[screenName]) {
|
||||
const nextSlides = _cloneDict(dockSlides);
|
||||
delete nextSlides[screenName];
|
||||
dockSlides = nextSlides;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function setDockSlide(screenName, x, y) {
|
||||
if (!screenName)
|
||||
return false;
|
||||
const numX = Number(x);
|
||||
const numY = Number(y);
|
||||
const cur = dockSlides[screenName];
|
||||
if (cur && Math.abs(cur.x - numX) < 0.5 && Math.abs(cur.y - numY) < 0.5)
|
||||
return true;
|
||||
const next = _cloneDict(dockSlides);
|
||||
next[screenName] = {
|
||||
"x": numX,
|
||||
"y": numY
|
||||
};
|
||||
dockSlides = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
readonly property var emptyNotificationState: ({
|
||||
"visible": false,
|
||||
"barSide": "top",
|
||||
"bodyX": 0,
|
||||
"bodyY": 0,
|
||||
"bodyW": 0,
|
||||
"bodyH": 0,
|
||||
"omitStartConnector": false,
|
||||
"omitEndConnector": false
|
||||
})
|
||||
|
||||
property var notificationStates: ({})
|
||||
|
||||
function _normalizeNotificationState(state) {
|
||||
return {
|
||||
"visible": !!(state && state.visible),
|
||||
"barSide": state && state.barSide ? state.barSide : "top",
|
||||
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
|
||||
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
|
||||
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
|
||||
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
|
||||
"omitStartConnector": !!(state && state.omitStartConnector),
|
||||
"omitEndConnector": !!(state && state.omitEndConnector)
|
||||
};
|
||||
}
|
||||
|
||||
function _sameNotificationGeometry(a, b) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
return Math.abs(Number(a.bodyX) - Number(b.bodyX)) < 0.5 && Math.abs(Number(a.bodyY) - Number(b.bodyY)) < 0.5 && Math.abs(Number(a.bodyW) - Number(b.bodyW)) < 0.5 && Math.abs(Number(a.bodyH) - Number(b.bodyH)) < 0.5;
|
||||
}
|
||||
|
||||
function _sameNotificationState(a, b) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
return a.visible === b.visible && a.barSide === b.barSide && a.omitStartConnector === b.omitStartConnector && a.omitEndConnector === b.omitEndConnector && _sameNotificationGeometry(a, b);
|
||||
}
|
||||
|
||||
function setNotificationState(screenName, state) {
|
||||
if (!screenName || !state)
|
||||
return false;
|
||||
|
||||
const normalized = _normalizeNotificationState(state);
|
||||
if (_sameNotificationState(notificationStates[screenName], normalized))
|
||||
return true;
|
||||
|
||||
const next = _cloneDict(notificationStates);
|
||||
next[screenName] = normalized;
|
||||
notificationStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearNotificationState(screenName) {
|
||||
if (!screenName || !notificationStates[screenName])
|
||||
return false;
|
||||
|
||||
const next = _cloneDict(notificationStates);
|
||||
delete next[screenName];
|
||||
notificationStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
// DankModal / DankLauncherV2Modal State
|
||||
readonly property var emptyModalState: ({
|
||||
"visible": false,
|
||||
"barSide": "bottom",
|
||||
"bodyX": 0,
|
||||
"bodyY": 0,
|
||||
"bodyW": 0,
|
||||
"bodyH": 0,
|
||||
"animX": 0,
|
||||
"animY": 0,
|
||||
"omitStartConnector": false,
|
||||
"omitEndConnector": false
|
||||
})
|
||||
|
||||
property var modalStates: ({})
|
||||
|
||||
function _normalizeModalState(state) {
|
||||
return {
|
||||
"visible": !!(state && state.visible),
|
||||
"barSide": state && state.barSide ? state.barSide : "bottom",
|
||||
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
|
||||
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
|
||||
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
|
||||
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
|
||||
"animX": Number(state && state.animX !== undefined ? state.animX : 0),
|
||||
"animY": Number(state && state.animY !== undefined ? state.animY : 0),
|
||||
"omitStartConnector": !!(state && state.omitStartConnector),
|
||||
"omitEndConnector": !!(state && state.omitEndConnector)
|
||||
};
|
||||
}
|
||||
|
||||
function _sameModalGeometry(a, b) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
return Math.abs(Number(a.bodyX) - Number(b.bodyX)) < 0.5 && Math.abs(Number(a.bodyY) - Number(b.bodyY)) < 0.5 && Math.abs(Number(a.bodyW) - Number(b.bodyW)) < 0.5 && Math.abs(Number(a.bodyH) - Number(b.bodyH)) < 0.5 && Math.abs(Number(a.animX) - Number(b.animX)) < 0.5 && Math.abs(Number(a.animY) - Number(b.animY)) < 0.5;
|
||||
}
|
||||
|
||||
function _sameModalState(a, b) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
return a.visible === b.visible && a.barSide === b.barSide && a.omitStartConnector === b.omitStartConnector && a.omitEndConnector === b.omitEndConnector && _sameModalGeometry(a, b);
|
||||
}
|
||||
|
||||
function setModalState(screenName, state) {
|
||||
if (!screenName || !state)
|
||||
return false;
|
||||
|
||||
const normalized = _normalizeModalState(state);
|
||||
if (_sameModalState(modalStates[screenName], normalized))
|
||||
return true;
|
||||
|
||||
const next = _cloneDict(modalStates);
|
||||
next[screenName] = normalized;
|
||||
modalStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearModalState(screenName) {
|
||||
if (!screenName || !modalStates[screenName])
|
||||
return false;
|
||||
|
||||
const next = _cloneDict(modalStates);
|
||||
delete next[screenName];
|
||||
modalStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function setModalAnim(screenName, animX, animY) {
|
||||
const cur = screenName ? modalStates[screenName] : null;
|
||||
if (!cur)
|
||||
return false;
|
||||
const nax = animX !== undefined ? Number(animX) : cur.animX;
|
||||
const nay = animY !== undefined ? Number(animY) : cur.animY;
|
||||
if (Math.abs(nax - cur.animX) < 0.5 && Math.abs(nay - cur.animY) < 0.5)
|
||||
return false;
|
||||
const next = _cloneDict(modalStates);
|
||||
next[screenName] = Object.assign({}, cur, {
|
||||
"animX": nax,
|
||||
"animY": nay
|
||||
});
|
||||
modalStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function setModalBody(screenName, bodyX, bodyY, bodyW, bodyH) {
|
||||
const cur = screenName ? modalStates[screenName] : null;
|
||||
if (!cur)
|
||||
return false;
|
||||
const nx = bodyX !== undefined ? Number(bodyX) : cur.bodyX;
|
||||
const ny = bodyY !== undefined ? Number(bodyY) : cur.bodyY;
|
||||
const nw = bodyW !== undefined ? Number(bodyW) : cur.bodyW;
|
||||
const nh = bodyH !== undefined ? Number(bodyH) : cur.bodyH;
|
||||
if (Math.abs(nx - cur.bodyX) < 0.5 && Math.abs(ny - cur.bodyY) < 0.5 && Math.abs(nw - cur.bodyW) < 0.5 && Math.abs(nh - cur.bodyH) < 0.5)
|
||||
return false;
|
||||
const next = _cloneDict(modalStates);
|
||||
next[screenName] = Object.assign({}, cur, {
|
||||
"bodyX": nx,
|
||||
"bodyY": ny,
|
||||
"bodyW": nw,
|
||||
"bodyH": nh
|
||||
});
|
||||
modalStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
property var dockRetractRequests: ({})
|
||||
|
||||
function requestDockRetract(requesterId, screenName, side) {
|
||||
if (!requesterId || !screenName || !side)
|
||||
return false;
|
||||
const existing = dockRetractRequests[requesterId];
|
||||
if (existing && existing.screenName === screenName && existing.side === side)
|
||||
return true;
|
||||
const next = _cloneDict(dockRetractRequests);
|
||||
next[requesterId] = {
|
||||
"screenName": screenName,
|
||||
"side": side
|
||||
};
|
||||
dockRetractRequests = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function releaseDockRetract(requesterId) {
|
||||
if (!requesterId || !dockRetractRequests[requesterId])
|
||||
return false;
|
||||
const next = _cloneDict(dockRetractRequests);
|
||||
delete next[requesterId];
|
||||
dockRetractRequests = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function dockRetractActiveForSide(screenName, side) {
|
||||
if (!screenName || !side)
|
||||
return false;
|
||||
for (const k in dockRetractRequests) {
|
||||
const r = dockRetractRequests[k];
|
||||
if (r && r.screenName === screenName && r.side === side)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prune state for screens that are no longer connected. Stale entries
|
||||
// accumulate across hotplug cycles otherwise — Frame's per-screen
|
||||
// FrameInstance doesn't notice when its peer dicts go orphan.
|
||||
function _pruneToLiveScreens() {
|
||||
const live = {};
|
||||
const screens = Quickshell.screens || [];
|
||||
for (let i = 0; i < screens.length; i++) {
|
||||
const s = screens[i];
|
||||
if (s && s.name)
|
||||
live[s.name] = true;
|
||||
}
|
||||
|
||||
function pruneKeyed(dict) {
|
||||
let changed = false;
|
||||
const next = {};
|
||||
for (const k in dict) {
|
||||
if (live[k])
|
||||
next[k] = dict[k];
|
||||
else
|
||||
changed = true;
|
||||
}
|
||||
return changed ? next : null;
|
||||
}
|
||||
|
||||
const nextDock = pruneKeyed(dockStates);
|
||||
if (nextDock !== null)
|
||||
dockStates = nextDock;
|
||||
const nextSlides = pruneKeyed(dockSlides);
|
||||
if (nextSlides !== null)
|
||||
dockSlides = nextSlides;
|
||||
const nextNotif = pruneKeyed(notificationStates);
|
||||
if (nextNotif !== null)
|
||||
notificationStates = nextNotif;
|
||||
const nextModal = pruneKeyed(modalStates);
|
||||
if (nextModal !== null)
|
||||
modalStates = nextModal;
|
||||
|
||||
let retractChanged = false;
|
||||
const nextRetract = {};
|
||||
for (const k in dockRetractRequests) {
|
||||
const r = dockRetractRequests[k];
|
||||
if (r && live[r.screenName])
|
||||
nextRetract[k] = r;
|
||||
else
|
||||
retractChanged = true;
|
||||
}
|
||||
if (retractChanged)
|
||||
dockRetractRequests = nextRetract;
|
||||
|
||||
if (popoutOwnerId && popoutScreen && !live[popoutScreen])
|
||||
releasePopout(popoutOwnerId);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
root._pruneToLiveScreens();
|
||||
}
|
||||
}
|
||||
}
|
||||
68
quickshell/Common/ConnectorGeometry.js
Normal file
68
quickshell/Common/ConnectorGeometry.js
Normal file
@@ -0,0 +1,68 @@
|
||||
.pragma library
|
||||
|
||||
// Geometry for connected-frame arc connectors.
|
||||
// `barSide` is one of "top" | "bottom" | "left" | "right" — the edge where the
|
||||
// host bar/dock sits. `placement` is "left" (start) or "right" (end) of the
|
||||
// body's far edge. `radius` is the connector's arc radius. `spacing` is the
|
||||
// gap between the host edge and the body.
|
||||
|
||||
function isVertical(barSide) {
|
||||
return barSide === "left" || barSide === "right";
|
||||
}
|
||||
|
||||
function isHorizontal(barSide) {
|
||||
return barSide === "top" || barSide === "bottom";
|
||||
}
|
||||
|
||||
function connectorWidth(barSide, spacing, radius) {
|
||||
return isVertical(barSide) ? (spacing + radius) : radius;
|
||||
}
|
||||
|
||||
function connectorHeight(barSide, spacing, radius) {
|
||||
return isVertical(barSide) ? radius : (spacing + radius);
|
||||
}
|
||||
|
||||
function seamX(barSide, baseX, bodyWidth, placement) {
|
||||
if (!isVertical(barSide))
|
||||
return placement === "left" ? baseX : baseX + bodyWidth;
|
||||
return barSide === "left" ? baseX : baseX + bodyWidth;
|
||||
}
|
||||
|
||||
function seamY(barSide, baseY, bodyHeight, placement) {
|
||||
if (barSide === "top")
|
||||
return baseY;
|
||||
if (barSide === "bottom")
|
||||
return baseY + bodyHeight;
|
||||
return placement === "left" ? baseY : baseY + bodyHeight;
|
||||
}
|
||||
|
||||
function connectorX(barSide, baseX, bodyWidth, placement, spacing, radius) {
|
||||
var s = seamX(barSide, baseX, bodyWidth, placement);
|
||||
var w = connectorWidth(barSide, spacing, radius);
|
||||
if (!isVertical(barSide))
|
||||
return placement === "left" ? s - w : s;
|
||||
return barSide === "left" ? s : s - w;
|
||||
}
|
||||
|
||||
function connectorY(barSide, baseY, bodyHeight, placement, spacing, radius) {
|
||||
var s = seamY(barSide, baseY, bodyHeight, placement);
|
||||
var h = connectorHeight(barSide, spacing, radius);
|
||||
if (barSide === "top")
|
||||
return s;
|
||||
if (barSide === "bottom")
|
||||
return s - h;
|
||||
return placement === "left" ? s - h : s;
|
||||
}
|
||||
|
||||
// Which corner of the connector's bounding rect hosts the concave arc that
|
||||
// carves into the body. Used for arc-sweep orientation.
|
||||
function arcCorner(barSide, placement) {
|
||||
var left = placement === "left";
|
||||
if (barSide === "top")
|
||||
return left ? "bottomLeft" : "bottomRight";
|
||||
if (barSide === "bottom")
|
||||
return left ? "topLeft" : "topRight";
|
||||
if (barSide === "left")
|
||||
return left ? "topRight" : "bottomRight";
|
||||
return left ? "topLeft" : "bottomLeft";
|
||||
}
|
||||
@@ -13,8 +13,13 @@ Item {
|
||||
|
||||
property color targetColor: "white"
|
||||
property real targetRadius: Theme.cornerRadius
|
||||
property real topLeftRadius: targetRadius
|
||||
property real topRightRadius: targetRadius
|
||||
property real bottomLeftRadius: targetRadius
|
||||
property real bottomRightRadius: targetRadius
|
||||
property color borderColor: "transparent"
|
||||
property real borderWidth: 0
|
||||
property bool useCustomSource: false
|
||||
|
||||
property bool shadowEnabled: Theme.elevationEnabled
|
||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
||||
@@ -46,7 +51,11 @@ Item {
|
||||
Rectangle {
|
||||
id: sourceRect
|
||||
anchors.fill: parent
|
||||
radius: root.targetRadius
|
||||
visible: !root.useCustomSource
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomLeftRadius: root.bottomLeftRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
color: root.targetColor
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
|
||||
@@ -15,7 +15,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SettingsData")
|
||||
|
||||
readonly property int settingsConfigVersion: 5
|
||||
readonly property int settingsConfigVersion: 11
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
@@ -38,6 +38,18 @@ Singleton {
|
||||
Custom
|
||||
}
|
||||
|
||||
enum AnimationVariant {
|
||||
Material,
|
||||
Fluent,
|
||||
Dynamic
|
||||
}
|
||||
|
||||
enum AnimationEffect {
|
||||
Standard, // 0 — M3: scale-in, rises from below
|
||||
Directional, // 1 — pure large slide, no scale
|
||||
Depth // 2 — medium slide with deep depth scale pop
|
||||
}
|
||||
|
||||
enum SuspendBehavior {
|
||||
Suspend,
|
||||
Hibernate,
|
||||
@@ -169,6 +181,10 @@ Singleton {
|
||||
property int modalCustomAnimationDuration: 150
|
||||
property bool enableRippleEffects: true
|
||||
onEnableRippleEffectsChanged: saveSettings()
|
||||
property int animationVariant: SettingsData.AnimationVariant.Material
|
||||
onAnimationVariantChanged: saveSettings()
|
||||
property int motionEffect: SettingsData.AnimationEffect.Standard
|
||||
onMotionEffectChanged: saveSettings()
|
||||
property bool m3ElevationEnabled: true
|
||||
onM3ElevationEnabledChanged: saveSettings()
|
||||
property int m3ElevationIntensity: 12
|
||||
@@ -187,6 +203,7 @@ Singleton {
|
||||
onPopoutElevationEnabledChanged: saveSettings()
|
||||
property bool barElevationEnabled: true
|
||||
onBarElevationEnabledChanged: saveSettings()
|
||||
|
||||
property bool blurEnabled: false
|
||||
onBlurEnabledChanged: saveSettings()
|
||||
property bool blurForegroundLayers: true
|
||||
@@ -203,6 +220,53 @@ Singleton {
|
||||
property bool blurredWallpaperLayer: false
|
||||
property bool blurWallpaperOnOverview: false
|
||||
|
||||
property bool frameEnabled: false
|
||||
onFrameEnabledChanged: saveSettings()
|
||||
property real frameThickness: 16
|
||||
onFrameThicknessChanged: saveSettings()
|
||||
property real frameRounding: 23
|
||||
onFrameRoundingChanged: saveSettings()
|
||||
property string frameColor: ""
|
||||
onFrameColorChanged: saveSettings()
|
||||
property real frameOpacity: 1.0
|
||||
onFrameOpacityChanged: saveSettings()
|
||||
property var frameScreenPreferences: ["all"]
|
||||
onFrameScreenPreferencesChanged: saveSettings()
|
||||
property real frameBarSize: 40
|
||||
onFrameBarSizeChanged: saveSettings()
|
||||
property bool frameShowOnOverview: false
|
||||
onFrameShowOnOverviewChanged: saveSettings()
|
||||
property bool frameBlurEnabled: true
|
||||
onFrameBlurEnabledChanged: saveSettings()
|
||||
property bool frameCloseGaps: true
|
||||
onFrameCloseGapsChanged: saveSettings()
|
||||
property string frameLauncherEmergeSide: "bottom"
|
||||
onFrameLauncherEmergeSideChanged: saveSettings()
|
||||
property bool frameLauncherArcExtender: false
|
||||
onFrameLauncherArcExtenderChanged: saveSettings()
|
||||
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
||||
property string frameMode: "separate"
|
||||
onFrameModeChanged: saveSettings()
|
||||
property var connectedFrameBarStyleBackups: ({})
|
||||
onConnectedFrameBarStyleBackupsChanged: saveSettings()
|
||||
readonly property bool connectedFrameModeActive: frameEnabled && frameMode === "connected"
|
||||
onConnectedFrameModeActiveChanged: {
|
||||
if (_loading)
|
||||
return;
|
||||
_reconcileConnectedFrameBarStyles();
|
||||
}
|
||||
|
||||
readonly property color effectiveFrameColor: {
|
||||
const fc = frameColor;
|
||||
if (!fc || fc === "default")
|
||||
return Theme.surfaceContainer;
|
||||
if (fc === "primary")
|
||||
return Theme.primary;
|
||||
if (fc === "surface")
|
||||
return Theme.surface;
|
||||
return fc;
|
||||
}
|
||||
|
||||
property bool showLauncherButton: true
|
||||
property bool showWorkspaceSwitcher: true
|
||||
property bool showFocusedWindow: true
|
||||
@@ -1276,6 +1340,9 @@ Singleton {
|
||||
|
||||
Store.parse(root, obj);
|
||||
|
||||
if (obj?.directionalAnimationMode === 3 && frameMode !== "connected")
|
||||
frameMode = "connected";
|
||||
|
||||
if (obj?.weatherLocation !== undefined)
|
||||
_legacyWeatherLocation = obj.weatherLocation;
|
||||
if (obj?.weatherCoordinates !== undefined)
|
||||
@@ -1303,6 +1370,7 @@ Singleton {
|
||||
_loading = false;
|
||||
}
|
||||
loadPluginSettings();
|
||||
Qt.callLater(() => _reconcileConnectedFrameBarStyles());
|
||||
}
|
||||
|
||||
property var _pendingMigration: null
|
||||
@@ -1416,6 +1484,141 @@ Singleton {
|
||||
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2));
|
||||
}
|
||||
|
||||
function _connectedFrameBarStyleSnapshot(config) {
|
||||
return {
|
||||
"shadowIntensity": config?.shadowIntensity ?? 0,
|
||||
"squareCorners": config?.squareCorners ?? false,
|
||||
"gothCornersEnabled": config?.gothCornersEnabled ?? false,
|
||||
"borderEnabled": config?.borderEnabled ?? false
|
||||
};
|
||||
}
|
||||
|
||||
function _hasConnectedFrameBarStyleBackups() {
|
||||
return connectedFrameBarStyleBackups && Object.keys(connectedFrameBarStyleBackups).length > 0;
|
||||
}
|
||||
|
||||
function _captureConnectedFrameBarStyleBackups(configs, overwriteExisting) {
|
||||
if (!Array.isArray(configs))
|
||||
return;
|
||||
|
||||
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
|
||||
const validIds = {};
|
||||
let changed = false;
|
||||
|
||||
for (let i = 0; i < configs.length; i++) {
|
||||
const config = configs[i];
|
||||
if (!config?.id)
|
||||
continue;
|
||||
validIds[config.id] = true;
|
||||
|
||||
if (!overwriteExisting && nextBackups[config.id] !== undefined)
|
||||
continue;
|
||||
|
||||
const snapshot = _connectedFrameBarStyleSnapshot(config);
|
||||
if (JSON.stringify(nextBackups[config.id]) !== JSON.stringify(snapshot)) {
|
||||
nextBackups[config.id] = snapshot;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (overwriteExisting) {
|
||||
for (const barId in nextBackups) {
|
||||
if (validIds[barId])
|
||||
continue;
|
||||
delete nextBackups[barId];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
connectedFrameBarStyleBackups = nextBackups;
|
||||
}
|
||||
|
||||
function _restoreConnectedFrameBarStyleBackups() {
|
||||
if (!_hasConnectedFrameBarStyleBackups())
|
||||
return;
|
||||
|
||||
const backups = connectedFrameBarStyleBackups || {};
|
||||
const configs = JSON.parse(JSON.stringify(barConfigs));
|
||||
let changed = false;
|
||||
|
||||
for (let i = 0; i < configs.length; i++) {
|
||||
const backup = backups[configs[i].id];
|
||||
if (!backup)
|
||||
continue;
|
||||
for (const key in backup) {
|
||||
if (configs[i][key] === backup[key])
|
||||
continue;
|
||||
configs[i][key] = backup[key];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
barConfigs = configs;
|
||||
connectedFrameBarStyleBackups = ({});
|
||||
if (changed)
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
// Zeroes out connected-mode-hostile fields (shadow, square/goth corners, border).
|
||||
// Returns { configs, changed } — `configs` is the same ref when no change.
|
||||
function _sanitizeBarConfigsForConnectedFrame(configs) {
|
||||
if (!connectedFrameModeActive || !Array.isArray(configs))
|
||||
return {
|
||||
"configs": configs,
|
||||
"changed": false
|
||||
};
|
||||
|
||||
let anyChanged = false;
|
||||
const out = configs.map(cfg => {
|
||||
if (!cfg)
|
||||
return cfg;
|
||||
let dirty = false;
|
||||
const s = Object.assign({}, cfg);
|
||||
if ((s.shadowIntensity ?? 0) !== 0) {
|
||||
s.shadowIntensity = 0;
|
||||
dirty = true;
|
||||
}
|
||||
if (s.squareCorners ?? false) {
|
||||
s.squareCorners = false;
|
||||
dirty = true;
|
||||
}
|
||||
if (s.gothCornersEnabled ?? false) {
|
||||
s.gothCornersEnabled = false;
|
||||
dirty = true;
|
||||
}
|
||||
if (s.borderEnabled ?? false) {
|
||||
s.borderEnabled = false;
|
||||
dirty = true;
|
||||
}
|
||||
if (dirty)
|
||||
anyChanged = true;
|
||||
return dirty ? s : cfg;
|
||||
});
|
||||
return {
|
||||
"configs": anyChanged ? out : configs,
|
||||
"changed": anyChanged
|
||||
};
|
||||
}
|
||||
|
||||
// Single entry point for connected-mode bar-style state.
|
||||
// active → capture backups (if not yet) and sanitize bar configs
|
||||
// !active → restore backups
|
||||
function _reconcileConnectedFrameBarStyles() {
|
||||
if (!connectedFrameModeActive) {
|
||||
_restoreConnectedFrameBarStyleBackups();
|
||||
return;
|
||||
}
|
||||
if (!_hasConnectedFrameBarStyleBackups())
|
||||
_captureConnectedFrameBarStyleBackups(barConfigs, true);
|
||||
const result = _sanitizeBarConfigsForConnectedFrame(barConfigs);
|
||||
if (result.changed) {
|
||||
barConfigs = result.configs;
|
||||
updateBarConfigs();
|
||||
}
|
||||
}
|
||||
|
||||
function detectAvailableIconThemes() {
|
||||
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || "";
|
||||
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
|
||||
@@ -1563,35 +1766,37 @@ Singleton {
|
||||
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
|
||||
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
|
||||
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
||||
const bottomGap = Math.max(0, rawBottomGap);
|
||||
const isConnected = connectedFrameModeActive;
|
||||
const bottomGap = isConnected ? 0 : Math.max(0, rawBottomGap);
|
||||
|
||||
const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
|
||||
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4);
|
||||
const popupGap = useAutoGaps ? Math.max(4, spacing) : manualGapValue;
|
||||
const popupGap = isConnected ? 0 : (useAutoGaps ? Math.max(4, spacing) : manualGapValue);
|
||||
const edgeSpacing = isConnected ? 0 : spacing;
|
||||
|
||||
switch (position) {
|
||||
case SettingsData.Position.Left:
|
||||
return {
|
||||
"x": barThickness + spacing + popupGap,
|
||||
"x": barThickness + edgeSpacing + popupGap,
|
||||
"y": relativeY,
|
||||
"width": widgetWidth
|
||||
};
|
||||
case SettingsData.Position.Right:
|
||||
return {
|
||||
"x": (screen?.width || 0) - (barThickness + spacing + popupGap),
|
||||
"x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap),
|
||||
"y": relativeY,
|
||||
"width": widgetWidth
|
||||
};
|
||||
case SettingsData.Position.Bottom:
|
||||
return {
|
||||
"x": relativeX,
|
||||
"y": (screen?.height || 0) - (barThickness + spacing + bottomGap + popupGap),
|
||||
"y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap),
|
||||
"width": widgetWidth
|
||||
};
|
||||
default:
|
||||
return {
|
||||
"x": relativeX,
|
||||
"y": barThickness + spacing + bottomGap + popupGap,
|
||||
"y": barThickness + edgeSpacing + bottomGap + popupGap,
|
||||
"width": widgetWidth
|
||||
};
|
||||
}
|
||||
@@ -1685,7 +1890,9 @@ Singleton {
|
||||
const screenWidth = screen.width;
|
||||
const screenHeight = screen.height;
|
||||
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
|
||||
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
||||
const isConnected = connectedFrameModeActive;
|
||||
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
||||
const bottomGap = isConnected ? 0 : rawBottomGap;
|
||||
|
||||
let topOffset = 0;
|
||||
let bottomOffset = 0;
|
||||
@@ -1707,7 +1914,7 @@ Singleton {
|
||||
const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4);
|
||||
const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4);
|
||||
const otherThickness = Math.max(26 + otherPadding * 0.6, Theme.barHeight - 4 - (8 - otherPadding)) + otherSpacing + wingSize;
|
||||
const otherBottomGap = other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0);
|
||||
const otherBottomGap = isConnected ? 0 : (other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0));
|
||||
|
||||
switch (other.position) {
|
||||
case SettingsData.Position.Top:
|
||||
@@ -1798,7 +2005,9 @@ Singleton {
|
||||
function addBarConfig(config) {
|
||||
const configs = JSON.parse(JSON.stringify(barConfigs));
|
||||
configs.push(config);
|
||||
barConfigs = configs;
|
||||
if (connectedFrameModeActive)
|
||||
_captureConnectedFrameBarStyleBackups(configs, false);
|
||||
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
@@ -1810,7 +2019,7 @@ Singleton {
|
||||
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
|
||||
|
||||
Object.assign(configs[index], updates);
|
||||
barConfigs = configs;
|
||||
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
|
||||
updateBarConfigs();
|
||||
|
||||
if (positionChanged) {
|
||||
@@ -1864,6 +2073,11 @@ Singleton {
|
||||
return;
|
||||
const configs = barConfigs.filter(cfg => cfg.id !== barId);
|
||||
barConfigs = configs;
|
||||
if (connectedFrameBarStyleBackups?.[barId] !== undefined) {
|
||||
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
|
||||
delete nextBackups[barId];
|
||||
connectedFrameBarStyleBackups = nextBackups;
|
||||
}
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
@@ -1958,6 +2172,95 @@ Singleton {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getFrameFilteredScreens() {
|
||||
var prefs = frameScreenPreferences || ["all"];
|
||||
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
|
||||
return Quickshell.screens;
|
||||
}
|
||||
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
|
||||
}
|
||||
|
||||
function getActiveBarEdgeForScreen(screen) {
|
||||
if (!screen)
|
||||
return "";
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled)
|
||||
continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
|
||||
continue;
|
||||
switch (bc.position ?? 0) {
|
||||
case SettingsData.Position.Top:
|
||||
return "top";
|
||||
case SettingsData.Position.Bottom:
|
||||
return "bottom";
|
||||
case SettingsData.Position.Left:
|
||||
return "left";
|
||||
case SettingsData.Position.Right:
|
||||
return "right";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getActiveBarEdgesForScreen(screen) {
|
||||
if (!screen)
|
||||
return [];
|
||||
var edges = [];
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled)
|
||||
continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
|
||||
continue;
|
||||
switch (bc.position ?? 0) {
|
||||
case SettingsData.Position.Top:
|
||||
edges.push("top");
|
||||
break;
|
||||
case SettingsData.Position.Bottom:
|
||||
edges.push("bottom");
|
||||
break;
|
||||
case SettingsData.Position.Left:
|
||||
edges.push("left");
|
||||
break;
|
||||
case SettingsData.Position.Right:
|
||||
edges.push("right");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
function frameEdgeInsetForSide(screen, side) {
|
||||
if (!frameEnabled || !screen)
|
||||
return 0;
|
||||
const edges = getActiveBarEdgesForScreen(screen);
|
||||
return edges.includes(side) ? frameBarSize : frameThickness;
|
||||
}
|
||||
|
||||
function getActiveBarThicknessForScreen(screen) {
|
||||
if (frameEnabled)
|
||||
return frameBarSize;
|
||||
if (!screen)
|
||||
return frameThickness;
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled)
|
||||
continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
|
||||
continue;
|
||||
const innerPadding = bc.innerPadding ?? 4;
|
||||
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
|
||||
const spacing = bc.spacing ?? 4;
|
||||
const bottomGap = bc.bottomGap ?? 0;
|
||||
return barT + spacing + bottomGap;
|
||||
}
|
||||
return frameThickness;
|
||||
}
|
||||
|
||||
function sendTestNotifications() {
|
||||
NotificationService.dismissAllPopups();
|
||||
sendTestNotification(0);
|
||||
|
||||
@@ -986,6 +986,46 @@ Singleton {
|
||||
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
// Theme is the canonical access point for animation variant state. The
|
||||
// aliases below forward to AnimVariants.qml so consumers don't need two
|
||||
// imports. ~200 call sites read through Theme.variantEnterCurve /
|
||||
// Theme.isConnectedEffect / etc. — do NOT migrate to AnimVariants directly.
|
||||
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
|
||||
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
|
||||
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
|
||||
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
|
||||
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
|
||||
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
|
||||
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
|
||||
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
|
||||
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
|
||||
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
|
||||
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
|
||||
readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
|
||||
readonly property real connectedCornerRadius: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 12;
|
||||
return SettingsData.connectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
|
||||
}
|
||||
readonly property color connectedSurfaceColor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return withAlpha(surfaceContainer, popupTransparency);
|
||||
return isConnectedEffect ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : withAlpha(surfaceContainer, popupTransparency);
|
||||
}
|
||||
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
|
||||
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined") ? true : (!isConnectedEffect || SettingsData.frameBlurEnabled)
|
||||
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
|
||||
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
|
||||
function variantDuration(baseDuration, entering) {
|
||||
return AnimVariants.variantDuration(baseDuration, entering);
|
||||
}
|
||||
function variantExitCleanupPadding() {
|
||||
return AnimVariants.variantExitCleanupPadding();
|
||||
}
|
||||
function variantCloseInterval(baseDuration) {
|
||||
return AnimVariants.variantCloseInterval(baseDuration);
|
||||
}
|
||||
|
||||
readonly property var animationPresetDurations: {
|
||||
"none": 0,
|
||||
"short": 250,
|
||||
@@ -1061,6 +1101,9 @@ Singleton {
|
||||
return base === 0 ? 0 : Math.round(base * 0.85);
|
||||
}
|
||||
|
||||
readonly property int notificationInlineExpandDuration: notificationAnimationBaseDuration === 0 ? 0 : 185
|
||||
readonly property int notificationInlineCollapseDuration: notificationAnimationBaseDuration === 0 ? 0 : 150
|
||||
|
||||
readonly property real notificationIconSizeNormal: 56
|
||||
readonly property real notificationIconSizeCompact: 48
|
||||
readonly property real notificationExpandedIconSizeNormal: 48
|
||||
@@ -1151,7 +1194,13 @@ Singleton {
|
||||
property real iconSizeLarge: 32
|
||||
|
||||
property real panelTransparency: 0.85
|
||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
|
||||
property real popupTransparency: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 1.0;
|
||||
if (isConnectedEffect)
|
||||
return SettingsData.frameOpacity !== undefined ? SettingsData.frameOpacity : 1.0;
|
||||
return SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0;
|
||||
}
|
||||
|
||||
function screenTransition() {
|
||||
if (CompositorService.isNiri) {
|
||||
@@ -1852,6 +1901,12 @@ Singleton {
|
||||
return Qt.rgba(c.r, c.g, c.b, a);
|
||||
}
|
||||
|
||||
function popupLayerColor(baseColor) {
|
||||
if (isConnectedEffect)
|
||||
return connectedSurfaceColor;
|
||||
return withAlpha(baseColor, popupTransparency);
|
||||
}
|
||||
|
||||
function blendAlpha(c, a) {
|
||||
return Qt.rgba(c.r, c.g, c.b, c.a * a);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ var SPEC = {
|
||||
modalAnimationSpeed: { def: 1 },
|
||||
modalCustomAnimationDuration: { def: 150 },
|
||||
enableRippleEffects: { def: true },
|
||||
animationVariant: { def: 0 },
|
||||
motionEffect: { def: 0 },
|
||||
m3ElevationEnabled: { def: true },
|
||||
m3ElevationIntensity: { def: 12 },
|
||||
m3ElevationOpacity: { def: 30 },
|
||||
@@ -443,6 +445,7 @@ var SPEC = {
|
||||
displayProfileAutoSelect: { def: false },
|
||||
displayShowDisconnected: { def: false },
|
||||
displaySnapToEdge: { def: true },
|
||||
connectedFrameBarStyleBackups: { def: {} },
|
||||
|
||||
barConfigs: {
|
||||
def: [{
|
||||
@@ -549,7 +552,21 @@ var SPEC = {
|
||||
clipboardEnterToPaste: { def: false },
|
||||
|
||||
launcherPluginVisibility: { def: {} },
|
||||
launcherPluginOrder: { def: [] }
|
||||
launcherPluginOrder: { def: [] },
|
||||
|
||||
frameEnabled: { def: false },
|
||||
frameThickness: { def: 16 },
|
||||
frameRounding: { def: 23 },
|
||||
frameColor: { def: "" },
|
||||
frameOpacity: { def: 1.0 },
|
||||
frameScreenPreferences: { def: ["all"] },
|
||||
frameBarSize: { def: 40 },
|
||||
frameShowOnOverview: { def: false },
|
||||
frameBlurEnabled: { def: true },
|
||||
frameCloseGaps: { def: true },
|
||||
frameLauncherEmergeSide: { def: "bottom" },
|
||||
frameLauncherArcExtender: { def: false },
|
||||
frameMode: { def: "separate" }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -248,6 +248,10 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 6;
|
||||
}
|
||||
|
||||
if (currentVersion < 11) {
|
||||
settings.configVersion = 11;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import qs.Modules.OSD
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankBar.Popouts
|
||||
import qs.Modules.Frame
|
||||
import qs.Modules.WorkspaceOverlays
|
||||
import qs.Services
|
||||
|
||||
@@ -187,6 +188,8 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Frame {}
|
||||
|
||||
Repeater {
|
||||
id: dankBarRepeater
|
||||
model: ScriptModel {
|
||||
@@ -331,7 +334,6 @@ Item {
|
||||
sourceComponent: Component {
|
||||
DankDashPopout {
|
||||
id: dankDashPopout
|
||||
onPopoutClosed: PopoutService.unloadDankDash()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,37 +162,36 @@ Item {
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function resolveTabIndex(tab: string): int {
|
||||
switch ((tab || "").toLowerCase()) {
|
||||
case "media":
|
||||
return 1;
|
||||
case "wallpaper":
|
||||
return 2;
|
||||
case "weather":
|
||||
return SettingsData.weatherEnabled ? 3 : 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function open(tab: string): string {
|
||||
const bar = root.getPreferredBar("clockButtonRef");
|
||||
if (!bar)
|
||||
return "DASH_OPEN_FAILED";
|
||||
|
||||
const tabIndex = resolveTabIndex(tab);
|
||||
const dash = root.dankDashPopoutLoader.item;
|
||||
const onSameScreen = dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name;
|
||||
|
||||
if (!onSameScreen) {
|
||||
bar.triggerWallpaperBrowser();
|
||||
if (dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name) {
|
||||
dash.currentTabIndex = tabIndex;
|
||||
if (dash.updateSurfacePosition)
|
||||
dash.updateSurfacePosition();
|
||||
return "DASH_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
if (!root.dankDashPopoutLoader.item)
|
||||
if (!bar.triggerDashTab(tabIndex))
|
||||
return "DASH_OPEN_FAILED";
|
||||
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
||||
break;
|
||||
case "wallpaper":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 2;
|
||||
break;
|
||||
case "weather":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
|
||||
break;
|
||||
default:
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
root.dankDashPopoutLoader.item.dashVisible = true;
|
||||
return "DASH_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
@@ -212,23 +211,8 @@ Item {
|
||||
|
||||
const bar = root.getPreferredBar("clockButtonRef");
|
||||
if (bar) {
|
||||
bar.triggerWallpaperBrowser();
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
||||
break;
|
||||
case "wallpaper":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 2;
|
||||
break;
|
||||
case "weather":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
|
||||
break;
|
||||
default:
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bar.triggerDashTab(resolveTabIndex(tab)))
|
||||
return "DASH_TOGGLE_FAILED";
|
||||
return "DASH_TOGGLE_SUCCESS";
|
||||
}
|
||||
return "DASH_TOGGLE_FAILED";
|
||||
|
||||
@@ -64,11 +64,19 @@ DankModal {
|
||||
activeImageLoads = 0;
|
||||
shouldHaveFocus = true;
|
||||
ClipboardService.reset();
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
if (clipboardAvailable) {
|
||||
if (Theme.isConnectedEffect) {
|
||||
Qt.callLater(() => {
|
||||
if (clipboardHistoryModal.shouldBeVisible)
|
||||
ClipboardService.refresh();
|
||||
});
|
||||
} else {
|
||||
ClipboardService.refresh();
|
||||
}
|
||||
}
|
||||
if (contentLoader.item?.searchField) {
|
||||
contentLoader.item.searchField.text = "";
|
||||
contentLoader.item.searchField.forceActiveFocus();
|
||||
|
||||
@@ -53,8 +53,6 @@ DankPopout {
|
||||
open();
|
||||
activeImageLoads = 0;
|
||||
ClipboardService.reset();
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
@@ -121,8 +119,16 @@ DankPopout {
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible)
|
||||
return;
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
if (clipboardAvailable) {
|
||||
if (Theme.isConnectedEffect) {
|
||||
Qt.callLater(() => {
|
||||
if (root.shouldBeVisible)
|
||||
ClipboardService.refresh();
|
||||
});
|
||||
} else {
|
||||
ClipboardService.refresh();
|
||||
}
|
||||
}
|
||||
keyboardController.reset();
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item?.searchField) {
|
||||
|
||||
@@ -26,9 +26,7 @@ Rectangle {
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: keyboardHints.enterToPaste
|
||||
? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
|
||||
: I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
|
||||
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankModal")
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
property string layerNamespace: "dms:modal"
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property Component content: null
|
||||
property Item directContent: null
|
||||
property real modalWidth: 400
|
||||
property real modalHeight: 300
|
||||
property var targetScreen
|
||||
readonly property var effectiveScreen: contentWindow.screen ?? targetScreen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
property bool showBackground: true
|
||||
property real backgroundOpacity: 0.5
|
||||
property string positioning: "center"
|
||||
@@ -37,7 +30,6 @@ Item {
|
||||
property real borderWidth: 0
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
property bool enableShadow: true
|
||||
property alias modalFocusScope: focusScope
|
||||
property bool shouldBeVisible: false
|
||||
property bool shouldHaveFocus: shouldBeVisible
|
||||
property bool allowFocusOverride: false
|
||||
@@ -46,452 +38,170 @@ Item {
|
||||
property bool keepPopoutsOpen: false
|
||||
property var customKeyboardFocus: null
|
||||
property bool useOverlayLayer: false
|
||||
readonly property alias contentWindow: contentWindow
|
||||
readonly property alias clickCatcher: clickCatcher
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
signal backgroundClicked
|
||||
|
||||
property bool animationsEnabled: true
|
||||
readonly property var contentLoader: impl.item ? impl.item.contentLoader : null
|
||||
readonly property alias modalFocusScope: _modalFocusScope
|
||||
|
||||
FocusScope {
|
||||
id: _modalFocusScope
|
||||
objectName: "modalFocusScope"
|
||||
focus: true
|
||||
anchors.fill: parent
|
||||
}
|
||||
readonly property var contentWindow: impl.item ? impl.item.contentWindow : null
|
||||
readonly property var clickCatcher: impl.item ? impl.item.clickCatcher : null
|
||||
readonly property var effectiveScreen: impl.item ? impl.item.effectiveScreen : null
|
||||
readonly property real screenWidth: impl.item ? impl.item.screenWidth : 1920
|
||||
readonly property real screenHeight: impl.item ? impl.item.screenHeight : 1080
|
||||
readonly property real dpr: impl.item ? impl.item.dpr : 1
|
||||
readonly property bool isClosing: impl.item ? (impl.item.isClosing ?? false) : false
|
||||
readonly property real alignedX: impl.item ? impl.item.alignedX : 0
|
||||
readonly property real alignedY: impl.item ? impl.item.alignedY : 0
|
||||
readonly property real alignedWidth: impl.item ? impl.item.alignedWidth : 0
|
||||
readonly property real alignedHeight: impl.item ? impl.item.alignedHeight : 0
|
||||
|
||||
function open() {
|
||||
closeTimer.stop();
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
|
||||
if (focusedScreen) {
|
||||
if (screenChanged)
|
||||
contentWindow.visible = false;
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow) {
|
||||
if (screenChanged)
|
||||
clickCatcher.visible = false;
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
}
|
||||
if (screenChanged) {
|
||||
Qt.callLater(() => root._finishOpen());
|
||||
} else {
|
||||
_finishOpen();
|
||||
}
|
||||
}
|
||||
|
||||
function _finishOpen() {
|
||||
ModalManager.openModal(root);
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
if (impl.item)
|
||||
impl.item.open();
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false;
|
||||
shouldHaveFocus = false;
|
||||
ModalManager.closeModal(root);
|
||||
closeTimer.restart();
|
||||
if (impl.item)
|
||||
impl.item.close();
|
||||
}
|
||||
|
||||
function instantClose() {
|
||||
animationsEnabled = false;
|
||||
shouldBeVisible = false;
|
||||
shouldHaveFocus = false;
|
||||
ModalManager.closeModal(root);
|
||||
closeTimer.stop();
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
Qt.callLater(() => animationsEnabled = true);
|
||||
if (impl.item && typeof impl.item.instantClose === "function")
|
||||
impl.item.instantClose();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
shouldBeVisible ? close() : open();
|
||||
if (impl.item)
|
||||
impl.item.toggle();
|
||||
}
|
||||
|
||||
readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
|
||||
property var _resolvedBackend: null
|
||||
|
||||
Component.onCompleted: _resolvedBackend = _desiredBackend
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onConnectedFrameModeActiveChanged() {
|
||||
root._maybeResolveBackend();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer Loader source-component swap until impl is fully closed; avoids
|
||||
// tearing down a modal mid-animation when frame mode is toggled.
|
||||
function _maybeResolveBackend() {
|
||||
if (_resolvedBackend === _desiredBackend)
|
||||
return;
|
||||
if (impl.item && (impl.item.shouldBeVisible || impl.item.isClosing))
|
||||
return;
|
||||
_resolvedBackend = _desiredBackend;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: impl
|
||||
sourceComponent: root._resolvedBackend
|
||||
onItemChanged: if (item)
|
||||
root._wireBackend(item)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: standaloneComp
|
||||
DankModalStandalone {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: connectedComp
|
||||
DankModalConnected {}
|
||||
}
|
||||
|
||||
function _wireBackend(it) {
|
||||
if (!it)
|
||||
return;
|
||||
|
||||
it.modalHandle = root;
|
||||
it.layerNamespace = Qt.binding(() => root.layerNamespace);
|
||||
it.content = Qt.binding(() => root.content);
|
||||
it.directContent = Qt.binding(() => root.directContent);
|
||||
it.modalWidth = Qt.binding(() => root.modalWidth);
|
||||
it.modalHeight = Qt.binding(() => root.modalHeight);
|
||||
it.targetScreen = Qt.binding(() => root.targetScreen);
|
||||
it.showBackground = Qt.binding(() => root.showBackground);
|
||||
it.backgroundOpacity = Qt.binding(() => root.backgroundOpacity);
|
||||
it.positioning = Qt.binding(() => root.positioning);
|
||||
it.customPosition = Qt.binding(() => root.customPosition);
|
||||
it.closeOnEscapeKey = Qt.binding(() => root.closeOnEscapeKey);
|
||||
it.closeOnBackgroundClick = Qt.binding(() => root.closeOnBackgroundClick);
|
||||
it.animationType = Qt.binding(() => root.animationType);
|
||||
it.animationDuration = Qt.binding(() => root.animationDuration);
|
||||
it.animationScaleCollapsed = Qt.binding(() => root.animationScaleCollapsed);
|
||||
it.animationOffset = Qt.binding(() => root.animationOffset);
|
||||
it.animationEnterCurve = Qt.binding(() => root.animationEnterCurve);
|
||||
it.animationExitCurve = Qt.binding(() => root.animationExitCurve);
|
||||
it.backgroundColor = Qt.binding(() => root.backgroundColor);
|
||||
it.borderColor = Qt.binding(() => root.borderColor);
|
||||
it.borderWidth = Qt.binding(() => root.borderWidth);
|
||||
it.cornerRadius = Qt.binding(() => root.cornerRadius);
|
||||
it.enableShadow = Qt.binding(() => root.enableShadow);
|
||||
it.allowFocusOverride = Qt.binding(() => root.allowFocusOverride);
|
||||
it.allowStacking = Qt.binding(() => root.allowStacking);
|
||||
it.keepContentLoaded = Qt.binding(() => root.keepContentLoaded);
|
||||
it.keepPopoutsOpen = Qt.binding(() => root.keepPopoutsOpen);
|
||||
it.customKeyboardFocus = Qt.binding(() => root.customKeyboardFocus);
|
||||
it.useOverlayLayer = Qt.binding(() => root.useOverlayLayer);
|
||||
|
||||
it.shouldBeVisible = root.shouldBeVisible;
|
||||
it.shouldHaveFocus = root.shouldHaveFocus;
|
||||
|
||||
if (it.modalFocusScope)
|
||||
_modalFocusScope.parent = it.modalFocusScope;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== root && !allowStacking && shouldBeVisible)
|
||||
close();
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (impl.item && impl.item.shouldBeVisible !== root.shouldBeVisible)
|
||||
impl.item.shouldBeVisible = root.shouldBeVisible;
|
||||
}
|
||||
function onShouldHaveFocusChanged() {
|
||||
if (impl.item && impl.item.shouldHaveFocus !== root.shouldHaveFocus)
|
||||
impl.item.shouldHaveFocus = root.shouldHaveFocus;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (!contentWindow.screen)
|
||||
return;
|
||||
const currentScreenName = contentWindow.screen.name;
|
||||
let screenStillExists = false;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === currentScreenName) {
|
||||
screenStillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (screenStillExists)
|
||||
return;
|
||||
const newScreen = CompositorService.getFocusedScreen();
|
||||
if (newScreen) {
|
||||
contentWindow.screen = newScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = newScreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
target: impl.item
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration + 50
|
||||
onTriggered: {
|
||||
if (shouldBeVisible)
|
||||
return;
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
|
||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
switch (positioning) {
|
||||
case "center":
|
||||
return (screenWidth - alignedWidth) / 2;
|
||||
case "top-right":
|
||||
return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL);
|
||||
case "custom":
|
||||
return customPosition.x;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
readonly property real alignedY: Theme.snap((() => {
|
||||
switch (positioning) {
|
||||
case "center":
|
||||
return (screenHeight - alignedHeight) / 2;
|
||||
case "top-right":
|
||||
return Theme.barHeight + Theme.spacingXS;
|
||||
case "custom":
|
||||
return customPosition.y;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
PanelWindow {
|
||||
id: clickCatcher
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (impl.item && root.shouldBeVisible !== impl.item.shouldBeVisible)
|
||||
root.shouldBeVisible = impl.item.shouldBeVisible;
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: Rectangle {
|
||||
x: root.alignedX
|
||||
y: root.alignedY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
intersection: Intersection.Xor
|
||||
function onShouldHaveFocusChanged() {
|
||||
if (impl.item && root.shouldHaveFocus !== impl.item.shouldHaveFocus)
|
||||
root.shouldHaveFocus = impl.item.shouldHaveFocus;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
|
||||
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
function onOpened() {
|
||||
root.opened();
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace
|
||||
WlrLayershell.layer: {
|
||||
if (root.useOverlayLayer)
|
||||
return WlrLayershell.Overlay;
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (customKeyboardFocus !== null)
|
||||
return customKeyboardFocus;
|
||||
if (!shouldHaveFocus)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (root.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
function onDialogClosed() {
|
||||
root.dialogClosed();
|
||||
root._maybeResolveBackend();
|
||||
}
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: root.useSingleWindow
|
||||
bottom: root.useSingleWindow
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
|
||||
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
|
||||
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
opened();
|
||||
} else {
|
||||
if (Qt.inputMethod) {
|
||||
Qt.inputMethod.hide();
|
||||
Qt.inputMethod.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
z: -2
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.useBackground
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
||||
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
||||
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.shouldBeVisible
|
||||
hoverEnabled: false
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: mouse.accepted = true
|
||||
onClicked: mouse.accepted = true
|
||||
z: -1
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
|
||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
|
||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: animatedContent
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
|
||||
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: modalShadowLayer
|
||||
anchors.fill: parent
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: root.cornerRadius
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !BlurService.enabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper;
|
||||
root.directContent.anchors.fill = directContentWrapper;
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDirectContentChanged() {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper;
|
||||
root.directContent.anchors.fill = directContentWrapper;
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
objectName: "modalFocusScope"
|
||||
anchors.fill: parent
|
||||
visible: root.shouldBeVisible || contentWindow.visible
|
||||
focus: root.shouldBeVisible
|
||||
Keys.onEscapePressed: event => {
|
||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||
root.close();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
function onBackgroundClicked() {
|
||||
root.backgroundClicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
838
quickshell/Modals/Common/DankModalConnected.qml
Normal file
838
quickshell/Modals/Common/DankModalConnected.qml
Normal file
@@ -0,0 +1,838 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankModalConnected")
|
||||
|
||||
property var modalHandle: root
|
||||
property string layerNamespace: "dms:modal"
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property Item directContent: null
|
||||
property real modalWidth: 400
|
||||
property real modalHeight: 300
|
||||
property var targetScreen
|
||||
readonly property var effectiveScreen: contentWindow.screen ?? targetScreen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
property bool showBackground: true
|
||||
property real backgroundOpacity: 0.5
|
||||
property string positioning: "center"
|
||||
property point customPosition: Qt.point(0, 0)
|
||||
property bool closeOnEscapeKey: true
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
|
||||
// Opposite side from the launcher by default; subclasses may override
|
||||
property string preferredConnectedBarSide: SettingsData.frameModalEmergeSide
|
||||
|
||||
readonly property bool frameConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
|
||||
|
||||
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
|
||||
|
||||
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== ""
|
||||
|
||||
function _dockOccupiesSide(side) {
|
||||
if (!SettingsData.showDock)
|
||||
return false;
|
||||
switch (side) {
|
||||
case "top":
|
||||
return SettingsData.dockPosition === SettingsData.Position.Top;
|
||||
case "bottom":
|
||||
return SettingsData.dockPosition === SettingsData.Position.Bottom;
|
||||
case "left":
|
||||
return SettingsData.dockPosition === SettingsData.Position.Left;
|
||||
case "right":
|
||||
return SettingsData.dockPosition === SettingsData.Position.Right;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
|
||||
|
||||
readonly property bool connectedMotionParity: Theme.isConnectedEffect
|
||||
property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
|
||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||
property real animationOffset: Theme.effectAnimOffset
|
||||
property list<real> animationEnterCurve: connectedMotionParity ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
|
||||
property list<real> animationExitCurve: connectedMotionParity ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 0
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
|
||||
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
|
||||
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
|
||||
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
|
||||
readonly property real effectiveCornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : cornerRadius
|
||||
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
property bool enableShadow: true
|
||||
property alias modalFocusScope: focusScope
|
||||
property bool shouldBeVisible: false
|
||||
property bool shouldHaveFocus: shouldBeVisible
|
||||
property bool allowFocusOverride: false
|
||||
property bool allowStacking: false
|
||||
property bool keepContentLoaded: false
|
||||
property bool keepPopoutsOpen: false
|
||||
property var customKeyboardFocus: null
|
||||
property bool useOverlayLayer: false
|
||||
property real frozenMotionOffsetX: 0
|
||||
property real frozenMotionOffsetY: 0
|
||||
readonly property alias contentWindow: contentWindow
|
||||
readonly property alias clickCatcher: clickCatcher
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property bool useBackground: false
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
signal backgroundClicked
|
||||
|
||||
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
|
||||
Timer {
|
||||
id: _syncTimer
|
||||
interval: 0
|
||||
onTriggered: root._flushSync()
|
||||
}
|
||||
|
||||
property bool animationsEnabled: true
|
||||
|
||||
property string _chromeClaimId: ""
|
||||
property bool _fullSyncPending: false
|
||||
|
||||
function _nextChromeClaimId() {
|
||||
return layerNamespace + ":modal:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000);
|
||||
}
|
||||
|
||||
function _currentScreenName() {
|
||||
return effectiveScreen ? effectiveScreen.name : "";
|
||||
}
|
||||
|
||||
function _publishModalChromeState() {
|
||||
const screenName = _currentScreenName();
|
||||
if (!screenName)
|
||||
return;
|
||||
ConnectedModeState.setModalState(screenName, {
|
||||
"visible": shouldBeVisible || contentWindow.visible,
|
||||
"barSide": resolvedConnectedBarSide,
|
||||
"bodyX": alignedX,
|
||||
"bodyY": alignedY,
|
||||
"bodyW": alignedWidth,
|
||||
"bodyH": alignedHeight,
|
||||
"animX": modalContainer ? modalContainer.animX : 0,
|
||||
"animY": modalContainer ? modalContainer.animY : 0,
|
||||
"omitStartConnector": false,
|
||||
"omitEndConnector": false
|
||||
});
|
||||
}
|
||||
|
||||
function _syncModalChromeState() {
|
||||
if (!frameOwnsConnectedChrome) {
|
||||
_releaseModalChrome();
|
||||
return;
|
||||
}
|
||||
if (!_chromeClaimId)
|
||||
_chromeClaimId = _nextChromeClaimId();
|
||||
_publishModalChromeState();
|
||||
if (_dockBlocksEmergence && (shouldBeVisible || contentWindow.visible))
|
||||
ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide);
|
||||
else
|
||||
ConnectedModeState.releaseDockRetract(_chromeClaimId);
|
||||
}
|
||||
|
||||
property bool _animSyncQueued: false
|
||||
property bool _bodySyncQueued: false
|
||||
|
||||
function _queueFullSync() {
|
||||
_fullSyncPending = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _queueAnimSync() {
|
||||
_animSyncQueued = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _queueBodySync() {
|
||||
_bodySyncQueued = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _flushSync() {
|
||||
const fullDirty = _fullSyncPending;
|
||||
const animDirty = _animSyncQueued;
|
||||
const bodyDirty = _bodySyncQueued;
|
||||
_fullSyncPending = false;
|
||||
_animSyncQueued = false;
|
||||
_bodySyncQueued = false;
|
||||
if (fullDirty)
|
||||
_syncModalChromeState();
|
||||
if (animDirty)
|
||||
_syncModalAnim();
|
||||
if (bodyDirty)
|
||||
_syncModalBody();
|
||||
}
|
||||
|
||||
function _syncModalAnim() {
|
||||
if (!frameOwnsConnectedChrome || !_chromeClaimId)
|
||||
return;
|
||||
const screenName = _currentScreenName();
|
||||
if (!screenName || !modalContainer)
|
||||
return;
|
||||
ConnectedModeState.setModalAnim(screenName, modalContainer.animX, modalContainer.animY);
|
||||
}
|
||||
|
||||
function _syncModalBody() {
|
||||
if (!frameOwnsConnectedChrome || !_chromeClaimId)
|
||||
return;
|
||||
const screenName = _currentScreenName();
|
||||
if (!screenName)
|
||||
return;
|
||||
ConnectedModeState.setModalBody(screenName, alignedX, alignedY, alignedWidth, alignedHeight);
|
||||
}
|
||||
|
||||
function _releaseModalChrome() {
|
||||
if (_chromeClaimId) {
|
||||
ConnectedModeState.releaseDockRetract(_chromeClaimId);
|
||||
_chromeClaimId = "";
|
||||
}
|
||||
const screenName = _currentScreenName();
|
||||
if (screenName)
|
||||
ConnectedModeState.clearModalState(screenName);
|
||||
}
|
||||
|
||||
onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
|
||||
onResolvedConnectedBarSideChanged: _queueFullSync()
|
||||
onShouldBeVisibleChanged: _queueFullSync()
|
||||
onAlignedXChanged: _queueBodySync()
|
||||
onAlignedYChanged: _queueBodySync()
|
||||
onAlignedWidthChanged: _queueBodySync()
|
||||
onAlignedHeightChanged: _queueBodySync()
|
||||
|
||||
Component.onDestruction: _releaseModalChrome()
|
||||
|
||||
Connections {
|
||||
target: contentWindow
|
||||
function onVisibleChanged() {
|
||||
if (contentWindow.visible)
|
||||
root._syncModalChromeState();
|
||||
else
|
||||
root._releaseModalChrome();
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
closeTimer.stop();
|
||||
animationsEnabled = false;
|
||||
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
|
||||
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
|
||||
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
|
||||
if (Theme.isDirectionalEffect || root.useBackground) {
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
}
|
||||
ModalManager.openModal(modalHandle);
|
||||
|
||||
Qt.callLater(() => {
|
||||
animationsEnabled = true;
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow && !clickCatcher.visible)
|
||||
clickCatcher.visible = true;
|
||||
if (!contentWindow.visible)
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false;
|
||||
shouldHaveFocus = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
closeTimer.restart();
|
||||
}
|
||||
|
||||
function instantClose() {
|
||||
animationsEnabled = false;
|
||||
shouldBeVisible = false;
|
||||
shouldHaveFocus = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
closeTimer.stop();
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
Qt.callLater(() => animationsEnabled = true);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
shouldBeVisible ? close() : open();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== modalHandle && !allowStacking && shouldBeVisible)
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (!contentWindow.screen)
|
||||
return;
|
||||
const currentScreenName = contentWindow.screen.name;
|
||||
let screenStillExists = false;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === currentScreenName) {
|
||||
screenStillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (screenStillExists)
|
||||
return;
|
||||
const newScreen = CompositorService.getFocusedScreen();
|
||||
if (newScreen) {
|
||||
contentWindow.screen = newScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = newScreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: Theme.variantCloseInterval(animationDuration)
|
||||
onTriggered: {
|
||||
if (shouldBeVisible)
|
||||
return;
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
// shadowRenderPadding is zeroed when frame owns the chrome
|
||||
// Wayland then clips any content translating past
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowMotionPadding: {
|
||||
if (Theme.isConnectedEffect)
|
||||
return 0;
|
||||
if (animationType === "slide")
|
||||
return 30;
|
||||
if (Theme.isDirectionalEffect)
|
||||
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
|
||||
if (Theme.isDepthEffect)
|
||||
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
|
||||
return Math.max(0, animationOffset);
|
||||
}
|
||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
|
||||
function _frameEdgeInset(side) {
|
||||
if (!effectiveScreen)
|
||||
return 0;
|
||||
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
|
||||
}
|
||||
|
||||
// frameEdgeInsetForSide is the full inset; do not add frameBarSize
|
||||
readonly property real _connectedAlignedX: {
|
||||
switch (resolvedConnectedBarSide) {
|
||||
case "top":
|
||||
case "bottom":
|
||||
{
|
||||
const insetL = _frameEdgeInset("left");
|
||||
const insetR = _frameEdgeInset("right");
|
||||
const usable = Math.max(0, screenWidth - insetL - insetR);
|
||||
return insetL + Math.max(0, (usable - alignedWidth) / 2);
|
||||
}
|
||||
case "left":
|
||||
return _frameEdgeInset("left");
|
||||
case "right":
|
||||
return screenWidth - alignedWidth - _frameEdgeInset("right");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
readonly property real _connectedAlignedY: {
|
||||
switch (resolvedConnectedBarSide) {
|
||||
case "top":
|
||||
return _frameEdgeInset("top");
|
||||
case "bottom":
|
||||
return screenHeight - alignedHeight - _frameEdgeInset("bottom");
|
||||
case "left":
|
||||
case "right":
|
||||
{
|
||||
const insetT = _frameEdgeInset("top");
|
||||
const insetB = _frameEdgeInset("bottom");
|
||||
const usable = Math.max(0, screenHeight - insetT - insetB);
|
||||
return insetT + Math.max(0, (usable - alignedHeight) / 2);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
readonly property real alignedX: Theme.snap(frameOwnsConnectedChrome ? _connectedAlignedX : (() => {
|
||||
switch (positioning) {
|
||||
case "center":
|
||||
return (screenWidth - alignedWidth) / 2;
|
||||
case "top-right":
|
||||
return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL);
|
||||
case "custom":
|
||||
return customPosition.x;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
readonly property real alignedY: Theme.snap(frameOwnsConnectedChrome ? _connectedAlignedY : (() => {
|
||||
switch (positioning) {
|
||||
case "center":
|
||||
return (screenHeight - alignedHeight) / 2;
|
||||
case "top-right":
|
||||
return Theme.barHeight + Theme.spacingXS;
|
||||
case "custom":
|
||||
return customPosition.y;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
PanelWindow {
|
||||
id: clickCatcher
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: Rectangle {
|
||||
x: root.alignedX
|
||||
y: root.alignedY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
intersection: Intersection.Xor
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome
|
||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0
|
||||
blurHeight: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.height * s : 0
|
||||
blurRadius: root.effectiveCornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace
|
||||
WlrLayershell.layer: {
|
||||
if (root.useOverlayLayer)
|
||||
return WlrLayershell.Overlay;
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (customKeyboardFocus !== null)
|
||||
return customKeyboardFocus;
|
||||
if (!shouldHaveFocus)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (root.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: root.useSingleWindow
|
||||
bottom: root.useSingleWindow
|
||||
}
|
||||
|
||||
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: actualMarginLeft
|
||||
top: actualMarginTop
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
|
||||
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
|
||||
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
opened();
|
||||
} else {
|
||||
if (Qt.inputMethod) {
|
||||
Qt.inputMethod.hide();
|
||||
Qt.inputMethod.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
z: -2
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
|
||||
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
|
||||
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.shouldBeVisible
|
||||
hoverEnabled: false
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: mouse.accepted = true
|
||||
onClicked: mouse.accepted = true
|
||||
z: -1
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
|
||||
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
|
||||
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
|
||||
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
|
||||
readonly property real customDistLeft: customAnchorX
|
||||
readonly property real customDistRight: root.screenWidth - customAnchorX
|
||||
readonly property real customDistTop: customAnchorY
|
||||
readonly property real customDistBottom: root.screenHeight - customAnchorY
|
||||
// Connected emergence: travel from the resolved bar edge, matching DankPopout cadence.
|
||||
readonly property real connectedEmergenceTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
|
||||
readonly property real connectedEmergenceTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
|
||||
readonly property real offsetX: {
|
||||
if (root.frameOwnsConnectedChrome) {
|
||||
switch (root.resolvedConnectedBarSide) {
|
||||
case "left":
|
||||
return -connectedEmergenceTravelX;
|
||||
case "right":
|
||||
return connectedEmergenceTravelX;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (slide && !directionalEffect && !depthEffect)
|
||||
return 15;
|
||||
if (directionalEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return 0;
|
||||
case "custom":
|
||||
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
|
||||
return -directionalTravel;
|
||||
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
|
||||
return directionalTravel;
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (depthEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return 0;
|
||||
case "custom":
|
||||
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
|
||||
return -depthTravel;
|
||||
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
|
||||
return depthTravel;
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
readonly property real offsetY: {
|
||||
if (root.frameOwnsConnectedChrome) {
|
||||
switch (root.resolvedConnectedBarSide) {
|
||||
case "top":
|
||||
return -connectedEmergenceTravelY;
|
||||
case "bottom":
|
||||
return connectedEmergenceTravelY;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (slide && !directionalEffect && !depthEffect)
|
||||
return -30;
|
||||
if (directionalEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return -Math.max(directionalTravel * 0.65, 96);
|
||||
case "custom":
|
||||
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
|
||||
return -directionalTravel;
|
||||
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
|
||||
return directionalTravel;
|
||||
return 0;
|
||||
default:
|
||||
// Default to sliding down from top when centered
|
||||
return -Math.max(directionalTravel, root.screenHeight * 0.24);
|
||||
}
|
||||
}
|
||||
if (depthEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return -depthTravel * 0.75;
|
||||
case "custom":
|
||||
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
|
||||
return -depthTravel;
|
||||
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
|
||||
return depthTravel;
|
||||
return depthTravel * 0.45;
|
||||
default:
|
||||
return -depthTravel;
|
||||
}
|
||||
}
|
||||
return root.animationOffset;
|
||||
}
|
||||
|
||||
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
|
||||
|
||||
// openProgress: 0 = closed (at frozenMotionOffset, scaleCollapsed), 1 = open (at 0, scale 1).
|
||||
QtObject {
|
||||
id: morph
|
||||
property real openProgress: root.shouldBeVisible ? 1 : 0
|
||||
Behavior on openProgress {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real animX: root.frozenMotionOffsetX * (1 - morph.openProgress)
|
||||
readonly property real animY: root.frozenMotionOffsetY * (1 - morph.openProgress)
|
||||
readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress
|
||||
|
||||
onAnimXChanged: if (root.frameOwnsConnectedChrome)
|
||||
root._queueAnimSync()
|
||||
onAnimYChanged: if (root.frameOwnsConnectedChrome)
|
||||
root._queueAnimSync()
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: animatedContent
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
|
||||
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||
|
||||
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||
scale: modalContainer.scaleValue
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on publishedOpacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: modalShadowLayer
|
||||
anchors.fill: parent
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: root.effectiveCornerRadius
|
||||
targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBackgroundColor
|
||||
borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor
|
||||
borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth
|
||||
shadowEnabled: !root.frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.effectiveCornerRadius
|
||||
color: "transparent"
|
||||
border.color: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? "transparent" : BlurService.borderColor
|
||||
border.width: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? 0 : BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper;
|
||||
root.directContent.anchors.fill = directContentWrapper;
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDirectContentChanged() {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper;
|
||||
root.directContent.anchors.fill = directContentWrapper;
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
objectName: "modalFocusScope"
|
||||
anchors.fill: parent
|
||||
visible: root.shouldBeVisible || contentWindow.visible
|
||||
focus: root.shouldBeVisible
|
||||
Keys.onEscapePressed: event => {
|
||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||
root.close();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
484
quickshell/Modals/Common/DankModalStandalone.qml
Normal file
484
quickshell/Modals/Common/DankModalStandalone.qml
Normal file
@@ -0,0 +1,484 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankModalStandalone")
|
||||
|
||||
property var modalHandle: root
|
||||
property string layerNamespace: "dms:modal"
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property Item directContent: null
|
||||
property real modalWidth: 400
|
||||
property real modalHeight: 300
|
||||
property var targetScreen
|
||||
readonly property var effectiveScreen: contentWindow.screen ?? targetScreen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
property bool showBackground: true
|
||||
property real backgroundOpacity: 0.5
|
||||
property string positioning: "center"
|
||||
property point customPosition: Qt.point(0, 0)
|
||||
property bool closeOnEscapeKey: true
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
property int animationDuration: Theme.modalAnimationDuration
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 0
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
property bool enableShadow: true
|
||||
property alias modalFocusScope: focusScope
|
||||
property bool shouldBeVisible: false
|
||||
property bool isClosing: false
|
||||
property bool shouldHaveFocus: shouldBeVisible
|
||||
property bool allowFocusOverride: false
|
||||
property bool allowStacking: false
|
||||
property bool keepContentLoaded: false
|
||||
property bool keepPopoutsOpen: false
|
||||
property var customKeyboardFocus: null
|
||||
property bool useOverlayLayer: false
|
||||
readonly property alias contentWindow: contentWindow
|
||||
readonly property alias clickCatcher: clickCatcher
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
signal backgroundClicked
|
||||
|
||||
property bool animationsEnabled: true
|
||||
|
||||
function open() {
|
||||
closeTimer.stop();
|
||||
isClosing = false;
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
|
||||
if (focusedScreen) {
|
||||
if (screenChanged)
|
||||
contentWindow.visible = false;
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow) {
|
||||
if (screenChanged)
|
||||
clickCatcher.visible = false;
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
}
|
||||
if (screenChanged) {
|
||||
Qt.callLater(() => root._finishOpen());
|
||||
} else {
|
||||
_finishOpen();
|
||||
}
|
||||
}
|
||||
|
||||
function _finishOpen() {
|
||||
ModalManager.openModal(modalHandle);
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
}
|
||||
|
||||
function close() {
|
||||
isClosing = true;
|
||||
shouldBeVisible = false;
|
||||
shouldHaveFocus = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
closeTimer.restart();
|
||||
}
|
||||
|
||||
function instantClose() {
|
||||
animationsEnabled = false;
|
||||
isClosing = false;
|
||||
shouldBeVisible = false;
|
||||
shouldHaveFocus = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
closeTimer.stop();
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
Qt.callLater(() => animationsEnabled = true);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
shouldBeVisible ? close() : open();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== modalHandle && !allowStacking && shouldBeVisible)
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (!contentWindow.screen)
|
||||
return;
|
||||
const currentScreenName = contentWindow.screen.name;
|
||||
let screenStillExists = false;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === currentScreenName) {
|
||||
screenStillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (screenStillExists)
|
||||
return;
|
||||
const newScreen = CompositorService.getFocusedScreen();
|
||||
if (newScreen) {
|
||||
contentWindow.screen = newScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = newScreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration + 50
|
||||
onTriggered: {
|
||||
if (shouldBeVisible)
|
||||
return;
|
||||
isClosing = false;
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
|
||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
switch (positioning) {
|
||||
case "center":
|
||||
return (screenWidth - alignedWidth) / 2;
|
||||
case "top-right":
|
||||
return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL);
|
||||
case "custom":
|
||||
return customPosition.x;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
readonly property real alignedY: Theme.snap((() => {
|
||||
switch (positioning) {
|
||||
case "center":
|
||||
return (screenHeight - alignedHeight) / 2;
|
||||
case "top-right":
|
||||
return Theme.barHeight + Theme.spacingXS;
|
||||
case "custom":
|
||||
return customPosition.y;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
PanelWindow {
|
||||
id: clickCatcher
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: Rectangle {
|
||||
x: root.alignedX
|
||||
y: root.alignedY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
intersection: Intersection.Xor
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: shouldBeVisible ? modalContainer.width * s : 0
|
||||
blurHeight: shouldBeVisible ? modalContainer.height * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace
|
||||
WlrLayershell.layer: {
|
||||
if (root.useOverlayLayer)
|
||||
return WlrLayershell.Overlay;
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (customKeyboardFocus !== null)
|
||||
return customKeyboardFocus;
|
||||
if (!shouldHaveFocus)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (root.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: root.useSingleWindow
|
||||
bottom: root.useSingleWindow
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
|
||||
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
|
||||
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
opened();
|
||||
} else {
|
||||
if (Qt.inputMethod) {
|
||||
Qt.inputMethod.hide();
|
||||
Qt.inputMethod.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
z: -2
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.useBackground
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: root.animationDuration
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
||||
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
||||
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.shouldBeVisible
|
||||
hoverEnabled: false
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: mouse.accepted = true
|
||||
onClicked: mouse.accepted = true
|
||||
z: -1
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
|
||||
QtObject {
|
||||
id: morph
|
||||
property real openProgress: root.shouldBeVisible ? 1 : 0
|
||||
Behavior on openProgress {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real animX: modalContainer.offsetX * (1 - morph.openProgress)
|
||||
readonly property real animY: modalContainer.offsetY * (1 - morph.openProgress)
|
||||
readonly property real scaleValue: root.animationScaleCollapsed + (1.0 - root.animationScaleCollapsed) * morph.openProgress
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: animatedContent
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
|
||||
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: modalShadowLayer
|
||||
anchors.fill: parent
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: root.cornerRadius
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper;
|
||||
root.directContent.anchors.fill = directContentWrapper;
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDirectContentChanged() {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper;
|
||||
root.directContent.anchors.fill = directContentWrapper;
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
objectName: "modalFocusScope"
|
||||
anchors.fill: parent
|
||||
visible: root.shouldBeVisible || contentWindow.visible
|
||||
focus: root.shouldBeVisible
|
||||
Keys.onEscapePressed: event => {
|
||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||
root.close();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,466 +1,118 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankLauncherV2Modal")
|
||||
|
||||
visible: false
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property bool keyboardActive: false
|
||||
property bool contentVisible: false
|
||||
property var spotlightContent: launcherContentLoader.item
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
property bool _pendingInitialize: false
|
||||
property string _pendingQuery: ""
|
||||
property string _pendingMode: ""
|
||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property var effectiveScreen: launcherWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
|
||||
readonly property int baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 500;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 620;
|
||||
}
|
||||
}
|
||||
readonly property int baseHeight: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 480;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 600;
|
||||
}
|
||||
}
|
||||
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
|
||||
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
|
||||
readonly property real modalX: (screenWidth - modalWidth) / 2
|
||||
readonly property real modalY: (screenHeight - modalHeight) / 2
|
||||
|
||||
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property real cornerRadius: Theme.cornerRadius
|
||||
readonly property color borderColor: {
|
||||
if (!SettingsData.dankLauncherV2BorderEnabled)
|
||||
return Theme.outlineMedium;
|
||||
switch (SettingsData.dankLauncherV2BorderColor) {
|
||||
case "primary":
|
||||
return Theme.primary;
|
||||
case "secondary":
|
||||
return Theme.secondary;
|
||||
case "outline":
|
||||
return Theme.outline;
|
||||
case "surfaceText":
|
||||
return Theme.surfaceText;
|
||||
default:
|
||||
return Theme.primary;
|
||||
}
|
||||
}
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||
readonly property bool spotlightOpen: impl.item ? impl.item.spotlightOpen : false
|
||||
readonly property bool isClosing: impl.item ? impl.item.isClosing : false
|
||||
readonly property bool keyboardActive: impl.item ? impl.item.keyboardActive : false
|
||||
readonly property bool contentVisible: impl.item ? impl.item.contentVisible : false
|
||||
readonly property var spotlightContent: impl.item ? impl.item.spotlightContent : null
|
||||
readonly property bool openedFromOverview: impl.item ? impl.item.openedFromOverview : false
|
||||
readonly property var effectiveScreen: impl.item ? impl.item.effectiveScreen : null
|
||||
readonly property real screenWidth: impl.item ? impl.item.screenWidth : 1920
|
||||
readonly property real screenHeight: impl.item ? impl.item.screenHeight : 1080
|
||||
readonly property real dpr: impl.item ? impl.item.dpr : 1
|
||||
readonly property int modalWidth: impl.item ? impl.item.modalWidth : 620
|
||||
readonly property int modalHeight: impl.item ? impl.item.modalHeight : 600
|
||||
readonly property real modalX: impl.item ? impl.item.modalX : 0
|
||||
readonly property real modalY: impl.item ? impl.item.modalY : 0
|
||||
readonly property bool frameOwnsConnectedChrome: impl.item ? (impl.item.frameOwnsConnectedChrome ?? false) : false
|
||||
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
|
||||
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||
_pendingQuery = query || "";
|
||||
_pendingMode = mode || "";
|
||||
_pendingInitialize = true;
|
||||
contentVisible = true;
|
||||
launcherContentLoader.active = true;
|
||||
|
||||
if (spotlightContent) {
|
||||
_initializeAndShow(_pendingQuery, _pendingMode);
|
||||
_pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
|
||||
function _initializeAndShow(query, mode) {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
contentVisible = true;
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
|
||||
var targetQuery = "";
|
||||
|
||||
if (query) {
|
||||
targetQuery = query;
|
||||
} else if (SettingsData.rememberLastQuery) {
|
||||
targetQuery = SessionData.launcherLastQuery || "";
|
||||
}
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = targetQuery;
|
||||
}
|
||||
if (spotlightContent.controller) {
|
||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
||||
spotlightContent.controller.searchMode = targetMode;
|
||||
spotlightContent.controller.activePluginId = "";
|
||||
spotlightContent.controller.activePluginName = "";
|
||||
spotlightContent.controller.pluginFilter = "";
|
||||
spotlightContent.controller.fileSearchType = "all";
|
||||
spotlightContent.controller.fileSearchExt = "";
|
||||
spotlightContent.controller.fileSearchFolder = "";
|
||||
spotlightContent.controller.fileSearchSort = "score";
|
||||
spotlightContent.controller.collapsedSections = {};
|
||||
spotlightContent.controller.selectedFlatIndex = 0;
|
||||
spotlightContent.controller.selectedItem = null;
|
||||
spotlightContent.controller.historyIndex = -1;
|
||||
spotlightContent.controller.searchQuery = targetQuery;
|
||||
|
||||
spotlightContent.controller.performSearch();
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
}
|
||||
if (spotlightContent.actionPanel) {
|
||||
spotlightContent.actionPanel.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function _finishShow(query, mode) {
|
||||
spotlightOpen = true;
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
}
|
||||
|
||||
function show() {
|
||||
closeCleanupTimer.stop();
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow("", ""));
|
||||
return;
|
||||
}
|
||||
|
||||
_finishShow("", "");
|
||||
if (impl.item)
|
||||
impl.item.show();
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
closeCleanupTimer.stop();
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow(query, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
_finishShow(query, "");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!spotlightOpen)
|
||||
return;
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
contentVisible = false;
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(root);
|
||||
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
spotlightOpen ? hide() : show();
|
||||
if (impl.item)
|
||||
impl.item.showWithQuery(query);
|
||||
}
|
||||
|
||||
function showWithMode(mode) {
|
||||
closeCleanupTimer.stop();
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow("", mode));
|
||||
return;
|
||||
}
|
||||
|
||||
spotlightOpen = true;
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize("", mode);
|
||||
if (impl.item)
|
||||
impl.item.showWithMode(mode);
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
} else {
|
||||
showWithMode(mode);
|
||||
}
|
||||
function hide() {
|
||||
if (impl.item)
|
||||
impl.item.hide();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (impl.item)
|
||||
impl.item.toggle();
|
||||
}
|
||||
|
||||
function toggleWithQuery(query) {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
} else {
|
||||
showWithQuery(query);
|
||||
if (impl.item)
|
||||
impl.item.toggleWithQuery(query);
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
if (impl.item)
|
||||
impl.item.toggleWithMode(mode);
|
||||
}
|
||||
|
||||
readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
|
||||
property var _resolvedBackend: null
|
||||
|
||||
Component.onCompleted: _resolvedBackend = _desiredBackend
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onConnectedFrameModeActiveChanged() {
|
||||
root._maybeResolveBackend();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeCleanupTimer
|
||||
interval: Theme.modalAnimationDuration + 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isClosing = false;
|
||||
if (root.unloadContentOnClose)
|
||||
launcherContentLoader.active = false;
|
||||
dialogClosed();
|
||||
}
|
||||
// Defer Loader source-component swap until impl is fully closed; avoids
|
||||
// tearing down the launcher mid-animation when frame mode is toggled.
|
||||
function _maybeResolveBackend() {
|
||||
if (_resolvedBackend === _desiredBackend)
|
||||
return;
|
||||
if (impl.item && (impl.item.spotlightOpen || impl.item.isClosing))
|
||||
return;
|
||||
_resolvedBackend = _desiredBackend;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: impl
|
||||
sourceComponent: root._resolvedBackend
|
||||
onItemChanged: if (item)
|
||||
root._wireBackend(item)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: standaloneComp
|
||||
DankLauncherV2ModalStandalone {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: connectedComp
|
||||
DankLauncherV2ModalConnected {}
|
||||
}
|
||||
|
||||
function _wireBackend(it) {
|
||||
if (!it)
|
||||
return;
|
||||
it.modalHandle = root;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: spotlightContent?.controller ?? null
|
||||
target: impl.item
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onModeChanged(mode) {
|
||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
||||
return;
|
||||
SessionData.setLauncherLastMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
active: false
|
||||
|
||||
onCleared: {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== root && spotlightOpen) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (Quickshell.screens.length === 0)
|
||||
return;
|
||||
|
||||
const screenName = launcherWindow.screen?.name;
|
||||
if (screenName) {
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === screenName)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (spotlightOpen)
|
||||
hide();
|
||||
|
||||
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||
if (newScreen)
|
||||
launcherWindow.screen = newScreen;
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: spotlightOpen || isClosing
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: launcherWindow
|
||||
readonly property real s: Math.min(1, modalContainer.scale)
|
||||
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
|
||||
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
|
||||
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
|
||||
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
log.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
log.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: spotlightOpen ? fullScreenMask : null
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fullScreenMask
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: backgroundDarken
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: contentVisible || opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: mouse => {
|
||||
var contentX = modalContainer.x;
|
||||
var contentY = modalContainer.y;
|
||||
var contentW = modalContainer.width;
|
||||
var contentH = modalContainer.height;
|
||||
|
||||
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.modalX
|
||||
y: root.modalY
|
||||
width: root.modalWidth
|
||||
height: root.modalHeight
|
||||
visible: contentVisible || opacity > 0
|
||||
|
||||
opacity: contentVisible ? 1 : 0
|
||||
scale: contentVisible ? 1 : 0.96
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
anchors.fill: parent
|
||||
level: Theme.elevationLevel3
|
||||
fallbackOffset: 6
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
targetRadius: root.cornerRadius
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !BlurService.enabled
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
}
|
||||
function onDialogClosed() {
|
||||
root.dialogClosed();
|
||||
root._maybeResolveBackend();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,922 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankLauncherV2ModalConnected")
|
||||
|
||||
property var modalHandle: root
|
||||
|
||||
visible: false
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property bool keyboardActive: false
|
||||
property bool contentVisible: false
|
||||
readonly property bool launcherMotionVisible: Theme.isConnectedEffect ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive)
|
||||
property var spotlightContent: launcherContentLoader.item
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
property bool _windowEnabled: true
|
||||
property bool _pendingInitialize: false
|
||||
property string _pendingQuery: ""
|
||||
property string _pendingMode: ""
|
||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||
|
||||
// Animation state — matches DankPopout/DankModal pattern
|
||||
property bool animationsEnabled: true
|
||||
property bool _motionActive: false
|
||||
property real _frozenMotionX: 0
|
||||
property real _frozenMotionY: 0
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property var effectiveScreen: contentWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
|
||||
readonly property int baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 500;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 620;
|
||||
}
|
||||
}
|
||||
readonly property int baseHeight: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 480;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 600;
|
||||
}
|
||||
}
|
||||
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
|
||||
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
|
||||
|
||||
readonly property string preferredConnectedBarSide: SettingsData.frameLauncherEmergeSide
|
||||
|
||||
readonly property bool frameConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
|
||||
|
||||
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
|
||||
|
||||
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== ""
|
||||
readonly property bool launcherArcExtenderActive: frameOwnsConnectedChrome && SettingsData.frameLauncherArcExtender && (resolvedConnectedBarSide === "top" || resolvedConnectedBarSide === "bottom")
|
||||
|
||||
function _dockOccupiesSide(side) {
|
||||
if (!SettingsData.showDock)
|
||||
return false;
|
||||
switch (side) {
|
||||
case "top":
|
||||
return SettingsData.dockPosition === SettingsData.Position.Top;
|
||||
case "bottom":
|
||||
return SettingsData.dockPosition === SettingsData.Position.Bottom;
|
||||
case "left":
|
||||
return SettingsData.dockPosition === SettingsData.Position.Left;
|
||||
case "right":
|
||||
return SettingsData.dockPosition === SettingsData.Position.Right;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
|
||||
|
||||
function _frameEdgeInset(side) {
|
||||
if (!effectiveScreen)
|
||||
return 0;
|
||||
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
|
||||
}
|
||||
|
||||
// frameEdgeInsetForSide is the full inset; do not add frameBarSize.
|
||||
// Positions the modal flush to the emerge side, centered on the cross axis.
|
||||
readonly property var _connectedModalPos: {
|
||||
const fallback = {
|
||||
"x": (screenWidth - modalWidth) / 2,
|
||||
"y": (screenHeight - modalHeight) / 2
|
||||
};
|
||||
switch (resolvedConnectedBarSide) {
|
||||
case "top":
|
||||
case "bottom":
|
||||
{
|
||||
const insetL = _frameEdgeInset("left");
|
||||
const insetR = _frameEdgeInset("right");
|
||||
const insetT = _frameEdgeInset("top");
|
||||
const insetB = _frameEdgeInset("bottom");
|
||||
const usable = Math.max(0, screenWidth - insetL - insetR);
|
||||
const usableH = Math.max(0, screenHeight - insetT - insetB);
|
||||
return {
|
||||
"x": insetL + Math.max(0, (usable - modalWidth) / 2),
|
||||
"y": launcherArcExtenderActive ? insetT + Math.max(0, (usableH - modalHeight) / 2) : (resolvedConnectedBarSide === "top" ? insetT : screenHeight - modalHeight - insetB)
|
||||
};
|
||||
}
|
||||
case "left":
|
||||
case "right":
|
||||
{
|
||||
const insetT = _frameEdgeInset("top");
|
||||
const insetB = _frameEdgeInset("bottom");
|
||||
const usable = Math.max(0, screenHeight - insetT - insetB);
|
||||
return {
|
||||
"x": resolvedConnectedBarSide === "left" ? _frameEdgeInset("left") : screenWidth - modalWidth - _frameEdgeInset("right"),
|
||||
"y": insetT + Math.max(0, (usable - modalHeight) / 2)
|
||||
};
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
readonly property real modalX: frameOwnsConnectedChrome ? _connectedModalPos.x : ((screenWidth - modalWidth) / 2)
|
||||
readonly property real modalY: frameOwnsConnectedChrome ? _connectedModalPos.y : ((screenHeight - modalHeight) / 2)
|
||||
|
||||
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
|
||||
readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
|
||||
readonly property list<real> launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
|
||||
readonly property list<real> launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
|
||||
readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
readonly property color borderColor: {
|
||||
if (!SettingsData.dankLauncherV2BorderEnabled)
|
||||
return Theme.outlineMedium;
|
||||
switch (SettingsData.dankLauncherV2BorderColor) {
|
||||
case "primary":
|
||||
return Theme.primary;
|
||||
case "secondary":
|
||||
return Theme.secondary;
|
||||
case "outline":
|
||||
return Theme.outline;
|
||||
case "surfaceText":
|
||||
return Theme.surfaceText;
|
||||
default:
|
||||
return Theme.primary;
|
||||
}
|
||||
}
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
|
||||
readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
|
||||
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
|
||||
// Shadow padding for the content window (render padding only, no motion padding).
|
||||
// Zeroed when frame owns the chrome and Wayland clips past the bar edge
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap(modalX, dpr)
|
||||
readonly property real alignedY: Theme.snap(modalY, dpr)
|
||||
readonly property real _connectedChromeX: alignedX
|
||||
readonly property real _connectedChromeY: {
|
||||
if (!launcherArcExtenderActive)
|
||||
return alignedY;
|
||||
return resolvedConnectedBarSide === "top" ? Theme.snap(_frameEdgeInset("top"), dpr) : alignedY;
|
||||
}
|
||||
readonly property real _connectedChromeWidth: alignedWidth
|
||||
readonly property real _connectedChromeHeight: {
|
||||
if (!launcherArcExtenderActive)
|
||||
return alignedHeight;
|
||||
if (resolvedConnectedBarSide === "top")
|
||||
return Theme.snap(Math.max(alignedHeight, alignedY + alignedHeight - _frameEdgeInset("top")), dpr);
|
||||
if (resolvedConnectedBarSide === "bottom")
|
||||
return Theme.snap(Math.max(alignedHeight, screenHeight - _frameEdgeInset("bottom") - alignedY), dpr);
|
||||
return alignedHeight;
|
||||
}
|
||||
readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight
|
||||
|
||||
// For directional/depth: window extends from screen top (content slides within)
|
||||
// For standard: small window tightly around the modal + shadow padding
|
||||
readonly property bool _needsExtendedWindow: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || Theme.isDepthEffect
|
||||
// Content window geometry
|
||||
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
|
||||
readonly property real _cwMarginTop: launcherArcExtenderActive ? _connectedChromeY : (_needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr))
|
||||
readonly property real _cwWidth: alignedWidth + shadowPad * 2
|
||||
readonly property real _cwHeight: {
|
||||
if (launcherArcExtenderActive)
|
||||
return _connectedChromeHeight;
|
||||
if (Theme.isDirectionalEffect && !Theme.isConnectedEffect)
|
||||
return screenHeight + shadowPad;
|
||||
if (Theme.isDepthEffect)
|
||||
return alignedY + alignedHeight + shadowPad;
|
||||
return alignedHeight + shadowPad * 2;
|
||||
}
|
||||
// Where the content container sits inside the content window
|
||||
readonly property real _ccX: shadowPad
|
||||
readonly property real _ccY: launcherArcExtenderActive ? 0 : (_needsExtendedWindow ? alignedY : shadowPad)
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
|
||||
Timer {
|
||||
id: _syncTimer
|
||||
interval: 0
|
||||
onTriggered: root._flushSync()
|
||||
}
|
||||
|
||||
property string _chromeClaimId: ""
|
||||
property bool _fullSyncPending: false
|
||||
|
||||
function _nextChromeClaimId() {
|
||||
return "dms:launcher-v2:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000);
|
||||
}
|
||||
|
||||
function _currentScreenName() {
|
||||
return effectiveScreen ? effectiveScreen.name : "";
|
||||
}
|
||||
|
||||
function _publishModalChromeState() {
|
||||
const screenName = _currentScreenName();
|
||||
if (!screenName)
|
||||
return;
|
||||
ConnectedModeState.setModalState(screenName, {
|
||||
"visible": spotlightOpen || contentWindow.visible,
|
||||
"barSide": resolvedConnectedBarSide,
|
||||
"bodyX": _connectedChromeX,
|
||||
"bodyY": _connectedChromeY,
|
||||
"bodyW": _connectedChromeWidth,
|
||||
"bodyH": _connectedChromeHeight,
|
||||
"animX": contentContainer ? contentContainer.animX : 0,
|
||||
"animY": contentContainer ? contentContainer.animY : 0,
|
||||
"omitStartConnector": false,
|
||||
"omitEndConnector": false
|
||||
});
|
||||
}
|
||||
|
||||
function _syncModalChromeState() {
|
||||
if (!frameOwnsConnectedChrome) {
|
||||
_releaseModalChrome();
|
||||
return;
|
||||
}
|
||||
if (!_chromeClaimId)
|
||||
_chromeClaimId = _nextChromeClaimId();
|
||||
_publishModalChromeState();
|
||||
if (_dockBlocksEmergence && (spotlightOpen || contentWindow.visible))
|
||||
ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide);
|
||||
else
|
||||
ConnectedModeState.releaseDockRetract(_chromeClaimId);
|
||||
}
|
||||
|
||||
property bool _animSyncQueued: false
|
||||
property bool _bodySyncQueued: false
|
||||
|
||||
function _queueFullSync() {
|
||||
_fullSyncPending = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _queueAnimSync() {
|
||||
_animSyncQueued = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _queueBodySync() {
|
||||
_bodySyncQueued = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _flushSync() {
|
||||
const fullDirty = _fullSyncPending;
|
||||
const animDirty = _animSyncQueued;
|
||||
const bodyDirty = _bodySyncQueued;
|
||||
_fullSyncPending = false;
|
||||
_animSyncQueued = false;
|
||||
_bodySyncQueued = false;
|
||||
if (fullDirty)
|
||||
_syncModalChromeState();
|
||||
if (animDirty)
|
||||
_syncModalAnim();
|
||||
if (bodyDirty)
|
||||
_syncModalBody();
|
||||
}
|
||||
|
||||
function _syncModalAnim() {
|
||||
if (!frameOwnsConnectedChrome || !_chromeClaimId)
|
||||
return;
|
||||
const screenName = _currentScreenName();
|
||||
if (!screenName || !contentContainer)
|
||||
return;
|
||||
ConnectedModeState.setModalAnim(screenName, contentContainer.animX, contentContainer.animY);
|
||||
}
|
||||
|
||||
function _syncModalBody() {
|
||||
if (!frameOwnsConnectedChrome || !_chromeClaimId)
|
||||
return;
|
||||
const screenName = _currentScreenName();
|
||||
if (!screenName)
|
||||
return;
|
||||
ConnectedModeState.setModalBody(screenName, _connectedChromeX, _connectedChromeY, _connectedChromeWidth, _connectedChromeHeight);
|
||||
}
|
||||
|
||||
function _releaseModalChrome() {
|
||||
if (_chromeClaimId) {
|
||||
ConnectedModeState.releaseDockRetract(_chromeClaimId);
|
||||
_chromeClaimId = "";
|
||||
}
|
||||
const screenName = _currentScreenName();
|
||||
if (screenName)
|
||||
ConnectedModeState.clearModalState(screenName);
|
||||
}
|
||||
|
||||
onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
|
||||
onLauncherArcExtenderActiveChanged: _queueFullSync()
|
||||
onResolvedConnectedBarSideChanged: _queueFullSync()
|
||||
onSpotlightOpenChanged: _queueFullSync()
|
||||
onAlignedXChanged: _queueBodySync()
|
||||
onAlignedYChanged: _queueBodySync()
|
||||
onAlignedWidthChanged: _queueBodySync()
|
||||
onAlignedHeightChanged: _queueBodySync()
|
||||
|
||||
Component.onDestruction: _releaseModalChrome()
|
||||
|
||||
Connections {
|
||||
target: contentWindow
|
||||
function onVisibleChanged() {
|
||||
if (contentWindow.visible)
|
||||
root._syncModalChromeState();
|
||||
else
|
||||
root._releaseModalChrome();
|
||||
}
|
||||
}
|
||||
|
||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||
_pendingQuery = query || "";
|
||||
_pendingMode = mode || "";
|
||||
_pendingInitialize = true;
|
||||
contentVisible = true;
|
||||
launcherContentLoader.active = true;
|
||||
|
||||
if (spotlightContent) {
|
||||
_initializeAndShow(_pendingQuery, _pendingMode);
|
||||
_pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
|
||||
function _initializeAndShow(query, mode) {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
contentVisible = true;
|
||||
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
||||
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query;
|
||||
}
|
||||
if (spotlightContent.controller) {
|
||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
||||
spotlightContent.controller.searchMode = targetMode;
|
||||
spotlightContent.controller.activePluginId = "";
|
||||
spotlightContent.controller.activePluginName = "";
|
||||
spotlightContent.controller.pluginFilter = "";
|
||||
spotlightContent.controller.fileSearchType = "all";
|
||||
spotlightContent.controller.fileSearchExt = "";
|
||||
spotlightContent.controller.fileSearchFolder = "";
|
||||
spotlightContent.controller.fileSearchSort = "score";
|
||||
spotlightContent.controller.collapsedSections = {};
|
||||
spotlightContent.controller.selectedFlatIndex = 0;
|
||||
spotlightContent.controller.selectedItem = null;
|
||||
if (query) {
|
||||
spotlightContent.controller.setSearchQuery(query);
|
||||
} else {
|
||||
spotlightContent.controller.searchQuery = "";
|
||||
spotlightContent.controller.performSearch();
|
||||
}
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
}
|
||||
if (spotlightContent.actionPanel) {
|
||||
spotlightContent.actionPanel.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function _openCommon(query, mode) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
// Disable animations so the snap is instant
|
||||
animationsEnabled = false;
|
||||
|
||||
// Freeze the collapsed offsets (they depend on height which could change)
|
||||
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
|
||||
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
backgroundWindow.screen = focusedScreen;
|
||||
contentWindow.screen = focusedScreen;
|
||||
}
|
||||
|
||||
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
// Make windows visible but do NOT request keyboard focus yet
|
||||
ModalManager.openModal(modalHandle);
|
||||
spotlightOpen = true;
|
||||
backgroundWindow.visible = true;
|
||||
contentWindow.visible = true;
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
// Load content and initialize (but no forceActiveFocus — that's deferred)
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
|
||||
// Frame 1: enable animations and trigger enter motion
|
||||
Qt.callLater(() => {
|
||||
root.animationsEnabled = true;
|
||||
root._motionActive = true;
|
||||
|
||||
// Frame 2: request keyboard focus + activate search field
|
||||
// Double-deferred to avoid compositor IPC competing with animation frames
|
||||
Qt.callLater(() => {
|
||||
root.keyboardActive = true;
|
||||
if (root.spotlightContent && root.spotlightContent.searchField)
|
||||
root.spotlightContent.searchField.forceActiveFocus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function show() {
|
||||
_openCommon("", "");
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
_openCommon(query, "");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!spotlightOpen)
|
||||
return;
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
|
||||
if (!Theme.isDirectionalEffect)
|
||||
contentVisible = false;
|
||||
|
||||
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
spotlightOpen ? hide() : show();
|
||||
}
|
||||
|
||||
function showWithMode(mode) {
|
||||
_openCommon("", mode);
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
} else {
|
||||
showWithMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleWithQuery(query) {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
} else {
|
||||
showWithQuery(query);
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeCleanupTimer
|
||||
interval: Theme.variantCloseInterval(root.launcherAnimationDuration)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isClosing = false;
|
||||
contentVisible = false;
|
||||
contentWindow.visible = false;
|
||||
backgroundWindow.visible = false;
|
||||
if (root.unloadContentOnClose)
|
||||
launcherContentLoader.active = false;
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: spotlightContent?.controller ?? null
|
||||
function onModeChanged(mode) {
|
||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
||||
return;
|
||||
SessionData.setLauncherLastMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [contentWindow]
|
||||
active: false
|
||||
|
||||
onCleared: {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== modalHandle && spotlightOpen) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (Quickshell.screens.length === 0)
|
||||
return;
|
||||
|
||||
const screen = contentWindow.screen;
|
||||
const screenName = screen?.name;
|
||||
|
||||
let needsReset = !screen || !screenName;
|
||||
if (!needsReset) {
|
||||
needsReset = true;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === screenName) {
|
||||
needsReset = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsReset)
|
||||
return;
|
||||
|
||||
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||
if (!newScreen)
|
||||
return;
|
||||
|
||||
root._windowEnabled = false;
|
||||
backgroundWindow.screen = newScreen;
|
||||
contentWindow.screen = newScreen;
|
||||
Qt.callLater(() => {
|
||||
root._windowEnabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: backgroundWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
readonly property real _topMargin: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
|
||||
readonly property real _bottomMargin: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
|
||||
readonly property real _leftMargin: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
|
||||
readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight:bg"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
WlrLayershell.margins {
|
||||
top: backgroundWindow._topMargin
|
||||
bottom: backgroundWindow._bottomMargin
|
||||
left: backgroundWindow._leftMargin
|
||||
right: backgroundWindow._rightMargin
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
|
||||
|
||||
Region {
|
||||
item: bgContentHole
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bgFullScreenMask
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bgContentHole
|
||||
visible: false
|
||||
x: root._cwMarginLeft + contentContainer.x - backgroundWindow._leftMargin
|
||||
y: root._cwMarginTop + contentContainer.y - backgroundWindow._topMargin
|
||||
width: root.alignedWidth
|
||||
height: root.contentSurfaceHeight
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: backgroundDarken
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: launcherMotionVisible || opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome
|
||||
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
||||
blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
|
||||
blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
|
||||
blurWidth: (root.spotlightOpen || root.isClosing) && !root.frameOwnsConnectedChrome ? root.alignedWidth * s : 0
|
||||
blurHeight: (root.spotlightOpen || root.isClosing) && !root.frameOwnsConnectedChrome ? root.alignedHeight * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root._cwMarginLeft
|
||||
top: root._cwMarginTop
|
||||
}
|
||||
|
||||
implicitWidth: root._cwWidth
|
||||
implicitHeight: root._cwHeight
|
||||
|
||||
mask: Region {
|
||||
item: contentInputMask
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentInputMask
|
||||
visible: false
|
||||
x: contentContainer.x
|
||||
y: contentContainer.y
|
||||
width: root.alignedWidth
|
||||
height: root.contentSurfaceHeight
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
|
||||
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
|
||||
x: root._ccX
|
||||
y: root._ccY
|
||||
width: root.alignedWidth
|
||||
height: root.contentSurfaceHeight
|
||||
|
||||
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
|
||||
readonly property bool dockTop: dockEdge === 0
|
||||
readonly property bool dockBottom: dockEdge === 1
|
||||
readonly property bool dockLeft: dockEdge === 2
|
||||
readonly property bool dockRight: dockEdge === 3
|
||||
|
||||
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
|
||||
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real _connectedTravelX: Math.max(Theme.effectAnimOffset, root.alignedWidth + Theme.spacingL)
|
||||
readonly property real _connectedTravelY: root.launcherArcExtenderActive ? root._connectedChromeHeight : Math.max(Theme.effectAnimOffset, root.alignedHeight + Theme.spacingL)
|
||||
readonly property real collapsedMotionX: {
|
||||
if (root.frameOwnsConnectedChrome) {
|
||||
switch (root.resolvedConnectedBarSide) {
|
||||
case "left":
|
||||
return -_connectedTravelX;
|
||||
case "right":
|
||||
return _connectedTravelX;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (directionalEffect) {
|
||||
if (dockLeft)
|
||||
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
|
||||
if (dockRight)
|
||||
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
|
||||
}
|
||||
if (depthEffect)
|
||||
return Theme.effectAnimOffset * 0.25;
|
||||
return 0;
|
||||
}
|
||||
readonly property real collapsedMotionY: {
|
||||
if (root.frameOwnsConnectedChrome) {
|
||||
switch (root.resolvedConnectedBarSide) {
|
||||
case "top":
|
||||
return -_connectedTravelY;
|
||||
case "bottom":
|
||||
return _connectedTravelY;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (directionalEffect) {
|
||||
if (dockTop)
|
||||
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
|
||||
if (dockBottom)
|
||||
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
|
||||
return 0;
|
||||
}
|
||||
if (depthEffect)
|
||||
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
|
||||
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
|
||||
}
|
||||
|
||||
// openProgress: 0 = closed (at frozenMotion, scaleCollapsed), 1 = open (at 0, scale 1).
|
||||
QtObject {
|
||||
id: morph
|
||||
property real openProgress: root._motionActive ? 1 : 0
|
||||
Behavior on openProgress {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real animX: root._frozenMotionX * (1 - morph.openProgress)
|
||||
readonly property real animY: root._frozenMotionY * (1 - morph.openProgress)
|
||||
readonly property real scaleValue: Theme.effectScaleCollapsed + (1.0 - Theme.effectScaleCollapsed) * morph.openProgress
|
||||
|
||||
onAnimXChanged: if (root.frameOwnsConnectedChrome)
|
||||
root._queueAnimSync()
|
||||
onAnimYChanged: if (root.frameOwnsConnectedChrome)
|
||||
root._queueAnimSync()
|
||||
|
||||
Item {
|
||||
id: directionalClipMask
|
||||
readonly property bool shouldClip: Theme.isDirectionalEffect
|
||||
readonly property real clipOversize: 2000
|
||||
|
||||
clip: shouldClip
|
||||
|
||||
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
|
||||
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
|
||||
|
||||
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
|
||||
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
|
||||
|
||||
Item {
|
||||
id: aligner
|
||||
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
|
||||
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
|
||||
width: contentContainer.width
|
||||
height: contentContainer.height
|
||||
|
||||
// Shadow mirrors contentWrapper position/scale/opacity
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: contentWrapper.publishedOpacity
|
||||
scale: contentWrapper.scale
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.backgroundColor
|
||||
borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor
|
||||
borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth
|
||||
targetRadius: root.cornerRadius
|
||||
shadowEnabled: !root.frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
// contentWrapper moves inside static contentContainer — DankPopout pattern
|
||||
Item {
|
||||
id: contentWrapper
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
property bool _renderActive: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || launcherMotionVisible
|
||||
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
|
||||
|
||||
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
|
||||
visible: _renderActive
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on publishedOpacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
onRunningChanged: if (!running && contentWrapper.publishedOpacity === 0)
|
||||
contentWrapper._renderActive = false
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onLauncherMotionVisibleChanged() {
|
||||
if (root.launcherMotionVisible)
|
||||
contentWrapper._renderActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
} // contentWrapper
|
||||
} // aligner
|
||||
} // directionalClipMask
|
||||
} // contentContainer
|
||||
} // PanelWindow
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankLauncherV2ModalStandalone")
|
||||
|
||||
property var modalHandle: root
|
||||
|
||||
visible: false
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property bool keyboardActive: false
|
||||
property bool contentVisible: false
|
||||
property var spotlightContent: launcherContentLoader.item
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
property bool _pendingInitialize: false
|
||||
property string _pendingQuery: ""
|
||||
property string _pendingMode: ""
|
||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property var effectiveScreen: launcherWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
|
||||
readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
|
||||
readonly property string resolvedConnectedBarSide: frameOwnsConnectedChrome ? (SettingsData.frameLauncherEmergeSide || "bottom") : ""
|
||||
|
||||
readonly property int baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 500;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 620;
|
||||
}
|
||||
}
|
||||
readonly property int baseHeight: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 480;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 600;
|
||||
}
|
||||
}
|
||||
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
|
||||
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
|
||||
readonly property real modalX: (screenWidth - modalWidth) / 2
|
||||
readonly property real modalY: (screenHeight - modalHeight) / 2
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap(modalX, dpr)
|
||||
readonly property real alignedY: Theme.snap(modalY, dpr)
|
||||
readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowPad, dpr))
|
||||
readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowPad, dpr))
|
||||
readonly property real contentX: Theme.snap(alignedX - windowX, dpr)
|
||||
readonly property real contentY: Theme.snap(alignedY - windowY, dpr)
|
||||
readonly property real windowWidth: alignedWidth + contentX + shadowPad
|
||||
readonly property real windowHeight: alignedHeight + contentY + shadowPad
|
||||
|
||||
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property real cornerRadius: Theme.cornerRadius
|
||||
readonly property color borderColor: {
|
||||
if (!SettingsData.dankLauncherV2BorderEnabled)
|
||||
return Theme.outlineMedium;
|
||||
switch (SettingsData.dankLauncherV2BorderColor) {
|
||||
case "primary":
|
||||
return Theme.primary;
|
||||
case "secondary":
|
||||
return Theme.secondary;
|
||||
case "outline":
|
||||
return Theme.outline;
|
||||
case "surfaceText":
|
||||
return Theme.surfaceText;
|
||||
default:
|
||||
return Theme.primary;
|
||||
}
|
||||
}
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||
_pendingQuery = query || "";
|
||||
_pendingMode = mode || "";
|
||||
_pendingInitialize = true;
|
||||
contentVisible = true;
|
||||
launcherContentLoader.active = true;
|
||||
|
||||
if (spotlightContent) {
|
||||
_initializeAndShow(_pendingQuery, _pendingMode);
|
||||
_pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
|
||||
function _initializeAndShow(query, mode) {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
contentVisible = true;
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
|
||||
var targetQuery = "";
|
||||
|
||||
if (query) {
|
||||
targetQuery = query;
|
||||
} else if (SettingsData.rememberLastQuery) {
|
||||
targetQuery = SessionData.launcherLastQuery || "";
|
||||
}
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = targetQuery;
|
||||
}
|
||||
if (spotlightContent.controller) {
|
||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
||||
spotlightContent.controller.searchMode = targetMode;
|
||||
spotlightContent.controller.activePluginId = "";
|
||||
spotlightContent.controller.activePluginName = "";
|
||||
spotlightContent.controller.pluginFilter = "";
|
||||
spotlightContent.controller.fileSearchType = "all";
|
||||
spotlightContent.controller.fileSearchExt = "";
|
||||
spotlightContent.controller.fileSearchFolder = "";
|
||||
spotlightContent.controller.fileSearchSort = "score";
|
||||
spotlightContent.controller.collapsedSections = {};
|
||||
spotlightContent.controller.selectedFlatIndex = 0;
|
||||
spotlightContent.controller.selectedItem = null;
|
||||
spotlightContent.controller.historyIndex = -1;
|
||||
spotlightContent.controller.searchQuery = targetQuery;
|
||||
|
||||
spotlightContent.controller.performSearch();
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
}
|
||||
if (spotlightContent.actionPanel) {
|
||||
spotlightContent.actionPanel.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function _finishShow(query, mode) {
|
||||
spotlightOpen = true;
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(modalHandle);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
}
|
||||
|
||||
function _openCommon(query, mode) {
|
||||
closeCleanupTimer.stop();
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow(query, mode));
|
||||
return;
|
||||
}
|
||||
_finishShow(query, mode);
|
||||
}
|
||||
|
||||
function show() {
|
||||
_openCommon("", "");
|
||||
}
|
||||
function showWithQuery(query) {
|
||||
_openCommon(query, "");
|
||||
}
|
||||
function showWithMode(mode) {
|
||||
_openCommon("", mode);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!spotlightOpen)
|
||||
return;
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
contentVisible = false;
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
spotlightOpen ? hide() : show();
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
} else {
|
||||
showWithMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleWithQuery(query) {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
} else {
|
||||
showWithQuery(query);
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeCleanupTimer
|
||||
interval: Theme.modalAnimationDuration + 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isClosing = false;
|
||||
if (root.unloadContentOnClose)
|
||||
launcherContentLoader.active = false;
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: spotlightContent?.controller ?? null
|
||||
|
||||
function onModeChanged(mode) {
|
||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
||||
return;
|
||||
SessionData.setLauncherLastMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
active: false
|
||||
|
||||
onCleared: {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== modalHandle && spotlightOpen) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (Quickshell.screens.length === 0)
|
||||
return;
|
||||
|
||||
const screenName = launcherWindow.screen?.name;
|
||||
if (screenName) {
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === screenName)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (spotlightOpen)
|
||||
hide();
|
||||
|
||||
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||
if (newScreen)
|
||||
launcherWindow.screen = newScreen;
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: clickCatcher
|
||||
screen: launcherWindow.screen
|
||||
visible: spotlightOpen
|
||||
color: "transparent"
|
||||
updatesEnabled: false
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: outsideClickMask
|
||||
|
||||
Region {
|
||||
item: outsideClickHole
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: outsideClickMask
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: outsideClickHole
|
||||
visible: false
|
||||
color: "transparent"
|
||||
x: root.alignedX
|
||||
y: root.alignedY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: spotlightOpen || isClosing
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: launcherWindow
|
||||
readonly property real s: Math.min(1, modalContainer.publishedScale)
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
|
||||
blurWidth: contentVisible ? modalContainer.width * s : 0
|
||||
blurHeight: contentVisible ? modalContainer.height * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root.windowX
|
||||
top: root.windowY
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
|
||||
implicitWidth: root.windowWidth
|
||||
implicitHeight: root.windowHeight
|
||||
|
||||
mask: Region {
|
||||
item: launcherInputMask
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: launcherInputMask
|
||||
visible: false
|
||||
color: "transparent"
|
||||
x: modalContainer.x
|
||||
y: modalContainer.y
|
||||
width: modalContainer.width
|
||||
height: modalContainer.height
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.contentX
|
||||
y: root.contentY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
visible: _renderActive
|
||||
|
||||
property bool _renderActive: contentVisible
|
||||
property real publishedScale: contentVisible ? 1 : 0.96
|
||||
|
||||
opacity: contentVisible ? 1 : 0
|
||||
scale: contentVisible ? 1 : 0.96
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
onRunningChanged: if (!running && !root.contentVisible)
|
||||
modalContainer._renderActive = false
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on publishedScale {
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onContentVisibleChanged() {
|
||||
if (root.contentVisible)
|
||||
modalContainer._renderActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
anchors.fill: parent
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
targetRadius: root.cornerRadius
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ FocusScope {
|
||||
editCommentField.text = existing?.comment || "";
|
||||
editEnvVarsField.text = existing?.envVars || "";
|
||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
editDgpuToggle.checked = existing?.launchOnDgpu || false;
|
||||
editMode = true;
|
||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||
}
|
||||
@@ -65,8 +64,6 @@ FocusScope {
|
||||
override.envVars = editEnvVarsField.text.trim();
|
||||
if (editExtraFlagsField.text.trim())
|
||||
override.extraFlags = editExtraFlagsField.text.trim();
|
||||
if (editDgpuToggle.checked)
|
||||
override.launchOnDgpu = true;
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeEditMode();
|
||||
}
|
||||
@@ -89,7 +86,7 @@ FocusScope {
|
||||
|
||||
Controller {
|
||||
id: controller
|
||||
active: root.parentModal?.spotlightOpen ?? true
|
||||
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
viewModeContext: root.viewModeContext
|
||||
|
||||
onItemExecuted: {
|
||||
@@ -149,18 +146,10 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Down:
|
||||
if (hasCtrl) {
|
||||
controller.navigateHistory("down");
|
||||
} else {
|
||||
controller.selectNext();
|
||||
}
|
||||
controller.selectNext();
|
||||
return;
|
||||
case Qt.Key_Up:
|
||||
if (hasCtrl) {
|
||||
controller.navigateHistory("up");
|
||||
} else {
|
||||
controller.selectPrevious();
|
||||
}
|
||||
controller.selectPrevious();
|
||||
return;
|
||||
case Qt.Key_PageDown:
|
||||
controller.selectPageDown(8);
|
||||
@@ -169,10 +158,6 @@ FocusScope {
|
||||
controller.selectPageUp(8);
|
||||
return;
|
||||
case Qt.Key_Right:
|
||||
if (hasCtrl) {
|
||||
controller.cycleMode();
|
||||
return;
|
||||
}
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectRight();
|
||||
return;
|
||||
@@ -180,25 +165,12 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Left:
|
||||
if (hasCtrl) {
|
||||
const reverse = true;
|
||||
controller.cycleMode(reverse);
|
||||
return;
|
||||
}
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectLeft();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_H:
|
||||
if (hasCtrl) {
|
||||
const reverse = true;
|
||||
controller.cycleMode(reverse);
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_J:
|
||||
if (hasCtrl) {
|
||||
controller.selectNext();
|
||||
@@ -213,13 +185,6 @@ FocusScope {
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_L:
|
||||
if (hasCtrl) {
|
||||
controller.cycleMode();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_N:
|
||||
if (hasCtrl) {
|
||||
controller.selectNextSection();
|
||||
@@ -235,19 +200,13 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Tab:
|
||||
if (hasCtrl && actionPanel.hasActions) {
|
||||
if (actionPanel.hasActions) {
|
||||
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
||||
return;
|
||||
}
|
||||
controller.selectNext();
|
||||
return;
|
||||
case Qt.Key_Backtab:
|
||||
if (hasCtrl && actionPanel.expanded) {
|
||||
const reverse = true;
|
||||
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
|
||||
return;
|
||||
}
|
||||
controller.selectPrevious();
|
||||
if (actionPanel.expanded)
|
||||
actionPanel.hide();
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
@@ -311,24 +270,29 @@ FocusScope {
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: !editMode && !(root.parentModal?.isClosing ?? false)
|
||||
visible: !editMode
|
||||
|
||||
Item {
|
||||
id: footerBar
|
||||
readonly property bool _connectedBottomEmerge: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "bottom")
|
||||
readonly property bool _connectedArcAtFooter: _connectedBottomEmerge && !(root.parentModal?.launcherArcExtenderActive ?? false)
|
||||
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: root.parentModal?.borderWidth ?? 1
|
||||
anchors.rightMargin: root.parentModal?.borderWidth ?? 1
|
||||
anchors.bottomMargin: root.parentModal?.borderWidth ?? 1
|
||||
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
|
||||
height: showFooter ? 36 : 0
|
||||
anchors.bottomMargin: _connectedBottomEmerge ? Theme.spacingM : (root.parentModal?.borderWidth ?? 1)
|
||||
height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0
|
||||
visible: showFooter
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: -Theme.cornerRadius
|
||||
// In connected mode the launcher provides the surface so update the toolbar for arcs
|
||||
visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false)
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
@@ -336,7 +300,7 @@ FocusScope {
|
||||
Row {
|
||||
id: modeButtonsRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
|
||||
spacing: 2
|
||||
@@ -408,7 +372,7 @@ FocusScope {
|
||||
Row {
|
||||
id: hintsRow
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
|
||||
spacing: Theme.spacingM
|
||||
@@ -429,7 +393,7 @@ FocusScope {
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Ctrl-Tab " + I18n.tr("actions")
|
||||
text: "Tab " + I18n.tr("actions")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
visible: actionPanel.hasActions
|
||||
@@ -503,7 +467,7 @@ FocusScope {
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
||||
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
placeholderText: ""
|
||||
ignoreUpDownKeys: true
|
||||
ignoreTabKeys: true
|
||||
@@ -737,6 +701,14 @@ FocusScope {
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||
opacity: {
|
||||
if (!root.parentModal)
|
||||
return 1;
|
||||
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
|
||||
return 1;
|
||||
return root.parentModal.isClosing ? 0 : 1;
|
||||
}
|
||||
|
||||
ResultsList {
|
||||
id: resultsList
|
||||
anchors.fill: parent
|
||||
@@ -769,7 +741,6 @@ FocusScope {
|
||||
}
|
||||
function onSearchQueryRequested(query) {
|
||||
searchField.text = query;
|
||||
searchField.cursorPosition = query.length;
|
||||
}
|
||||
function onModeChanged() {
|
||||
extFilterField.text = "";
|
||||
@@ -980,15 +951,6 @@ FocusScope {
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: editDgpuToggle
|
||||
width: parent.width
|
||||
text: I18n.tr("Launch on dGPU by default")
|
||||
visible: SessionService.nvidiaCommand.length > 0
|
||||
checked: false
|
||||
onToggled: checked => editDgpuToggle.checked = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
@@ -22,9 +21,9 @@ Rectangle {
|
||||
|
||||
onShowFileInfoChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
const fullPath = currentPath + "/" + currentFileName
|
||||
fileStatProcess.selectedFilePath = fullPath
|
||||
fileStatProcess.running = true
|
||||
const fullPath = currentPath + "/" + currentFileName;
|
||||
fileStatProcess.selectedFilePath = fullPath;
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +37,14 @@ Rectangle {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
const parts = text.trim().split('|')
|
||||
const parts = text.trim().split('|');
|
||||
if (parts.length >= 4) {
|
||||
fileStatProcess.fileStats = {
|
||||
"modifiedTime": parts[0],
|
||||
"permissions": parts[1],
|
||||
"size": parseInt(parts[2]) || 0,
|
||||
"fullPath": parts[3]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,31 +59,31 @@ Rectangle {
|
||||
|
||||
onCurrentFileNameChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
const fullPath = currentPath + "/" + currentFileName
|
||||
const fullPath = currentPath + "/" + currentFileName;
|
||||
if (fullPath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = fullPath
|
||||
fileStatProcess.running = true
|
||||
fileStatProcess.selectedFilePath = fullPath;
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileInfo(filePath, fileName, isDirectory) {
|
||||
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = filePath
|
||||
currentFileName = fileName || ""
|
||||
currentFileIsDir = isDirectory || false
|
||||
fileStatProcess.selectedFilePath = filePath;
|
||||
currentFileName = fileName || "";
|
||||
currentFileIsDir = isDirectory || false;
|
||||
|
||||
let ext = ""
|
||||
let ext = "";
|
||||
if (!isDirectory && fileName) {
|
||||
const lastDot = fileName.lastIndexOf('.')
|
||||
const lastDot = fileName.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
ext = fileName.substring(lastDot + 1).toLowerCase()
|
||||
ext = fileName.substring(lastDot + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
currentFileExtension = ext
|
||||
currentFileExtension = ext;
|
||||
|
||||
if (showFileInfo) {
|
||||
fileStatProcess.running = true
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,10 +99,10 @@ Rectangle {
|
||||
"permissions": "",
|
||||
"extension": "",
|
||||
"position": "N/A"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const hasValidFile = currentFileName !== ""
|
||||
const hasValidFile = currentFileName !== "";
|
||||
return {
|
||||
"exists": hasValidFile,
|
||||
"name": hasValidFile ? currentFileName : "Loading...",
|
||||
@@ -113,7 +112,7 @@ Rectangle {
|
||||
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
|
||||
"extension": currentFileExtension,
|
||||
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -209,23 +208,23 @@ Rectangle {
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0 || !bytes) {
|
||||
return "0 B"
|
||||
return "0 B";
|
||||
}
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDateTime(dateTimeString) {
|
||||
if (!dateTimeString) {
|
||||
return "Unknown"
|
||||
return "Unknown";
|
||||
}
|
||||
const parts = dateTimeString.split(' ')
|
||||
const parts = dateTimeString.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
return parts[0] + " " + parts[1].split('.')[0]
|
||||
return parts[0] + " " + parts[1].split('.')[0];
|
||||
}
|
||||
return dateTimeString
|
||||
return dateTimeString;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
|
||||
@@ -61,26 +61,22 @@ Item {
|
||||
border.color: Theme.primary
|
||||
opacity: 0
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
OpacityAnimator on opacity {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.8
|
||||
to: 0
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
from: 0.8
|
||||
to: 0
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
ScaleAnimator on scale {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.5
|
||||
to: 1.5
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
from: 0.5
|
||||
to: 1.5
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,26 +91,22 @@ Item {
|
||||
border.color: Theme.secondary
|
||||
opacity: 0
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
OpacityAnimator on opacity {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.8
|
||||
to: 0
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
from: 0.8
|
||||
to: 0
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
ScaleAnimator on scale {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.3
|
||||
to: 1.3
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
from: 0.3
|
||||
to: 1.3
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,9 @@ FocusScope {
|
||||
focus: active
|
||||
|
||||
sourceComponent: Component {
|
||||
DockTab {}
|
||||
DockTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
@@ -218,7 +220,9 @@ FocusScope {
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: ThemeColorsTab {}
|
||||
sourceComponent: ThemeColorsTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
@@ -518,5 +522,20 @@ FocusScope {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: frameLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 33
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: FrameTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,12 @@ Rectangle {
|
||||
"text": I18n.tr("Widgets"),
|
||||
"icon": "widgets",
|
||||
"tabIndex": 22
|
||||
},
|
||||
{
|
||||
"id": "frame",
|
||||
"text": I18n.tr("Frame"),
|
||||
"icon": "frame_source",
|
||||
"tabIndex": 33
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -8,9 +8,6 @@ DankPopout {
|
||||
|
||||
layerNamespace: "dms:app-launcher"
|
||||
|
||||
readonly property real screenWidth: screen?.width ?? 1920
|
||||
readonly property real screenHeight: screen?.height ?? 1080
|
||||
|
||||
property string _pendingMode: ""
|
||||
property string _pendingQuery: ""
|
||||
|
||||
@@ -44,35 +41,8 @@ DankPopout {
|
||||
openWithQuery(query);
|
||||
}
|
||||
|
||||
readonly property int _baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 500;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 620;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int _baseHeight: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 480;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 600;
|
||||
}
|
||||
}
|
||||
|
||||
popupWidth: Math.min(_baseWidth, screenWidth - 100)
|
||||
popupHeight: Math.min(_baseHeight, screenHeight - 100)
|
||||
|
||||
popupWidth: 560
|
||||
popupHeight: 640
|
||||
triggerWidth: 40
|
||||
positioning: ""
|
||||
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
||||
@@ -90,7 +60,7 @@ DankPopout {
|
||||
if (!lc)
|
||||
return;
|
||||
|
||||
const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || "";
|
||||
const query = _pendingQuery;
|
||||
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
|
||||
_pendingMode = "";
|
||||
_pendingQuery = "";
|
||||
@@ -102,9 +72,12 @@ DankPopout {
|
||||
if (lc.controller) {
|
||||
lc.controller.searchMode = mode;
|
||||
lc.controller.pluginFilter = "";
|
||||
lc.controller.searchQuery = query;
|
||||
|
||||
lc.controller.performSearch();
|
||||
lc.controller.searchQuery = "";
|
||||
if (query) {
|
||||
lc.controller.setSearchQuery(query);
|
||||
} else {
|
||||
lc.controller.performSearch();
|
||||
}
|
||||
}
|
||||
lc.resetScroll?.();
|
||||
lc.actionPanel?.hide();
|
||||
@@ -133,7 +106,7 @@ DankPopout {
|
||||
QtObject {
|
||||
id: modalAdapter
|
||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||
readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
|
||||
property bool isClosing: appDrawerPopout.isClosing
|
||||
|
||||
function hide() {
|
||||
appDrawerPopout.close();
|
||||
|
||||
@@ -37,7 +37,7 @@ Item {
|
||||
Loader {
|
||||
id: pluginDetailLoader
|
||||
width: parent.width
|
||||
height: parent.height - Theme.spacingS
|
||||
height: Math.max(0, parent.height - Theme.spacingS)
|
||||
y: Theme.spacingS
|
||||
active: false
|
||||
sourceComponent: null
|
||||
@@ -46,7 +46,7 @@ Item {
|
||||
Loader {
|
||||
id: coreDetailLoader
|
||||
width: parent.width
|
||||
height: parent.height - Theme.spacingS
|
||||
height: Math.max(0, parent.height - Theme.spacingS)
|
||||
y: Theme.spacingS
|
||||
active: false
|
||||
sourceComponent: null
|
||||
@@ -134,7 +134,7 @@ Item {
|
||||
}
|
||||
|
||||
pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent;
|
||||
pluginDetailLoader.active = parent.height > 0;
|
||||
pluginDetailLoader.active = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,19 +155,19 @@ Item {
|
||||
}
|
||||
|
||||
pluginDetailLoader.sourceComponent = pluginDetailInstance.ccDetailContent;
|
||||
pluginDetailLoader.active = parent.height > 0;
|
||||
pluginDetailLoader.active = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.expandedSection.startsWith("diskUsage_")) {
|
||||
coreDetailLoader.sourceComponent = diskUsageDetailComponent;
|
||||
coreDetailLoader.active = parent.height > 0;
|
||||
coreDetailLoader.active = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.expandedSection.startsWith("brightnessSlider_")) {
|
||||
coreDetailLoader.sourceComponent = brightnessDetailComponent;
|
||||
coreDetailLoader.active = parent.height > 0;
|
||||
coreDetailLoader.active = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
coreDetailLoader.active = parent.height > 0;
|
||||
coreDetailLoader.active = true;
|
||||
}
|
||||
|
||||
Component {
|
||||
|
||||
@@ -52,6 +52,35 @@ Column {
|
||||
return Math.max(100, maxPopoutHeight - totalRowHeight - rowSpacing);
|
||||
}
|
||||
|
||||
readonly property real targetImplicitHeight: {
|
||||
const rows = layoutResult.rows;
|
||||
let totalHeight = 0;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const widgets = rows[i] || [];
|
||||
const sliderOnly = widgets.length > 0 && widgets.every(w => {
|
||||
const id = w.id || "";
|
||||
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
|
||||
});
|
||||
totalHeight += sliderOnly ? (editMode ? 56 : 36) : 60;
|
||||
if (expandedSection !== "" && i === expandedRowIndex)
|
||||
totalHeight += detailHeightForSection(expandedSection) + Theme.spacingS;
|
||||
}
|
||||
totalHeight += Math.max(0, rows.length - 1) * spacing;
|
||||
return totalHeight;
|
||||
}
|
||||
|
||||
function detailHeightForSection(section) {
|
||||
if (!section)
|
||||
return 0;
|
||||
if (section === "wifi" || section === "bluetooth" || section === "builtin_vpn")
|
||||
return Math.min(350, _maxDetailHeight);
|
||||
if (section.startsWith("brightnessSlider_"))
|
||||
return Math.min(400, _maxDetailHeight);
|
||||
if (section.startsWith("plugin_"))
|
||||
return Math.min(250, _maxDetailHeight);
|
||||
return Math.min(250, _maxDetailHeight);
|
||||
}
|
||||
|
||||
function calculateRowsAndWidgets() {
|
||||
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex);
|
||||
}
|
||||
@@ -182,7 +211,10 @@ Column {
|
||||
id: detailHost
|
||||
width: parent.width
|
||||
maxAvailableHeight: root._maxDetailHeight
|
||||
height: active ? (getDetailHeight(root.expandedSection) + Theme.spacingS) : 0
|
||||
height: active ? (root.detailHeightForSection(root.expandedSection) + Theme.spacingS) : 0
|
||||
clip: true
|
||||
property string retainedSection: ""
|
||||
property var retainedWidgetData: null
|
||||
property bool active: {
|
||||
if (root.expandedSection === "")
|
||||
return false;
|
||||
@@ -199,14 +231,48 @@ Column {
|
||||
|
||||
return rowIndex === root.expandedRowIndex;
|
||||
}
|
||||
visible: active
|
||||
expandedSection: root.expandedSection
|
||||
expandedWidgetData: root.expandedWidgetData
|
||||
visible: active || height > 0.5
|
||||
expandedSection: active ? root.expandedSection : retainedSection
|
||||
expandedWidgetData: active ? root.expandedWidgetData : retainedWidgetData
|
||||
bluetoothCodecSelector: root.bluetoothCodecSelector
|
||||
widgetModel: root.model
|
||||
collapseCallback: root.requestCollapse
|
||||
screenName: root.screenName
|
||||
screenModel: root.screenModel
|
||||
|
||||
function retainActiveDetail() {
|
||||
if (!active || !root.expandedSection)
|
||||
return;
|
||||
retainedSection = root.expandedSection;
|
||||
retainedWidgetData = root.expandedWidgetData;
|
||||
}
|
||||
|
||||
onActiveChanged: retainActiveDetail()
|
||||
onHeightChanged: {
|
||||
if (!active && height <= 0.5) {
|
||||
retainedSection = "";
|
||||
retainedWidgetData = null;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onExpandedSectionChanged() {
|
||||
detailHost.retainActiveDetail();
|
||||
}
|
||||
function onExpandedWidgetDataChanged() {
|
||||
detailHost.retainActiveDetail();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: true
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.popoutAnimationDuration, detailHost.active)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: detailHost.active ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,15 @@ Item {
|
||||
signal toggleWidgetSize(int index)
|
||||
|
||||
width: {
|
||||
const widgetWidth = widgetData?.width || 50
|
||||
if (widgetWidth <= 25) return gridCellWidth
|
||||
else if (widgetWidth <= 50) return gridCellWidth * 2
|
||||
else if (widgetWidth <= 75) return gridCellWidth * 3
|
||||
else return gridCellWidth * 4
|
||||
const widgetWidth = widgetData?.width || 50;
|
||||
if (widgetWidth <= 25)
|
||||
return gridCellWidth;
|
||||
else if (widgetWidth <= 50)
|
||||
return gridCellWidth * 2;
|
||||
else if (widgetWidth <= 75)
|
||||
return gridCellWidth * 3;
|
||||
else
|
||||
return gridCellWidth * 4;
|
||||
}
|
||||
height: isSlider ? 16 : gridCellHeight
|
||||
|
||||
@@ -42,10 +46,14 @@ Item {
|
||||
z: dragArea.drag.active ? 10000 : 1
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: 150 }
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 150 }
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,14 +66,17 @@ Item {
|
||||
property int globalWidgetIndex: root.widgetIndex
|
||||
property int widgetWidth: root.widgetData?.width || 50
|
||||
|
||||
|
||||
MouseArea {
|
||||
id: editModeBlocker
|
||||
anchors.fill: parent
|
||||
enabled: root.editMode
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: function(mouse) { mouse.accepted = true }
|
||||
onWheel: function(wheel) { wheel.accepted = true }
|
||||
onPressed: function (mouse) {
|
||||
mouse.accepted = true;
|
||||
}
|
||||
onWheel: function (wheel) {
|
||||
wheel.accepted = true;
|
||||
}
|
||||
z: 100
|
||||
}
|
||||
}
|
||||
@@ -79,19 +90,19 @@ Item {
|
||||
drag.axis: Drag.XAndYAxis
|
||||
drag.smoothed: true
|
||||
|
||||
onPressed: function(mouse) {
|
||||
onPressed: function (mouse) {
|
||||
if (editMode) {
|
||||
cursorShape = Qt.ClosedHandCursor
|
||||
cursorShape = Qt.ClosedHandCursor;
|
||||
if (root.gridLayout && root.gridLayout.moveToTop) {
|
||||
root.gridLayout.moveToTop(root)
|
||||
root.gridLayout.moveToTop(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: function(mouse) {
|
||||
onReleased: function (mouse) {
|
||||
if (editMode) {
|
||||
cursorShape = Qt.OpenHandCursor
|
||||
root.snapToGrid()
|
||||
cursorShape = Qt.OpenHandCursor;
|
||||
root.snapToGrid();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,9 +112,11 @@ Item {
|
||||
Drag.hotSpot.y: height / 2
|
||||
|
||||
function swapIndices(i, j) {
|
||||
if (i === j) return;
|
||||
if (i === j)
|
||||
return;
|
||||
const arr = SettingsData.controlCenterWidgets;
|
||||
if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) return;
|
||||
if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length)
|
||||
return;
|
||||
|
||||
const copy = arr.slice();
|
||||
const tmp = copy[i];
|
||||
@@ -114,37 +127,41 @@ Item {
|
||||
}
|
||||
|
||||
function snapToGrid() {
|
||||
if (!editMode || !gridLayout) return
|
||||
if (!editMode || !gridLayout)
|
||||
return;
|
||||
const globalPos = root.mapToItem(gridLayout, 0, 0);
|
||||
const cellWidth = gridLayout.width / gridColumns;
|
||||
const cellHeight = gridCellHeight + Theme.spacingS;
|
||||
|
||||
const globalPos = root.mapToItem(gridLayout, 0, 0)
|
||||
const cellWidth = gridLayout.width / gridColumns
|
||||
const cellHeight = gridCellHeight + Theme.spacingS
|
||||
const centerX = globalPos.x + (root.width / 2);
|
||||
const centerY = globalPos.y + (root.height / 2);
|
||||
|
||||
const centerX = globalPos.x + (root.width / 2)
|
||||
const centerY = globalPos.y + (root.height / 2)
|
||||
let targetCol = Math.max(0, Math.floor(centerX / cellWidth));
|
||||
let targetRow = Math.max(0, Math.floor(centerY / cellHeight));
|
||||
|
||||
let targetCol = Math.max(0, Math.floor(centerX / cellWidth))
|
||||
let targetRow = Math.max(0, Math.floor(centerY / cellHeight))
|
||||
targetCol = Math.min(targetCol, gridColumns - 1);
|
||||
|
||||
targetCol = Math.min(targetCol, gridColumns - 1)
|
||||
|
||||
const newIndex = findBestInsertionIndex(targetRow, targetCol)
|
||||
const newIndex = findBestInsertionIndex(targetRow, targetCol);
|
||||
|
||||
if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) {
|
||||
swapIndices(widgetIndex, newIndex)
|
||||
swapIndices(widgetIndex, newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function findBestInsertionIndex(targetRow, targetCol) {
|
||||
const widgets = SettingsData.controlCenterWidgets || [];
|
||||
const n = widgets.length;
|
||||
if (!n || widgetIndex < 0 || widgetIndex >= n) return -1;
|
||||
if (!n || widgetIndex < 0 || widgetIndex >= n)
|
||||
return -1;
|
||||
|
||||
function spanFor(width) {
|
||||
const w = width ?? 50;
|
||||
if (w <= 25) return 1;
|
||||
if (w <= 50) return 2;
|
||||
if (w <= 75) return 3;
|
||||
if (w <= 25)
|
||||
return 1;
|
||||
if (w <= 50)
|
||||
return 2;
|
||||
if (w <= 75)
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
@@ -169,7 +186,13 @@ Item {
|
||||
if (i === widgetIndex) {
|
||||
draggedOrigKey = centerKey;
|
||||
} else {
|
||||
pos.push({ index: i, row, startCol, span, centerKey });
|
||||
pos.push({
|
||||
index: i,
|
||||
row,
|
||||
startCol,
|
||||
span,
|
||||
centerKey
|
||||
});
|
||||
}
|
||||
|
||||
col += span;
|
||||
@@ -179,7 +202,8 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
if (pos.length === 0) return -1;
|
||||
if (pos.length === 0)
|
||||
return -1;
|
||||
|
||||
const centerColCoord = targetCol + 0.5;
|
||||
const targetKey = targetRow * cols + centerColCoord;
|
||||
@@ -192,15 +216,20 @@ Item {
|
||||
}
|
||||
|
||||
let lo = 0, hi = pos.length - 1;
|
||||
if (targetKey <= pos[0].centerKey) return pos[0].index;
|
||||
if (targetKey >= pos[hi].centerKey) return pos[hi].index;
|
||||
if (targetKey <= pos[0].centerKey)
|
||||
return pos[0].index;
|
||||
if (targetKey >= pos[hi].centerKey)
|
||||
return pos[hi].index;
|
||||
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
const mk = pos[mid].centerKey;
|
||||
if (targetKey < mk) hi = mid - 1;
|
||||
else if (targetKey > mk) lo = mid + 1;
|
||||
else return pos[mid].index;
|
||||
if (targetKey < mk)
|
||||
hi = mid - 1;
|
||||
else if (targetKey > mk)
|
||||
lo = mid + 1;
|
||||
else
|
||||
return pos[mid].index;
|
||||
}
|
||||
const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false;
|
||||
return (movingUp ? pos[lo].index : pos[hi].index);
|
||||
@@ -240,11 +269,11 @@ Item {
|
||||
currentSize: root.widgetData?.width || 50
|
||||
isSlider: root.isSlider
|
||||
widgetIndex: root.widgetIndex
|
||||
onSizeChanged: (newSize) => {
|
||||
var widgets = SettingsData.controlCenterWidgets.slice()
|
||||
onSizeChanged: newSize => {
|
||||
var widgets = SettingsData.controlCenterWidgets.slice();
|
||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||
widgets[widgetIndex].width = newSize
|
||||
SettingsData.set("controlCenterWidgets", widgets)
|
||||
widgets[widgetIndex].width = newSize;
|
||||
SettingsData.set("controlCenterWidgets", widgets);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,7 +299,9 @@ Item {
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 150 }
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +314,9 @@ Item {
|
||||
z: -1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,19 +20,53 @@ DankPopout {
|
||||
property int expandedWidgetIndex: -1
|
||||
property var expandedWidgetData: null
|
||||
property bool powerMenuOpen: powerMenuModalLoader?.item?.shouldBeVisible ?? false
|
||||
property real targetPopupHeight: 400
|
||||
property bool _heightUpdatePending: false
|
||||
|
||||
signal lockRequested
|
||||
|
||||
function _maxPopupHeight() {
|
||||
const screenHeight = (triggerScreen?.height ?? 1080);
|
||||
return screenHeight - 100;
|
||||
}
|
||||
|
||||
function _contentTargetHeight() {
|
||||
const item = contentLoader.item;
|
||||
if (!item)
|
||||
return 400;
|
||||
const naturalHeight = item.targetImplicitHeight !== undefined ? item.targetImplicitHeight : item.implicitHeight;
|
||||
return Math.max(300, naturalHeight + 20);
|
||||
}
|
||||
|
||||
function updateTargetPopupHeight() {
|
||||
const target = Math.min(_maxPopupHeight(), _contentTargetHeight());
|
||||
if (Math.abs(targetPopupHeight - target) < 0.5)
|
||||
return;
|
||||
targetPopupHeight = target;
|
||||
}
|
||||
|
||||
function queueTargetPopupHeightUpdate() {
|
||||
if (_heightUpdatePending)
|
||||
return;
|
||||
_heightUpdatePending = true;
|
||||
Qt.callLater(() => {
|
||||
_heightUpdatePending = false;
|
||||
updateTargetPopupHeight();
|
||||
});
|
||||
}
|
||||
|
||||
function collapseAll() {
|
||||
expandedSection = "";
|
||||
expandedWidgetIndex = -1;
|
||||
expandedWidgetData = null;
|
||||
queueTargetPopupHeightUpdate();
|
||||
}
|
||||
|
||||
onEditModeChanged: {
|
||||
if (editMode) {
|
||||
collapseAll();
|
||||
}
|
||||
queueTargetPopupHeightUpdate();
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
@@ -52,12 +86,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
popupWidth: 550
|
||||
popupHeight: {
|
||||
const screenHeight = (triggerScreen?.height ?? 1080);
|
||||
const maxHeight = screenHeight - 100;
|
||||
const contentHeight = contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400;
|
||||
return Math.min(maxHeight, contentHeight);
|
||||
}
|
||||
popupHeight: targetPopupHeight
|
||||
triggerWidth: 80
|
||||
positioning: ""
|
||||
screen: triggerScreen
|
||||
@@ -95,6 +124,7 @@ DankPopout {
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
collapseAll();
|
||||
queueTargetPopupHeightUpdate();
|
||||
Qt.callLater(() => {
|
||||
if (NetworkService.activeService)
|
||||
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled;
|
||||
@@ -111,6 +141,28 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
onExpandedSectionChanged: queueTargetPopupHeightUpdate()
|
||||
onExpandedWidgetIndexChanged: queueTargetPopupHeightUpdate()
|
||||
onTriggerScreenChanged: queueTargetPopupHeightUpdate()
|
||||
|
||||
Connections {
|
||||
target: contentLoader
|
||||
function onLoaded() {
|
||||
root.queueTargetPopupHeightUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentLoader.item
|
||||
ignoreUnknownSignals: true
|
||||
function onTargetImplicitHeightChanged() {
|
||||
root.queueTargetPopupHeightUpdate();
|
||||
}
|
||||
function onImplicitHeightChanged() {
|
||||
root.queueTargetPopupHeightUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
WidgetModel {
|
||||
id: widgetModel
|
||||
}
|
||||
@@ -122,7 +174,13 @@ DankPopout {
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
|
||||
readonly property real targetImplicitHeight: {
|
||||
let total = headerPane.implicitHeight + Theme.spacingS + widgetGrid.targetImplicitHeight;
|
||||
if (editControls.visible)
|
||||
total += Theme.spacingS + editControls.height;
|
||||
return total + Theme.spacingM;
|
||||
}
|
||||
implicitHeight: targetImplicitHeight
|
||||
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
||||
|
||||
color: "transparent"
|
||||
@@ -136,95 +194,107 @@ DankPopout {
|
||||
z: 5000
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
x: Theme.spacingL
|
||||
y: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
DankFlickable {
|
||||
id: contentFlickable
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: Math.max(height, mainColumn.implicitHeight + Theme.spacingM)
|
||||
interactive: contentHeight > height
|
||||
|
||||
HeaderPane {
|
||||
id: headerPane
|
||||
width: parent.width
|
||||
editMode: root.editMode
|
||||
onEditModeToggled: root.editMode = !root.editMode
|
||||
onPowerButtonClicked: {
|
||||
if (powerMenuModalLoader) {
|
||||
powerMenuModalLoader.active = true;
|
||||
if (powerMenuModalLoader.item) {
|
||||
const bounds = Qt.rect(root.alignedX, root.alignedY, root.popupWidth, root.popupHeight);
|
||||
powerMenuModalLoader.item.openFromControlCenter(bounds, root.screen);
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: contentFlickable.width - Theme.spacingL * 2
|
||||
x: Theme.spacingL
|
||||
y: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
HeaderPane {
|
||||
id: headerPane
|
||||
width: parent.width
|
||||
editMode: root.editMode
|
||||
onEditModeToggled: root.editMode = !root.editMode
|
||||
onPowerButtonClicked: {
|
||||
if (powerMenuModalLoader) {
|
||||
powerMenuModalLoader.active = true;
|
||||
if (powerMenuModalLoader.item) {
|
||||
const bounds = Qt.rect(root.alignedX, root.alignedY, root.popupWidth, root.popupHeight);
|
||||
powerMenuModalLoader.item.openFromControlCenter(bounds, root.screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onLockRequested: {
|
||||
root.close();
|
||||
root.lockRequested();
|
||||
}
|
||||
onSettingsButtonClicked: {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
DragDropGrid {
|
||||
id: widgetGrid
|
||||
width: parent.width
|
||||
editMode: root.editMode
|
||||
maxPopoutHeight: {
|
||||
const screenHeight = (root.triggerScreen?.height ?? 1080);
|
||||
return screenHeight - 100 - Theme.spacingL - headerPane.height - Theme.spacingS;
|
||||
}
|
||||
expandedSection: root.expandedSection
|
||||
expandedWidgetIndex: root.expandedWidgetIndex
|
||||
expandedWidgetData: root.expandedWidgetData
|
||||
model: widgetModel
|
||||
bluetoothCodecSelector: bluetoothCodecSelector
|
||||
colorPickerModal: root.colorPickerModal
|
||||
screenName: root.triggerScreen?.name || ""
|
||||
screenModel: root.triggerScreen?.model || ""
|
||||
parentScreen: root.triggerScreen
|
||||
onExpandClicked: (widgetData, globalIndex) => {
|
||||
root.expandedWidgetIndex = globalIndex;
|
||||
root.expandedWidgetData = widgetData;
|
||||
if (widgetData.id === "diskUsage") {
|
||||
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"));
|
||||
} else if (widgetData.id === "brightnessSlider") {
|
||||
root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default"));
|
||||
} else {
|
||||
root.toggleSection(widgetData.id);
|
||||
onLockRequested: {
|
||||
root.close();
|
||||
root.lockRequested();
|
||||
}
|
||||
onSettingsButtonClicked: {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
onRemoveWidget: index => widgetModel.removeWidget(index)
|
||||
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
|
||||
onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index)
|
||||
onCollapseRequested: root.collapseAll()
|
||||
}
|
||||
|
||||
EditControls {
|
||||
width: parent.width
|
||||
visible: editMode
|
||||
popupScreen: root.screen
|
||||
popoutX: root.alignedX
|
||||
popoutY: root.alignedY
|
||||
popoutWidth: root.alignedWidth
|
||||
popoutHeight: root.alignedHeight
|
||||
availableWidgets: {
|
||||
if (!editMode)
|
||||
return [];
|
||||
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id);
|
||||
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets());
|
||||
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id));
|
||||
DragDropGrid {
|
||||
id: widgetGrid
|
||||
width: parent.width
|
||||
editMode: root.editMode
|
||||
maxPopoutHeight: {
|
||||
const screenHeight = (root.triggerScreen?.height ?? 1080);
|
||||
return screenHeight - 100 - Theme.spacingL - headerPane.implicitHeight - Theme.spacingS;
|
||||
}
|
||||
expandedSection: root.expandedSection
|
||||
expandedWidgetIndex: root.expandedWidgetIndex
|
||||
expandedWidgetData: root.expandedWidgetData
|
||||
model: widgetModel
|
||||
bluetoothCodecSelector: bluetoothCodecSelector
|
||||
colorPickerModal: root.colorPickerModal
|
||||
screenName: root.triggerScreen?.name || ""
|
||||
screenModel: root.triggerScreen?.model || ""
|
||||
parentScreen: root.triggerScreen
|
||||
onExpandClicked: (widgetData, globalIndex) => {
|
||||
root.expandedWidgetIndex = globalIndex;
|
||||
root.expandedWidgetData = widgetData;
|
||||
if (widgetData.id === "diskUsage") {
|
||||
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"));
|
||||
} else if (widgetData.id === "brightnessSlider") {
|
||||
root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default"));
|
||||
} else {
|
||||
root.toggleSection(widgetData.id);
|
||||
}
|
||||
}
|
||||
onRemoveWidget: index => widgetModel.removeWidget(index)
|
||||
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
|
||||
onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index)
|
||||
onCollapseRequested: root.collapseAll()
|
||||
}
|
||||
|
||||
EditControls {
|
||||
id: editControls
|
||||
width: parent.width
|
||||
visible: editMode
|
||||
popupScreen: root.screen
|
||||
popoutX: root.alignedX
|
||||
popoutY: root.alignedY
|
||||
popoutWidth: root.alignedWidth
|
||||
popoutHeight: root.alignedHeight
|
||||
availableWidgets: {
|
||||
if (!editMode)
|
||||
return [];
|
||||
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id);
|
||||
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets());
|
||||
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id));
|
||||
}
|
||||
onAddWidget: widgetId => widgetModel.addWidget(widgetId)
|
||||
onResetToDefault: () => widgetModel.resetToDefault()
|
||||
onClearAll: () => widgetModel.clearAll()
|
||||
}
|
||||
onAddWidget: widgetId => widgetModel.addWidget(widgetId)
|
||||
onResetToDefault: () => widgetModel.resetToDefault()
|
||||
onClearAll: () => widgetModel.clearAll()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ Rectangle {
|
||||
size: 24
|
||||
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
|
||||
|
||||
RotationAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: parent.visible
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
|
||||
@@ -178,7 +178,7 @@ Rectangle {
|
||||
size: 32
|
||||
color: Theme.primary
|
||||
|
||||
RotationAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: NetworkService.wifiToggling
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
@@ -494,7 +494,7 @@ Rectangle {
|
||||
size: 48
|
||||
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
|
||||
|
||||
RotationAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: wifiScanningOverlay.visible
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
|
||||
@@ -10,6 +10,8 @@ Item {
|
||||
required property var axis
|
||||
required property var barConfig
|
||||
|
||||
visible: !SettingsData.frameEnabled
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.left: parent.left
|
||||
@@ -37,6 +39,8 @@ Item {
|
||||
}
|
||||
|
||||
property real rt: {
|
||||
if (SettingsData.frameEnabled)
|
||||
return SettingsData.frameRounding;
|
||||
if (barConfig?.squareCorners ?? false)
|
||||
return 0;
|
||||
if (barWindow.hasMaximizedToplevel)
|
||||
@@ -255,11 +259,12 @@ Item {
|
||||
h = h - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${cr} 0`;
|
||||
d += ` L ${w - cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`;
|
||||
let d = `M ${crE} 0`;
|
||||
d += ` L ${w - crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${w} ${crE}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w} ${h + r}`;
|
||||
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
|
||||
@@ -273,9 +278,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
|
||||
}
|
||||
d += ` L 0 ${cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
||||
d += ` L 0 ${crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${crE} 0`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -285,11 +290,12 @@ Item {
|
||||
h = h - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${cr} ${fullH}`;
|
||||
d += ` L ${w - cr} ${fullH}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`;
|
||||
let d = `M ${crE} ${fullH}`;
|
||||
d += ` L ${w - crE} ${fullH}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${w} ${fullH - crE}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w} 0`;
|
||||
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
|
||||
@@ -303,9 +309,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
||||
}
|
||||
d += ` L 0 ${fullH - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`;
|
||||
d += ` L 0 ${fullH - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${fullH}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -314,11 +320,12 @@ Item {
|
||||
w = w - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M 0 ${cr}`;
|
||||
d += ` L 0 ${h - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`;
|
||||
let d = `M 0 ${crE}`;
|
||||
d += ` L 0 ${h - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${h}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w + r} ${h}`;
|
||||
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
|
||||
@@ -332,9 +339,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
|
||||
}
|
||||
d += ` L ${cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
||||
d += ` L ${crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 0 ${crE}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -344,11 +351,12 @@ Item {
|
||||
w = w - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${fullW} ${cr}`;
|
||||
d += ` L ${fullW} ${h - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`;
|
||||
let d = `M ${fullW} ${crE}`;
|
||||
d += ` L ${fullW} ${h - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${fullW - crE} ${h}`;
|
||||
if (r > 0) {
|
||||
d += ` L 0 ${h}`;
|
||||
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
|
||||
@@ -362,9 +370,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
||||
}
|
||||
d += ` L ${fullW - cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`;
|
||||
d += ` L ${fullW - crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${fullW} ${crE}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,31 @@ Item {
|
||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
||||
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
||||
|
||||
readonly property real _frameLeftInset: {
|
||||
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentLeftBar
|
||||
? SettingsData.frameBarSize
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameRightInset: {
|
||||
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentRightBar
|
||||
? SettingsData.frameBarSize
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameTopInset: {
|
||||
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentTopBar
|
||||
? SettingsData.frameThickness
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameBottomInset: {
|
||||
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentBottomBar
|
||||
? SettingsData.frameThickness
|
||||
: 0
|
||||
}
|
||||
|
||||
property alias hLeftSection: hLeftSection
|
||||
property alias hCenterSection: hCenterSection
|
||||
property alias hRightSection: hRightSection
|
||||
@@ -31,10 +56,14 @@ Item {
|
||||
property alias vRightSection: vRightSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0
|
||||
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0
|
||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
|
||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
|
||||
anchors.topMargin: (barWindow.isVertical
|
||||
? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
|
||||
: 0) + _frameTopInset
|
||||
anchors.bottomMargin: (barWindow.isVertical
|
||||
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
|
||||
: 0) + _frameBottomInset
|
||||
clip: false
|
||||
|
||||
property int componentMapRevision: 0
|
||||
@@ -1156,6 +1185,7 @@ Item {
|
||||
if (!notificationCenterLoader.item) {
|
||||
return;
|
||||
}
|
||||
notificationCenterLoader.item.triggerScreen = barWindow.screen;
|
||||
const effectiveBarConfig = topBarContent.barConfig;
|
||||
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||
if (notificationCenterLoader.item.setBarContext) {
|
||||
|
||||
@@ -43,10 +43,10 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
function triggerWallpaperBrowser() {
|
||||
function triggerDashTab(tabIndex) {
|
||||
dankDashPopoutLoader.active = true;
|
||||
if (!dankDashPopoutLoader.item) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let section = "center";
|
||||
@@ -82,7 +82,12 @@ PanelWindow {
|
||||
dankDashPopoutLoader.item.triggerScreen = barWindow.screen;
|
||||
}
|
||||
|
||||
PopoutManager.requestPopout(dankDashPopoutLoader.item, 2, (barConfig?.id ?? "default") + "-" + section + "-2");
|
||||
PopoutManager.requestPopout(dankDashPopoutLoader.item, tabIndex, (barConfig?.id ?? "default") + "-" + section + "-" + tabIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
function triggerWallpaperBrowser() {
|
||||
triggerDashTab(2);
|
||||
}
|
||||
|
||||
readonly property var dBarLayer: {
|
||||
@@ -94,7 +99,9 @@ PanelWindow {
|
||||
case "background":
|
||||
return WlrLayer.background;
|
||||
default:
|
||||
return WlrLayer.Top;
|
||||
// Elevate to Overlay when Frame is enabled so the bar stays above
|
||||
// the FrameWindow (WlrLayer.Top) when it is re-mapped on mode switch.
|
||||
return SettingsData.frameEnabled ? WlrLayer.Overlay : WlrLayer.Top;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +141,11 @@ PanelWindow {
|
||||
teardown();
|
||||
if (!BlurService.enabled || !BlurService.available)
|
||||
return;
|
||||
// In frame mode, FrameWindow owns the blur region for the entire screen edge
|
||||
// (including the bar area). The bar must not set its own competing blur region
|
||||
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
|
||||
if (SettingsData.frameEnabled)
|
||||
return;
|
||||
|
||||
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
|
||||
const hasBar = barHasTransparency;
|
||||
@@ -188,6 +200,13 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onFrameEnabledChanged() {
|
||||
barBlur.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: topBarSlide
|
||||
function onXChanged() {
|
||||
@@ -239,7 +258,7 @@ PanelWindow {
|
||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||
readonly property string _barId: barConfig?.id ?? "default"
|
||||
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
||||
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||
readonly property color _bgColor: SettingsData.frameEnabled ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||
|
||||
function _updateBackgroundAlpha() {
|
||||
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
||||
@@ -385,7 +404,7 @@ PanelWindow {
|
||||
shouldHideForWindows = filtered.length > 0;
|
||||
}
|
||||
|
||||
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)
|
||||
property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
|
||||
|
||||
Behavior on effectiveSpacing {
|
||||
enabled: barWindow.visible
|
||||
@@ -396,7 +415,8 @@ PanelWindow {
|
||||
}
|
||||
|
||||
readonly property int notificationCount: NotificationService.notifications.length
|
||||
readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
||||
readonly property real effectiveBarThickness: SettingsData.frameEnabled ? SettingsData.frameBarSize : Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
||||
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled ? SettingsData.frameShowOnOverview : (barConfig?.openOnOverview ?? false)
|
||||
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
|
||||
|
||||
readonly property bool hasAdjacentTopBar: {
|
||||
@@ -645,14 +665,14 @@ PanelWindow {
|
||||
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
|
||||
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right)
|
||||
|
||||
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (barConfig?.bottomGap ?? 0))
|
||||
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (Theme.isConnectedEffect ? 0 : (barConfig?.bottomGap ?? 0)))
|
||||
|
||||
Item {
|
||||
id: inputMask
|
||||
|
||||
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
|
||||
|
||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
|
||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
||||
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
||||
|
||||
@@ -793,7 +813,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false);
|
||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
||||
if (inOverviewWithShow)
|
||||
return true;
|
||||
|
||||
@@ -890,7 +910,7 @@ PanelWindow {
|
||||
top: barWindow.isVertical ? parent.top : undefined
|
||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||
}
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
|
||||
acceptedButtons: Qt.NoButton
|
||||
enabled: (barConfig?.autoHide ?? false) && !inOverview
|
||||
@@ -935,6 +955,17 @@ PanelWindow {
|
||||
barConfig: barWindow.barConfig
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -2
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: {
|
||||
const screenName = barWindow.screen?.name;
|
||||
if (screenName && PopoutManager.currentPopoutsByScreen[screenName])
|
||||
PopoutManager.closeAllPopouts();
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: scrollArea
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -148,19 +148,16 @@ DankPopout {
|
||||
opacity: enabled ? 1.0 : 0.5
|
||||
onClicked: SystemUpdateService.checkForUpdates()
|
||||
|
||||
RotationAnimation {
|
||||
target: refreshButton
|
||||
property: "rotation"
|
||||
RotationAnimator on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
running: SystemUpdateService.isChecking
|
||||
loops: Animation.Infinite
|
||||
running: SystemUpdateService.isChecking
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
if (!running)
|
||||
refreshButton.rotation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,20 +73,17 @@ BasePill {
|
||||
return root.isActive ? Theme.primary : Theme.surfaceText;
|
||||
}
|
||||
|
||||
RotationAnimation {
|
||||
RotationAnimator on rotation {
|
||||
id: rotationAnimation
|
||||
target: statusIcon
|
||||
property: "rotation"
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
running: root.isChecking
|
||||
loops: Animation.Infinite
|
||||
running: root.isChecking
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
if (!running)
|
||||
statusIcon.rotation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,20 +127,17 @@ BasePill {
|
||||
return root.isActive ? Theme.primary : Theme.surfaceText;
|
||||
}
|
||||
|
||||
RotationAnimation {
|
||||
RotationAnimator on rotation {
|
||||
id: rotationAnimationHorizontal
|
||||
target: statusIconHorizontal
|
||||
property: "rotation"
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
running: root.isChecking
|
||||
loops: Animation.Infinite
|
||||
running: root.isChecking
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
if (!running)
|
||||
statusIconHorizontal.rotation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ DankPopout {
|
||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||
triggerWidth: 80
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: dashVisible
|
||||
|
||||
property bool __focusArmed: false
|
||||
property bool __contentReady: false
|
||||
@@ -54,7 +53,8 @@ DankPopout {
|
||||
function __hideDropdowns() {
|
||||
__volumeCloseTimer.stop();
|
||||
__dropdownType = 0;
|
||||
__mediaTabRef?.resetDropdownStates();
|
||||
if (__mediaTabRef && typeof __mediaTabRef.resetDropdownStates === "function")
|
||||
__mediaTabRef.resetDropdownStates();
|
||||
}
|
||||
|
||||
function __startCloseTimer() {
|
||||
@@ -75,7 +75,11 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
overlayContent: Component {
|
||||
overlayContent: shouldBeVisible ? mediaDropdownOverlayComponent : null
|
||||
|
||||
Component {
|
||||
id: mediaDropdownOverlayComponent
|
||||
|
||||
MediaDropdownOverlay {
|
||||
dropdownType: root.__dropdownType
|
||||
anchorPos: root.__dropdownAnchor
|
||||
@@ -183,11 +187,8 @@ DankPopout {
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
Qt.callLater(function () {
|
||||
mainContainer.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
if (root.shouldBeVisible)
|
||||
mainContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +380,10 @@ DankPopout {
|
||||
section: root.triggerSection
|
||||
barPosition: root.effectiveBarPosition
|
||||
Component.onCompleted: root.__mediaTabRef = this
|
||||
Component.onDestruction: {
|
||||
if (root.__mediaTabRef === this)
|
||||
root.__mediaTabRef = null;
|
||||
}
|
||||
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
|
||||
root.__showVolumeDropdown(pos, rightEdge, player, players);
|
||||
}
|
||||
|
||||
@@ -50,11 +50,9 @@ Item {
|
||||
}
|
||||
|
||||
function volumeAreaExited() {
|
||||
__volumeHoverCount--;
|
||||
Qt.callLater(() => {
|
||||
if (__volumeHoverCount <= 0)
|
||||
panelExited();
|
||||
});
|
||||
__volumeHoverCount = Math.max(0, __volumeHoverCount - 1);
|
||||
if (__volumeHoverCount === 0)
|
||||
panelExited();
|
||||
}
|
||||
|
||||
readonly property Item __activePanel: {
|
||||
@@ -93,23 +91,25 @@ Item {
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
|
||||
opacity: dropdownType === 1 ? 1 : 0
|
||||
scale: dropdownType === 1 ? 1 : 0.96
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,23 +233,25 @@ Item {
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 2
|
||||
|
||||
opacity: dropdownType === 2 ? 1 : 0
|
||||
scale: dropdownType === 2 ? 1 : 0.96
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,23 +389,25 @@ Item {
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 2
|
||||
|
||||
opacity: dropdownType === 3 ? 1 : 0
|
||||
scale: dropdownType === 3 ? 1 : 0.96
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ Item {
|
||||
onTriggered: refreshButtonTwo.isRefreshing = false
|
||||
}
|
||||
|
||||
NumberAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: refreshButtonTwo.isRefreshing
|
||||
from: 0
|
||||
to: 360
|
||||
@@ -930,7 +930,7 @@ Item {
|
||||
onTriggered: refreshButton.isRefreshing = false
|
||||
}
|
||||
|
||||
NumberAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: refreshButton.isRefreshing
|
||||
from: 0
|
||||
to: 360
|
||||
|
||||
@@ -20,14 +20,16 @@ Variants {
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: dock
|
||||
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive
|
||||
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
|
||||
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
|
||||
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
|
||||
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:dock"
|
||||
WlrLayershell.layer: SettingsData.frameEnabled ? WlrLayer.Overlay : WlrLayer.Top
|
||||
|
||||
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
|
||||
@@ -43,6 +45,25 @@ Variants {
|
||||
property real backgroundTransparency: SettingsData.dockTransparency
|
||||
property bool groupByApp: SettingsData.dockGroupByApp
|
||||
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
|
||||
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
|
||||
readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect && !!(dock.screen || modelData) && SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide)
|
||||
readonly property real connectedJoinInset: {
|
||||
if (Theme.isConnectedEffect)
|
||||
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
||||
if (SettingsData.frameEnabled)
|
||||
return SettingsData.frameEdgeInsetForSide(dock.screen || modelData, dock.connectedBarSide);
|
||||
return 0;
|
||||
}
|
||||
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
|
||||
readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
|
||||
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor
|
||||
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
|
||||
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
|
||||
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
|
||||
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
|
||||
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
|
||||
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && !isVertical ? Theme.connectedCornerRadius : 0
|
||||
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0
|
||||
|
||||
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
|
||||
|
||||
@@ -114,14 +135,76 @@ Variants {
|
||||
return getBarHeight(leftBar);
|
||||
}
|
||||
|
||||
readonly property real dockMargin: SettingsData.dockSpacing
|
||||
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
|
||||
readonly property real dockMargin: SettingsData.dockMargin
|
||||
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
readonly property real effectiveDockBottomGap: Theme.isConnectedEffect ? 0 : SettingsData.dockBottomGap
|
||||
readonly property real effectiveDockMargin: Theme.isConnectedEffect ? 0 : SettingsData.dockMargin
|
||||
readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin
|
||||
readonly property real joinedEdgeMargin: Theme.isConnectedEffect ? 0 : (barSpacing + effectiveDockMargin + 1 + dock.borderThickness)
|
||||
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
|
||||
function px(v) {
|
||||
return Math.round(v * _dpr) / _dpr;
|
||||
}
|
||||
|
||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) || (dockVariants.trashContextMenu && dockVariants.trashContextMenu.visible && dockVariants.trashContextMenu.screen === modelData)
|
||||
// Dock window origin in screen-relative coordinates (FrameWindow space).
|
||||
function _dockWindowOriginX() {
|
||||
if (!dock.isVertical)
|
||||
return 0;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
return (dock.screen ? dock.screen.width : 0) - dock.width;
|
||||
return 0;
|
||||
}
|
||||
function _dockWindowOriginY() {
|
||||
if (dock.isVertical)
|
||||
return 0;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||
return (dock.screen ? dock.screen.height : 0) - dock.height;
|
||||
return 0;
|
||||
}
|
||||
|
||||
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
|
||||
|
||||
function _syncDockChromeState() {
|
||||
if (!dock._dockScreenName)
|
||||
return;
|
||||
if (!SettingsData.connectedFrameModeActive) {
|
||||
ConnectedModeState.clearDockState(dock._dockScreenName);
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectedModeState.setDockState(dock._dockScreenName, {
|
||||
"reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps,
|
||||
"barSide": dock.connectedBarSide,
|
||||
"bodyX": dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x,
|
||||
"bodyY": dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y,
|
||||
"bodyW": dock.hasApps ? dockBackground.width : 0,
|
||||
"bodyH": dock.hasApps ? dockBackground.height : 0,
|
||||
"slideX": dockSlide.x,
|
||||
"slideY": dockSlide.y
|
||||
});
|
||||
}
|
||||
|
||||
function _syncDockSlide() {
|
||||
if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive)
|
||||
return;
|
||||
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
|
||||
}
|
||||
|
||||
property bool _slideSyncPending: false
|
||||
function _queueSlideSync() {
|
||||
if (!SettingsData.connectedFrameModeActive)
|
||||
return;
|
||||
if (_slideSyncPending)
|
||||
return;
|
||||
_slideSyncPending = true;
|
||||
Qt.callLater(dock._flushSlideSync);
|
||||
}
|
||||
function _flushSlideSync() {
|
||||
_slideSyncPending = false;
|
||||
dock._syncDockSlide();
|
||||
}
|
||||
|
||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
|
||||
property bool revealSticky: false
|
||||
|
||||
readonly property bool shouldHideForWindows: {
|
||||
@@ -131,7 +214,7 @@ Variants {
|
||||
return false;
|
||||
|
||||
const screenName = dock.modelData?.name ?? "";
|
||||
const dockThickness = effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin;
|
||||
const dockThickness = dock.connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin;
|
||||
const screenWidth = dock.screen?.width ?? 0;
|
||||
const screenHeight = dock.screen?.height ?? 0;
|
||||
|
||||
@@ -259,7 +342,17 @@ Variants {
|
||||
onTriggered: dock.revealSticky = false
|
||||
}
|
||||
|
||||
// Flip `reveal` false when a modal claims this edge; reuses the slide animation
|
||||
readonly property bool _modalRetractActive: {
|
||||
if (!dock._dockScreenName)
|
||||
return false;
|
||||
return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide);
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
if (_modalRetractActive)
|
||||
return false;
|
||||
|
||||
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
|
||||
return true;
|
||||
}
|
||||
@@ -282,6 +375,23 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(() => dock._syncDockChromeState())
|
||||
Component.onDestruction: ConnectedModeState.clearDockState(dock._dockScreenName)
|
||||
|
||||
onRevealChanged: dock._syncDockChromeState()
|
||||
onWidthChanged: dock._syncDockChromeState()
|
||||
onHeightChanged: dock._syncDockChromeState()
|
||||
onVisibleChanged: dock._syncDockChromeState()
|
||||
onHasAppsChanged: dock._syncDockChromeState()
|
||||
onConnectedBarSideChanged: dock._syncDockChromeState()
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onConnectedFrameModeActiveChanged() {
|
||||
dock._syncDockChromeState();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDockTransparencyChanged() {
|
||||
@@ -303,13 +413,13 @@ Variants {
|
||||
return -1;
|
||||
if (barSpacing > 0)
|
||||
return -1;
|
||||
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin);
|
||||
return px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockBottomGap + effectiveDockMargin);
|
||||
}
|
||||
|
||||
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
|
||||
|
||||
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitWidth: isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
|
||||
Item {
|
||||
id: maskItem
|
||||
@@ -319,17 +429,17 @@ Variants {
|
||||
x: {
|
||||
const baseX = dockCore.x + dockMouseArea.x;
|
||||
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
return baseX - (expanded ? animationHeadroom + borderThickness : 0);
|
||||
return baseX - (expanded ? borderThickness : 0);
|
||||
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
|
||||
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
|
||||
}
|
||||
y: {
|
||||
const baseY = dockCore.y + dockMouseArea.y;
|
||||
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||
return baseY - (expanded ? animationHeadroom + borderThickness : 0);
|
||||
return baseY - (expanded ? borderThickness : 0);
|
||||
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
|
||||
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
|
||||
}
|
||||
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
|
||||
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
|
||||
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
|
||||
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
@@ -389,7 +499,7 @@ Variants {
|
||||
const screenHeight = dock.screen ? dock.screen.height : 0;
|
||||
|
||||
const gap = Theme.spacingS;
|
||||
const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness;
|
||||
const bgMargin = dock.joinedEdgeMargin + dock.connectedJoinInset;
|
||||
const btnW = dock.hoveredButton.width;
|
||||
const btnH = dock.hoveredButton.height;
|
||||
|
||||
@@ -460,11 +570,11 @@ Variants {
|
||||
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
|
||||
}
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1;
|
||||
}
|
||||
width: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1;
|
||||
}
|
||||
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
|
||||
@@ -506,7 +616,11 @@ Variants {
|
||||
return 0;
|
||||
if (dock.reveal)
|
||||
return 0;
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10;
|
||||
if (Theme.isConnectedEffect) {
|
||||
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
|
||||
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
|
||||
}
|
||||
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return hideDistance;
|
||||
} else {
|
||||
@@ -518,7 +632,11 @@ Variants {
|
||||
return 0;
|
||||
if (dock.reveal)
|
||||
return 0;
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10;
|
||||
if (Theme.isConnectedEffect) {
|
||||
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
|
||||
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
|
||||
}
|
||||
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return hideDistance;
|
||||
} else {
|
||||
@@ -529,18 +647,27 @@ Variants {
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
id: slideXAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
|
||||
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
|
||||
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
|
||||
onRunningChanged: if (!running)
|
||||
dock._syncDockChromeState()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
id: slideYAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
|
||||
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
|
||||
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
|
||||
onRunningChanged: if (!running)
|
||||
dock._syncDockChromeState()
|
||||
}
|
||||
}
|
||||
|
||||
onXChanged: dock._queueSlideSync()
|
||||
onYChanged: dock._queueSlideSync()
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -554,33 +681,60 @@ Variants {
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
|
||||
|
||||
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
|
||||
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
layer.enabled: true
|
||||
// Avoid an offscreen texture seam where the connected dock meets the frame.
|
||||
layer.enabled: !Theme.isConnectedEffect
|
||||
clip: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal)
|
||||
color: dock.surfaceColor
|
||||
topLeftRadius: dock.surfaceTopLeftRadius
|
||||
topRightRadius: dock.surfaceTopRightRadius
|
||||
bottomLeftRadius: dock.surfaceBottomLeftRadius
|
||||
bottomRightRadius: dock.surfaceBottomRightRadius
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal)
|
||||
color: "transparent"
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
topLeftRadius: dock.surfaceTopLeftRadius
|
||||
topRightRadius: dock.surfaceTopRightRadius
|
||||
bottomLeftRadius: dock.surfaceBottomLeftRadius
|
||||
bottomRightRadius: dock.surfaceBottomRightRadius
|
||||
border.color: dock.surfaceBorderColor
|
||||
border.width: dock.surfaceBorderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
// Sync dockBackground geometry to ConnectedModeState
|
||||
onXChanged: dock._syncDockChromeState()
|
||||
onYChanged: dock._syncDockChromeState()
|
||||
onWidthChanged: dock._syncDockChromeState()
|
||||
onHeightChanged: dock._syncDockChromeState()
|
||||
}
|
||||
|
||||
ConnectedShape {
|
||||
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
|
||||
barSide: dock.connectedBarSide
|
||||
bodyWidth: dockBackground.width
|
||||
bodyHeight: dockBackground.height
|
||||
connectorRadius: Theme.connectedCornerRadius
|
||||
surfaceRadius: dock.surfaceRadius
|
||||
fillColor: dock.surfaceColor
|
||||
x: dockBackground.x - bodyX
|
||||
y: dockBackground.y - bodyY
|
||||
}
|
||||
|
||||
Shape {
|
||||
@@ -589,12 +743,12 @@ Variants {
|
||||
y: dockBackground.y - borderThickness
|
||||
width: dockBackground.width + borderThickness * 2
|
||||
height: dockBackground.height + borderThickness * 2
|
||||
visible: SettingsData.dockBorderEnabled && dock.hasApps
|
||||
visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
readonly property real borderThickness: Math.max(1, dock.borderThickness)
|
||||
readonly property real i: borderThickness / 2
|
||||
readonly property real cr: Theme.cornerRadius
|
||||
readonly property real cr: dock.surfaceRadius
|
||||
readonly property real w: dockBackground.width
|
||||
readonly property real h: dockBackground.height
|
||||
|
||||
|
||||
24
quickshell/Modules/Frame/Frame.qml
Normal file
24
quickshell/Modules/Frame/Frame.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
Variants {
|
||||
id: root
|
||||
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Loader {
|
||||
id: instanceLoader
|
||||
|
||||
required property var modelData
|
||||
|
||||
active: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(instanceLoader.modelData, SettingsData.frameScreenPreferences)
|
||||
asynchronous: false
|
||||
|
||||
sourceComponent: FrameInstance {
|
||||
screen: instanceLoader.modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
54
quickshell/Modules/Frame/FrameBorder.qml
Normal file
54
quickshell/Modules/Frame/FrameBorder.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
required property real cutoutTopInset
|
||||
required property real cutoutBottomInset
|
||||
required property real cutoutLeftInset
|
||||
required property real cutoutRightInset
|
||||
required property real cutoutRadius
|
||||
property color borderColor: Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
||||
|
||||
Rectangle {
|
||||
id: borderRect
|
||||
|
||||
anchors.fill: parent
|
||||
// Bake frameOpacity into the color alpha rather than using the `opacity` property
|
||||
color: root.borderColor
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskSource: cutoutMask
|
||||
maskEnabled: true
|
||||
maskInverted: true
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: cutoutMask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: root.cutoutTopInset
|
||||
bottomMargin: root.cutoutBottomInset
|
||||
leftMargin: root.cutoutLeftInset
|
||||
rightMargin: root.cutoutRightInset
|
||||
}
|
||||
radius: root.cutoutRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
87
quickshell/Modules/Frame/FrameExclusions.qml
Normal file
87
quickshell/Modules/Frame/FrameExclusions.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property var screen
|
||||
|
||||
readonly property var barEdges: {
|
||||
SettingsData.barConfigs; // force re-eval when bar configs change
|
||||
return SettingsData.getActiveBarEdgesForScreen(screen);
|
||||
}
|
||||
|
||||
// One thin invisible PanelWindow per edge.
|
||||
// Skips any edge where a bar already provides its own exclusiveZone.
|
||||
|
||||
readonly property bool screenEnabled: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
|
||||
|
||||
Loader {
|
||||
active: root.screenEnabled && !root.barEdges.includes("top")
|
||||
sourceComponent: EdgeExclusion {
|
||||
targetScreen: root.screen
|
||||
anchorTop: true
|
||||
anchorLeft: true
|
||||
anchorRight: true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.screenEnabled && !root.barEdges.includes("bottom")
|
||||
sourceComponent: EdgeExclusion {
|
||||
targetScreen: root.screen
|
||||
anchorBottom: true
|
||||
anchorLeft: true
|
||||
anchorRight: true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.screenEnabled && !root.barEdges.includes("left")
|
||||
sourceComponent: EdgeExclusion {
|
||||
targetScreen: root.screen
|
||||
anchorLeft: true
|
||||
anchorTop: true
|
||||
anchorBottom: true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.screenEnabled && !root.barEdges.includes("right")
|
||||
sourceComponent: EdgeExclusion {
|
||||
targetScreen: root.screen
|
||||
anchorRight: true
|
||||
anchorTop: true
|
||||
anchorBottom: true
|
||||
}
|
||||
}
|
||||
|
||||
component EdgeExclusion: PanelWindow {
|
||||
required property var targetScreen
|
||||
|
||||
screen: targetScreen
|
||||
property bool anchorTop: false
|
||||
property bool anchorBottom: false
|
||||
property bool anchorLeft: false
|
||||
property bool anchorRight: false
|
||||
|
||||
WlrLayershell.namespace: "dms:frame-exclusion"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
exclusiveZone: SettingsData.frameThickness
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
implicitWidth: 1
|
||||
implicitHeight: 1
|
||||
|
||||
anchors {
|
||||
top: anchorTop
|
||||
bottom: anchorBottom
|
||||
left: anchorLeft
|
||||
right: anchorRight
|
||||
}
|
||||
}
|
||||
}
|
||||
17
quickshell/Modules/Frame/FrameInstance.qml
Normal file
17
quickshell/Modules/Frame/FrameInstance.qml
Normal file
@@ -0,0 +1,17 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var screen
|
||||
|
||||
FrameWindow {
|
||||
targetScreen: root.screen
|
||||
}
|
||||
|
||||
FrameExclusions {
|
||||
screen: root.screen
|
||||
}
|
||||
}
|
||||
1495
quickshell/Modules/Frame/FrameWindow.qml
Normal file
1495
quickshell/Modules/Frame/FrameWindow.qml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
@@ -119,15 +118,7 @@ Item {
|
||||
function greeterPamStackHasModule(moduleName) {
|
||||
if (pamModuleEnabled(greetdPamText, moduleName))
|
||||
return true;
|
||||
const includedPamStacks = [
|
||||
["system-auth", systemAuthPamText],
|
||||
["common-auth", commonAuthPamText],
|
||||
["password-auth", passwordAuthPamText],
|
||||
["system-login", systemLoginPamText],
|
||||
["system-local-login", systemLocalLoginPamText],
|
||||
["common-auth-pc", commonAuthPcPamText],
|
||||
["login", loginPamText]
|
||||
];
|
||||
const includedPamStacks = [["system-auth", systemAuthPamText], ["common-auth", commonAuthPamText], ["password-auth", passwordAuthPamText], ["system-login", systemLoginPamText], ["system-local-login", systemLocalLoginPamText], ["common-auth-pc", commonAuthPcPamText], ["login", loginPamText]];
|
||||
for (let i = 0; i < includedPamStacks.length; i++) {
|
||||
const stack = includedPamStacks[i];
|
||||
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
||||
@@ -609,13 +600,7 @@ Item {
|
||||
running: false
|
||||
// sh wrapper: emits PROBE_UNAVAILABLE if gdbus is absent or fprintd unreachable,
|
||||
// keeping the PAM-only fallback active in those cases.
|
||||
command: ["sh", "-c",
|
||||
"command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " +
|
||||
"gdbus call --system " +
|
||||
"--dest net.reactivated.Fprint " +
|
||||
"--object-path /net/reactivated/Fprint/Manager " +
|
||||
"--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " +
|
||||
"|| echo PROBE_UNAVAILABLE"]
|
||||
command: ["sh", "-c", "command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " + "gdbus call --system " + "--dest net.reactivated.Fprint " + "--object-path /net/reactivated/Fprint/Manager " + "--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " + "|| echo PROBE_UNAVAILABLE"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.includes("PROBE_UNAVAILABLE"))
|
||||
@@ -625,7 +610,7 @@ Item {
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
}
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
if (!root.fprintdProbeComplete)
|
||||
root.maybeAutoStartExternalAuth(); // PAM-only fallback stays active
|
||||
}
|
||||
@@ -1754,7 +1739,7 @@ Item {
|
||||
authTimeout.interval = defaultAuthTimeoutMs;
|
||||
authTimeout.stop();
|
||||
if (resumePasswordSubmit) {
|
||||
Qt.callLater(function() {
|
||||
Qt.callLater(function () {
|
||||
root.startAuthSession(true);
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -1025,7 +1025,7 @@ Item {
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
|
||||
}
|
||||
|
||||
RotationAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: pam.passwd.active && !root.unlocking
|
||||
loops: Animation.Infinite
|
||||
duration: Anims.durLong
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: {
|
||||
if (PluginService.isPluginLoaded("dankNotepadModule")) {
|
||||
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadModule", "highlightedHtml", "")
|
||||
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadModule", "highlightedHtml", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,65 +32,57 @@ Column {
|
||||
property string lastPluginContent: ""
|
||||
property int loadRequestId: 0
|
||||
|
||||
signal saveRequested()
|
||||
signal openRequested()
|
||||
signal newRequested()
|
||||
signal previewRequested()
|
||||
signal escapePressed()
|
||||
signal contentChanged()
|
||||
signal settingsRequested()
|
||||
signal saveRequested
|
||||
signal openRequested
|
||||
signal newRequested
|
||||
signal previewRequested
|
||||
signal escapePressed
|
||||
signal contentChanged
|
||||
signal settingsRequested
|
||||
|
||||
function hasUnsavedChanges() {
|
||||
if (!currentTab || !contentLoaded) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentTab.isTemporary) {
|
||||
return textArea.text.length > 0
|
||||
return textArea.text.length > 0;
|
||||
}
|
||||
return textArea.text !== lastSavedContent
|
||||
return textArea.text !== lastSavedContent;
|
||||
}
|
||||
|
||||
function loadCurrentTabContent() {
|
||||
if (!currentTab) return
|
||||
|
||||
const requestedTabId = currentTab.id
|
||||
const requestId = ++loadRequestId
|
||||
contentLoaded = false
|
||||
NotepadStorageService.loadTabContent(
|
||||
NotepadStorageService.currentTabIndex,
|
||||
(content) => {
|
||||
const activeTab = NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex
|
||||
? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex]
|
||||
: null
|
||||
if (requestId !== loadRequestId || !activeTab || activeTab.id !== requestedTabId)
|
||||
return
|
||||
|
||||
lastSavedContent = content
|
||||
textArea.text = content
|
||||
contentLoaded = true
|
||||
syncContentToPlugin()
|
||||
}
|
||||
)
|
||||
if (!currentTab)
|
||||
return;
|
||||
const requestedTabId = currentTab.id;
|
||||
const requestId = ++loadRequestId;
|
||||
contentLoaded = false;
|
||||
NotepadStorageService.loadTabContent(NotepadStorageService.currentTabIndex, content => {
|
||||
const activeTab = NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null;
|
||||
if (requestId !== loadRequestId || !activeTab || activeTab.id !== requestedTabId)
|
||||
return;
|
||||
lastSavedContent = content;
|
||||
textArea.text = content;
|
||||
contentLoaded = true;
|
||||
syncContentToPlugin();
|
||||
});
|
||||
}
|
||||
|
||||
function saveCurrentTabContent() {
|
||||
if (!currentTab || !contentLoaded) return
|
||||
|
||||
NotepadStorageService.saveTabContent(
|
||||
NotepadStorageService.currentTabIndex,
|
||||
textArea.text
|
||||
)
|
||||
lastSavedContent = textArea.text
|
||||
if (!currentTab || !contentLoaded)
|
||||
return;
|
||||
NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, textArea.text);
|
||||
lastSavedContent = textArea.text;
|
||||
}
|
||||
|
||||
function autoSaveToSession() {
|
||||
if (!currentTab || !contentLoaded) return
|
||||
saveCurrentTabContent()
|
||||
if (!currentTab || !contentLoaded)
|
||||
return;
|
||||
saveCurrentTabContent();
|
||||
}
|
||||
|
||||
function setTextDocumentLineHeight() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
property string lastTextForLineModel: ""
|
||||
@@ -99,147 +90,146 @@ Column {
|
||||
|
||||
function updateLineModel() {
|
||||
if (!SettingsData.notepadShowLineNumbers) {
|
||||
lineModel = []
|
||||
lastTextForLineModel = ""
|
||||
return
|
||||
lineModel = [];
|
||||
lastTextForLineModel = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (textArea.text !== lastTextForLineModel || lineModel.length === 0) {
|
||||
lastTextForLineModel = textArea.text
|
||||
lineModel = textArea.text.split('\n')
|
||||
lastTextForLineModel = textArea.text;
|
||||
lineModel = textArea.text.split('\n');
|
||||
}
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
let matches = []
|
||||
currentMatchIndex = -1
|
||||
let matches = [];
|
||||
currentMatchIndex = -1;
|
||||
|
||||
if (!searchQuery || searchQuery.length === 0) {
|
||||
searchMatches = []
|
||||
matchCount = 0
|
||||
textArea.select(0, 0)
|
||||
return
|
||||
searchMatches = [];
|
||||
matchCount = 0;
|
||||
textArea.select(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = textArea.text
|
||||
const query = searchQuery.toLowerCase()
|
||||
let index = 0
|
||||
const text = textArea.text;
|
||||
const query = searchQuery.toLowerCase();
|
||||
let index = 0;
|
||||
|
||||
while (index < text.length) {
|
||||
const foundIndex = text.toLowerCase().indexOf(query, index)
|
||||
if (foundIndex === -1) break
|
||||
|
||||
const foundIndex = text.toLowerCase().indexOf(query, index);
|
||||
if (foundIndex === -1)
|
||||
break;
|
||||
matches.push({
|
||||
start: foundIndex,
|
||||
end: foundIndex + searchQuery.length
|
||||
})
|
||||
index = foundIndex + 1
|
||||
});
|
||||
index = foundIndex + 1;
|
||||
}
|
||||
|
||||
searchMatches = matches
|
||||
matchCount = matches.length
|
||||
searchMatches = matches;
|
||||
matchCount = matches.length;
|
||||
|
||||
if (matchCount > 0) {
|
||||
currentMatchIndex = 0
|
||||
highlightCurrentMatch()
|
||||
currentMatchIndex = 0;
|
||||
highlightCurrentMatch();
|
||||
} else {
|
||||
textArea.select(0, 0)
|
||||
textArea.select(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function highlightCurrentMatch() {
|
||||
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
|
||||
const match = searchMatches[currentMatchIndex]
|
||||
const match = searchMatches[currentMatchIndex];
|
||||
|
||||
textArea.cursorPosition = match.start
|
||||
textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters)
|
||||
textArea.cursorPosition = match.start;
|
||||
textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters);
|
||||
|
||||
const flickable = textArea.parent
|
||||
const flickable = textArea.parent;
|
||||
if (flickable && flickable.contentY !== undefined) {
|
||||
const lineHeight = textArea.font.pixelSize * 1.5
|
||||
const approxLine = textArea.text.substring(0, match.start).split('\n').length
|
||||
const targetY = approxLine * lineHeight - flickable.height / 2
|
||||
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height))
|
||||
const lineHeight = textArea.font.pixelSize * 1.5;
|
||||
const approxLine = textArea.text.substring(0, match.start).split('\n').length;
|
||||
const targetY = approxLine * lineHeight - flickable.height / 2;
|
||||
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findNext() {
|
||||
if (matchCount === 0 || searchMatches.length === 0) return
|
||||
|
||||
currentMatchIndex = (currentMatchIndex + 1) % matchCount
|
||||
highlightCurrentMatch()
|
||||
if (matchCount === 0 || searchMatches.length === 0)
|
||||
return;
|
||||
currentMatchIndex = (currentMatchIndex + 1) % matchCount;
|
||||
highlightCurrentMatch();
|
||||
}
|
||||
|
||||
function findPrevious() {
|
||||
if (matchCount === 0 || searchMatches.length === 0) return
|
||||
|
||||
currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1
|
||||
highlightCurrentMatch()
|
||||
if (matchCount === 0 || searchMatches.length === 0)
|
||||
return;
|
||||
currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1;
|
||||
highlightCurrentMatch();
|
||||
}
|
||||
|
||||
function showSearch() {
|
||||
searchVisible = true
|
||||
searchVisible = true;
|
||||
Qt.callLater(() => {
|
||||
searchField.forceActiveFocus()
|
||||
})
|
||||
searchField.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
function togglePreview() {
|
||||
if (!inlinePreviewVisible) {
|
||||
inlinePreviewVisible = true
|
||||
previewMode = "split"
|
||||
inlinePreviewVisible = true;
|
||||
previewMode = "split";
|
||||
} else if (previewMode === "split") {
|
||||
previewMode = "full"
|
||||
previewMode = "full";
|
||||
} else {
|
||||
inlinePreviewVisible = false
|
||||
previewMode = "split"
|
||||
inlinePreviewVisible = false;
|
||||
previewMode = "split";
|
||||
}
|
||||
syncContentToPlugin()
|
||||
syncContentToPlugin();
|
||||
}
|
||||
|
||||
function renderPreviewHtml() {
|
||||
if (!inlinePreviewVisible) return ""
|
||||
return pluginHighlightedHtml.length > 0 ? pluginHighlightedHtml : "<p><i>Rendering preview…</i></p>"
|
||||
if (!inlinePreviewVisible)
|
||||
return "";
|
||||
return pluginHighlightedHtml.length > 0 ? pluginHighlightedHtml : "<p><i>Rendering preview…</i></p>";
|
||||
}
|
||||
|
||||
function syncContentToPlugin() {
|
||||
if (!PluginService.isPluginLoaded("dankNotepadModule"))
|
||||
return
|
||||
|
||||
return;
|
||||
if (!currentTab)
|
||||
return
|
||||
|
||||
const filePath = currentTab?.filePath || ""
|
||||
const ext = filePath.split('.').pop().toLowerCase()
|
||||
const content = textArea.text
|
||||
return;
|
||||
const filePath = currentTab?.filePath || "";
|
||||
const ext = filePath.split('.').pop().toLowerCase();
|
||||
const content = textArea.text;
|
||||
|
||||
if (content === lastPluginContent && SettingsData.getBuiltInPluginSetting("dankNotepadModule", "previewActive", false) === inlinePreviewVisible) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
lastPluginContent = content
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "previewActive", inlinePreviewVisible)
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "currentFilePath", filePath)
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "currentFileExtension", ext)
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "sourceContent", content)
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "updatedAt", Date.now())
|
||||
lastPluginContent = content;
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "previewActive", inlinePreviewVisible);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "currentFilePath", filePath);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "currentFileExtension", ext);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "sourceContent", content);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "updatedAt", Date.now());
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
searchVisible = false
|
||||
searchQuery = ""
|
||||
searchMatches = []
|
||||
matchCount = 0
|
||||
currentMatchIndex = -1
|
||||
textArea.select(0, 0)
|
||||
textArea.forceActiveFocus()
|
||||
searchVisible = false;
|
||||
searchQuery = "";
|
||||
searchMatches = [];
|
||||
matchCount = 0;
|
||||
currentMatchIndex = -1;
|
||||
textArea.select(0, 0);
|
||||
textArea.forceActiveFocus();
|
||||
}
|
||||
|
||||
function copyPlainTextToClipboard() {
|
||||
if (!inlinePreviewVisible || !textArea.text) return
|
||||
|
||||
const content = textArea.text
|
||||
if (!inlinePreviewVisible || !textArea.text)
|
||||
return;
|
||||
const content = textArea.text;
|
||||
if (content.length > 0) {
|
||||
const proc = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
@@ -249,22 +239,19 @@ Column {
|
||||
command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"]
|
||||
environment: { "CONTENT": content }
|
||||
running: false
|
||||
}`,
|
||||
root,
|
||||
"copyProc"
|
||||
)
|
||||
proc.content = content
|
||||
proc.running = true
|
||||
}`, root, "copyProc");
|
||||
proc.content = content;
|
||||
proc.running = true;
|
||||
proc.exited.connect(() => {
|
||||
ToastService.showInfo(I18n.tr("Copied to clipboard"))
|
||||
proc.destroy()
|
||||
})
|
||||
ToastService.showInfo(I18n.tr("Copied to clipboard"));
|
||||
proc.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function copyHtmlToClipboard() {
|
||||
if (!inlinePreviewVisible || !pluginHighlightedHtml) return
|
||||
|
||||
if (!inlinePreviewVisible || !pluginHighlightedHtml)
|
||||
return;
|
||||
if (pluginHighlightedHtml.length > 0) {
|
||||
const proc = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
@@ -274,16 +261,13 @@ Column {
|
||||
command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"]
|
||||
environment: { "CONTENT": content }
|
||||
running: false
|
||||
}`,
|
||||
root,
|
||||
"copyProcHtml"
|
||||
)
|
||||
proc.content = pluginHighlightedHtml
|
||||
proc.running = true
|
||||
}`, root, "copyProcHtml");
|
||||
proc.content = pluginHighlightedHtml;
|
||||
proc.running = true;
|
||||
proc.exited.connect(() => {
|
||||
ToastService.showInfo(I18n.tr("HTML copied to clipboard"))
|
||||
proc.destroy()
|
||||
})
|
||||
ToastService.showInfo(I18n.tr("HTML copied to clipboard"));
|
||||
proc.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,43 +318,43 @@ Column {
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: {
|
||||
text = root.searchQuery
|
||||
text = root.searchQuery;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onSearchQueryChanged() {
|
||||
if (searchField.text !== root.searchQuery) {
|
||||
searchField.text = root.searchQuery
|
||||
searchField.text = root.searchQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (root.searchQuery !== text) {
|
||||
root.searchQuery = text
|
||||
root.performSearch()
|
||||
root.searchQuery = text;
|
||||
root.performSearch();
|
||||
}
|
||||
}
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hideSearch()
|
||||
event.accepted = true
|
||||
root.hideSearch();
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onReturnPressed: event => {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
root.findPrevious()
|
||||
root.findPrevious();
|
||||
} else {
|
||||
root.findNext()
|
||||
root.findNext();
|
||||
}
|
||||
event.accepted = true
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onEnterPressed: event => {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
root.findPrevious()
|
||||
root.findPrevious();
|
||||
} else {
|
||||
root.findNext()
|
||||
root.findNext();
|
||||
}
|
||||
event.accepted = true
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,31 +525,41 @@ Column {
|
||||
SequentialAnimation on opacity {
|
||||
running: textArea.activeFocus
|
||||
loops: Animation.Infinite
|
||||
PropertyAnimation { from: 1.0; to: 0.0; duration: 650; easing.type: Easing.InOutQuad }
|
||||
PropertyAnimation { from: 0.0; to: 1.0; duration: 650; easing.type: Easing.InOutQuad }
|
||||
OpacityAnimator {
|
||||
from: 1.0
|
||||
to: 0.0
|
||||
duration: 650
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
OpacityAnimator {
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
duration: 650
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadCurrentTabContent()
|
||||
setTextDocumentLineHeight()
|
||||
root.updateLineModel()
|
||||
loadCurrentTabContent();
|
||||
setTextDocumentLineHeight();
|
||||
root.updateLineModel();
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus()
|
||||
})
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NotepadStorageService
|
||||
function onCurrentTabIndexChanged() {
|
||||
loadCurrentTabContent()
|
||||
loadCurrentTabContent();
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus()
|
||||
})
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
function onTabsChanged() {
|
||||
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {
|
||||
loadCurrentTabContent()
|
||||
loadCurrentTabContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,53 +567,53 @@ Column {
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onNotepadShowLineNumbersChanged() {
|
||||
root.updateLineModel()
|
||||
root.updateLineModel();
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (contentLoaded && text !== lastSavedContent) {
|
||||
autoSaveTimer.restart()
|
||||
autoSaveTimer.restart();
|
||||
}
|
||||
root.contentChanged()
|
||||
root.updateLineModel()
|
||||
pluginSyncTimer.restart()
|
||||
root.contentChanged();
|
||||
root.updateLineModel();
|
||||
pluginSyncTimer.restart();
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: (event) => {
|
||||
root.escapePressed()
|
||||
event.accepted = true
|
||||
Keys.onEscapePressed: event => {
|
||||
root.escapePressed();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
Keys.onPressed: event => {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_S:
|
||||
event.accepted = true
|
||||
root.saveRequested()
|
||||
break
|
||||
event.accepted = true;
|
||||
root.saveRequested();
|
||||
break;
|
||||
case Qt.Key_O:
|
||||
event.accepted = true
|
||||
root.openRequested()
|
||||
break
|
||||
event.accepted = true;
|
||||
root.openRequested();
|
||||
break;
|
||||
case Qt.Key_N:
|
||||
event.accepted = true
|
||||
root.newRequested()
|
||||
break
|
||||
event.accepted = true;
|
||||
root.newRequested();
|
||||
break;
|
||||
case Qt.Key_A:
|
||||
event.accepted = true
|
||||
textArea.selectAll()
|
||||
break
|
||||
event.accepted = true;
|
||||
textArea.selectAll();
|
||||
break;
|
||||
case Qt.Key_F:
|
||||
event.accepted = true
|
||||
root.showSearch()
|
||||
break
|
||||
event.accepted = true;
|
||||
root.showSearch();
|
||||
break;
|
||||
case Qt.Key_P:
|
||||
if (PluginService.isPluginLoaded("dankNotepadModule")) {
|
||||
event.accepted = true
|
||||
root.previewRequested()
|
||||
event.accepted = true;
|
||||
root.previewRequested();
|
||||
}
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -845,19 +839,16 @@ Column {
|
||||
StyledText {
|
||||
text: {
|
||||
const len = textArea.text.length;
|
||||
if (len === 0) return I18n.tr("Empty");
|
||||
return len === 1
|
||||
? I18n.tr("%1 character").arg(len)
|
||||
: I18n.tr("%1 characters").arg(len);
|
||||
if (len === 0)
|
||||
return I18n.tr("Empty");
|
||||
return len === 1 ? I18n.tr("%1 character").arg(len) : I18n.tr("%1 characters").arg(len);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: textArea.lineCount === 1
|
||||
? I18n.tr("Line: %1").arg(textArea.lineCount)
|
||||
: I18n.tr("Lines: %1").arg(textArea.lineCount)
|
||||
text: textArea.lineCount === 1 ? I18n.tr("Line: %1").arg(textArea.lineCount) : I18n.tr("Lines: %1").arg(textArea.lineCount)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
visible: textArea.text.length > 0
|
||||
@@ -867,29 +858,29 @@ Column {
|
||||
StyledText {
|
||||
text: {
|
||||
if (autoSaveTimer.running) {
|
||||
return I18n.tr("Auto-saving...")
|
||||
return I18n.tr("Auto-saving...");
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
if (currentTab && currentTab.isTemporary) {
|
||||
return I18n.tr("Unsaved note...")
|
||||
return I18n.tr("Unsaved note...");
|
||||
} else {
|
||||
return I18n.tr("Unsaved changes")
|
||||
return I18n.tr("Unsaved changes");
|
||||
}
|
||||
} else {
|
||||
return I18n.tr("Saved")
|
||||
return I18n.tr("Saved");
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (autoSaveTimer.running) {
|
||||
return Theme.primary
|
||||
return Theme.primary;
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
return Theme.warning
|
||||
return Theme.warning;
|
||||
} else {
|
||||
return Theme.success
|
||||
return Theme.success;
|
||||
}
|
||||
}
|
||||
opacity: textArea.text.length > 0 ? 1.0 : 0.0
|
||||
@@ -902,7 +893,7 @@ Column {
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
autoSaveToSession()
|
||||
autoSaveToSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -917,7 +908,7 @@ Column {
|
||||
target: SettingsData
|
||||
function onBuiltInPluginSettingsChanged() {
|
||||
if (PluginService.isPluginLoaded("dankNotepadModule")) {
|
||||
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadModule", "highlightedHtml", "")
|
||||
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadModule", "highlightedHtml", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ DankListView {
|
||||
property bool listInitialized: false
|
||||
property int swipingCardIndex: -1
|
||||
property real swipingCardOffset: 0
|
||||
property real __pendingStableHeight: 0
|
||||
property real __heightUpdateThreshold: 20
|
||||
property bool _stableHeightUpdatePending: false
|
||||
readonly property real shadowBlurPx: Theme.elevationEnabled ? ((Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined) ? Theme.elevationLevel1.blurPx : 4) : 0
|
||||
readonly property real shadowHorizontalGutter: Theme.snap(Math.max(Theme.spacingS, Math.min(32, shadowBlurPx * 1.5 + 6)), 1)
|
||||
readonly property real shadowVerticalGutter: Theme.snap(Math.max(Theme.spacingXS, 6), 1)
|
||||
@@ -27,51 +26,52 @@ DankListView {
|
||||
Qt.callLater(() => {
|
||||
if (listView) {
|
||||
listView.listInitialized = true;
|
||||
listView.stableContentHeight = listView.contentHeight;
|
||||
listView.syncStableContentHeight(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: heightUpdateDebounce
|
||||
interval: Theme.mediumDuration + 20
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!listView.isAnimatingExpansion && Math.abs(listView.__pendingStableHeight - listView.stableContentHeight) > listView.__heightUpdateThreshold) {
|
||||
listView.stableContentHeight = listView.__pendingStableHeight;
|
||||
}
|
||||
function targetContentHeight() {
|
||||
if (count <= 0)
|
||||
return contentHeight;
|
||||
|
||||
let total = topMargin + bottomMargin + Math.max(0, count - 1) * spacing;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const item = itemAtIndex(i);
|
||||
if (!item || item.nonAnimHeight === undefined)
|
||||
return contentHeight;
|
||||
total += item.nonAnimHeight;
|
||||
}
|
||||
return Math.max(0, total);
|
||||
}
|
||||
|
||||
function syncStableContentHeight(useTarget) {
|
||||
const nextHeight = useTarget ? targetContentHeight() : contentHeight;
|
||||
if (Math.abs(nextHeight - stableContentHeight) <= 0.5)
|
||||
return;
|
||||
stableContentHeight = nextHeight;
|
||||
}
|
||||
|
||||
function queueStableContentHeightUpdate(useTarget) {
|
||||
if (_stableHeightUpdatePending)
|
||||
return;
|
||||
_stableHeightUpdatePending = true;
|
||||
Qt.callLater(() => {
|
||||
_stableHeightUpdatePending = false;
|
||||
syncStableContentHeight(useTarget || isAnimatingExpansion);
|
||||
});
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (!isAnimatingExpansion) {
|
||||
__pendingStableHeight = contentHeight;
|
||||
if (Math.abs(contentHeight - stableContentHeight) > __heightUpdateThreshold) {
|
||||
heightUpdateDebounce.restart();
|
||||
} else {
|
||||
stableContentHeight = contentHeight;
|
||||
}
|
||||
}
|
||||
if (!isAnimatingExpansion)
|
||||
queueStableContentHeightUpdate(false);
|
||||
}
|
||||
|
||||
onIsAnimatingExpansionChanged: {
|
||||
if (isAnimatingExpansion) {
|
||||
heightUpdateDebounce.stop();
|
||||
let delta = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const item = itemAtIndex(i);
|
||||
if (item && item.children[0] && item.children[0].isAnimating) {
|
||||
const targetDelegateHeight = item.children[0].targetHeight + listView.delegateShadowGutter;
|
||||
delta += targetDelegateHeight - item.height;
|
||||
}
|
||||
}
|
||||
const targetHeight = contentHeight + delta;
|
||||
// During expansion, always update immediately without threshold check
|
||||
stableContentHeight = targetHeight;
|
||||
syncStableContentHeight(true);
|
||||
} else {
|
||||
__pendingStableHeight = contentHeight;
|
||||
heightUpdateDebounce.stop();
|
||||
stableContentHeight = __pendingStableHeight;
|
||||
queueStableContentHeightUpdate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +148,14 @@ DankListView {
|
||||
readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(listView.swipingCardOffset) / width * 0.02 : 1.0
|
||||
readonly property real swipeFadeStartOffset: width * 0.75
|
||||
readonly property real swipeFadeDistance: Math.max(1, width - swipeFadeStartOffset)
|
||||
readonly property real nonAnimHeight: notificationCard.targetHeight + listView.delegateShadowGutter
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
if (delegateRoot)
|
||||
if (delegateRoot) {
|
||||
delegateRoot.__delegateInitialized = true;
|
||||
listView.queueStableContentHeightUpdate(listView.isAnimatingExpansion);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,6 +183,7 @@ DankListView {
|
||||
onIsAnimatingChanged: {
|
||||
if (isAnimating) {
|
||||
listView.isAnimatingExpansion = true;
|
||||
listView.syncStableContentHeight(true);
|
||||
} else {
|
||||
Qt.callLater(() => {
|
||||
if (!notificationCard || !listView)
|
||||
@@ -197,6 +201,13 @@ DankListView {
|
||||
}
|
||||
}
|
||||
|
||||
onTargetHeightChanged: {
|
||||
if (isAnimating || listView.isAnimatingExpansion)
|
||||
listView.syncStableContentHeight(true);
|
||||
else
|
||||
listView.queueStableContentHeightUpdate(false);
|
||||
}
|
||||
|
||||
isGroupSelected: {
|
||||
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive)
|
||||
return false;
|
||||
|
||||
@@ -15,6 +15,13 @@ Rectangle {
|
||||
property bool userInitiatedExpansion: false
|
||||
property bool isAnimating: false
|
||||
property bool animateExpansion: true
|
||||
property bool isDescriptionToggleAnimation: false
|
||||
property bool _retainedExpandedContent: false
|
||||
property bool _clipAnimatedContent: false
|
||||
property real expandedContentOpacity: expanded ? 1 : 0
|
||||
property real collapsedContentOpacity: expanded ? 0 : 1
|
||||
readonly property bool renderExpandedContent: expanded || _retainedExpandedContent
|
||||
readonly property bool renderCollapsedContent: !expanded
|
||||
|
||||
property bool isGroupSelected: false
|
||||
property int selectedNotificationIndex: -1
|
||||
@@ -33,11 +40,12 @@ Rectangle {
|
||||
readonly property real actionButtonHeight: compactMode ? 20 : 24
|
||||
readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
|
||||
readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing
|
||||
readonly property bool connectedFrameMode: SettingsData.connectedFrameModeActive
|
||||
|
||||
width: parent ? parent.width : 400
|
||||
height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
|
||||
readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
|
||||
radius: Theme.cornerRadius
|
||||
radius: connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence
|
||||
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !BlurService.enabled
|
||||
readonly property var shadowElevation: Theme.elevationLevel1
|
||||
@@ -55,6 +63,16 @@ Rectangle {
|
||||
});
|
||||
}
|
||||
|
||||
function expansionMotionDuration() {
|
||||
if (isDescriptionToggleAnimation)
|
||||
return descriptionExpanded ? Theme.notificationInlineExpandDuration : Theme.notificationInlineCollapseDuration;
|
||||
return Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded);
|
||||
}
|
||||
|
||||
function expansionMotionCurve() {
|
||||
return root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve;
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: listLevelScaleAnimationsEnabled
|
||||
NumberAnimation {
|
||||
@@ -64,6 +82,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Behavior on shadowBlurPx {
|
||||
enabled: !root.connectedFrameMode
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
@@ -71,6 +90,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Behavior on shadowOffsetXPx {
|
||||
enabled: !root.connectedFrameMode
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
@@ -78,6 +98,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Behavior on shadowOffsetYPx {
|
||||
enabled: !root.connectedFrameMode
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
@@ -92,6 +113,24 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on expandedContentOpacity {
|
||||
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
|
||||
NumberAnimation {
|
||||
duration: root.expansionMotionDuration()
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.expansionMotionCurve()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on collapsedContentOpacity {
|
||||
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
|
||||
NumberAnimation {
|
||||
duration: root.expansionMotionDuration()
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.expansionMotionCurve()
|
||||
}
|
||||
}
|
||||
|
||||
color: {
|
||||
if (isGroupSelected && keyboardNavigationActive) {
|
||||
return Theme.primaryPressed;
|
||||
@@ -125,7 +164,31 @@ Rectangle {
|
||||
}
|
||||
return Theme.layerOutlineWidth;
|
||||
}
|
||||
clip: false
|
||||
clip: _clipAnimatedContent
|
||||
|
||||
onExpandedChanged: {
|
||||
if (__initialized && userInitiatedExpansion && animateExpansion)
|
||||
_clipAnimatedContent = true;
|
||||
if (expanded) {
|
||||
_retainedExpandedContent = false;
|
||||
return;
|
||||
}
|
||||
if (__initialized && userInitiatedExpansion && animateExpansion)
|
||||
_retainedExpandedContent = true;
|
||||
}
|
||||
|
||||
onHeightChanged: {
|
||||
if (Math.abs(height - targetHeight) > 0.5)
|
||||
return;
|
||||
_clipAnimatedContent = false;
|
||||
if (!expanded && _retainedExpandedContent)
|
||||
_retainedExpandedContent = false;
|
||||
}
|
||||
|
||||
onExpandedContentOpacityChanged: {
|
||||
if (!expanded && _retainedExpandedContent && expandedContentOpacity <= 0.01)
|
||||
_retainedExpandedContent = false;
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: cardHoverHandler
|
||||
@@ -145,7 +208,7 @@ Rectangle {
|
||||
shadowOffsetX: root.shadowOffsetXPx
|
||||
shadowOffsetY: root.shadowOffsetYPx
|
||||
shadowColor: root.shadowElevation ? Theme.elevationShadowColor(root.shadowElevation) : "transparent"
|
||||
shadowEnabled: root.shadowsAllowed
|
||||
shadowEnabled: root.shadowsAllowed && !root.connectedFrameMode
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -185,7 +248,8 @@ Rectangle {
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin
|
||||
height: collapsedContentHeight + extraHeight
|
||||
visible: !expanded
|
||||
visible: renderCollapsedContent
|
||||
opacity: root.collapsedContentOpacity
|
||||
|
||||
DankCircularImage {
|
||||
id: iconContainer
|
||||
@@ -348,6 +412,7 @@ Rectangle {
|
||||
onClicked: mouse => {
|
||||
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
|
||||
root.userInitiatedExpansion = true;
|
||||
root.isDescriptionToggleAnimation = true;
|
||||
const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : "";
|
||||
NotificationService.toggleMessageExpansion(messageId);
|
||||
Qt.callLater(() => {
|
||||
@@ -357,7 +422,7 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
propagateComposedEvents: true
|
||||
propagateComposedEvents: false
|
||||
onPressed: mouse => {
|
||||
if (parent.hoveredLink)
|
||||
mouse.accepted = false;
|
||||
@@ -382,7 +447,8 @@ Rectangle {
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: compactMode ? Theme.spacingXS : Theme.spacingS
|
||||
visible: expanded
|
||||
visible: renderExpandedContent
|
||||
opacity: root.expandedContentOpacity
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
@@ -513,7 +579,12 @@ Rectangle {
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: false
|
||||
enabled: expandedDelegateWrapper.__delegateInitialized && root.animateExpansion && root.userInitiatedExpansion
|
||||
NumberAnimation {
|
||||
duration: root.expansionMotionDuration()
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.expansionMotionCurve()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -650,6 +721,7 @@ Rectangle {
|
||||
onClicked: mouse => {
|
||||
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
|
||||
root.userInitiatedExpansion = true;
|
||||
root.isDescriptionToggleAnimation = true;
|
||||
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "");
|
||||
Qt.callLater(() => {
|
||||
if (root && !root.isAnimating)
|
||||
@@ -658,7 +730,7 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
propagateComposedEvents: true
|
||||
propagateComposedEvents: false
|
||||
onPressed: mouse => {
|
||||
if (parent.hoveredLink) {
|
||||
mouse.accepted = false;
|
||||
@@ -825,7 +897,8 @@ Rectangle {
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: !expanded
|
||||
visible: renderCollapsedContent
|
||||
opacity: root.collapsedContentOpacity
|
||||
anchors.right: clearButton.visible ? clearButton.left : parent.right
|
||||
anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL
|
||||
anchors.top: collapsedContent.bottom
|
||||
@@ -882,7 +955,8 @@ Rectangle {
|
||||
property bool isHovered: false
|
||||
readonly property int actionCount: (notificationGroup?.latestNotification?.actions || []).length
|
||||
|
||||
visible: !expanded && actionCount < 3
|
||||
visible: renderCollapsedContent && actionCount < 3
|
||||
opacity: root.collapsedContentOpacity
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.top: collapsedContent.bottom
|
||||
@@ -913,10 +987,11 @@ Rectangle {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
|
||||
visible: renderCollapsedContent && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.userInitiatedExpansion = true;
|
||||
root.isDescriptionToggleAnimation = false;
|
||||
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
|
||||
}
|
||||
z: -1
|
||||
@@ -940,6 +1015,7 @@ Rectangle {
|
||||
buttonSize: compactMode ? 24 : 28
|
||||
onClicked: {
|
||||
root.userInitiatedExpansion = true;
|
||||
root.isDescriptionToggleAnimation = false;
|
||||
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
|
||||
}
|
||||
}
|
||||
@@ -957,15 +1033,18 @@ Rectangle {
|
||||
Behavior on height {
|
||||
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
|
||||
NumberAnimation {
|
||||
duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
|
||||
duration: root.expansionMotionDuration()
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: root.expansionMotionCurve()
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
root.isAnimating = true;
|
||||
} else {
|
||||
root.isAnimating = false;
|
||||
root.userInitiatedExpansion = false;
|
||||
root.isDescriptionToggleAnimation = false;
|
||||
root._retainedExpandedContent = false;
|
||||
root._clipAnimatedContent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ DankPopout {
|
||||
property real stablePopupHeight: 400
|
||||
property real _lastAlignedContentHeight: -1
|
||||
property bool _pendingSizedOpen: false
|
||||
property bool _heightUpdatePending: false
|
||||
|
||||
function updateStablePopupHeight() {
|
||||
const item = contentLoader.item;
|
||||
@@ -30,6 +31,16 @@ DankPopout {
|
||||
stablePopupHeight = target;
|
||||
}
|
||||
|
||||
function queueStablePopupHeightUpdate() {
|
||||
if (_heightUpdatePending)
|
||||
return;
|
||||
_heightUpdatePending = true;
|
||||
Qt.callLater(() => {
|
||||
_heightUpdatePending = false;
|
||||
updateStablePopupHeight();
|
||||
});
|
||||
}
|
||||
|
||||
NotificationKeyboardController {
|
||||
id: keyboardController
|
||||
listView: null
|
||||
@@ -39,11 +50,9 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
|
||||
popupWidth: 400
|
||||
popupHeight: stablePopupHeight
|
||||
positioning: ""
|
||||
animationScaleCollapsed: 0.94
|
||||
animationOffset: 0
|
||||
suspendShadowWhileResizing: false
|
||||
|
||||
screen: triggerScreen
|
||||
@@ -130,7 +139,7 @@ DankPopout {
|
||||
Connections {
|
||||
target: contentLoader.item
|
||||
function onImplicitHeightChanged() {
|
||||
root.updateStablePopupHeight();
|
||||
root.queueStablePopupHeightUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,327 +123,327 @@ Rectangle {
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Notification Settings")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(dndRow.implicitHeight, dndToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: dndRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Do Not Disturb")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
StyledText {
|
||||
text: I18n.tr("Notification Settings")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: dndToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SessionData.doNotDisturb
|
||||
onToggled: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||
}
|
||||
}
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(dndRow.implicitHeight, dndToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
Row {
|
||||
id: dndRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Notification Timeouts")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
text: I18n.tr("Low Priority")
|
||||
description: I18n.tr("Timeout for low priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutLow", timeoutOptions[i].value);
|
||||
break;
|
||||
DankIcon {
|
||||
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
text: I18n.tr("Normal Priority")
|
||||
description: I18n.tr("Timeout for normal priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutNormal", timeoutOptions[i].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
text: I18n.tr("Critical Priority")
|
||||
description: I18n.tr("Timeout for critical priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutCritical", timeoutOptions[i].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(overlayRow.implicitHeight, overlayToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: overlayRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: overlayToggle.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "notifications_active"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationOverlayEnabled ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: overlayRow.width - Theme.iconSizeSmall - Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Notification Overlay")
|
||||
text: I18n.tr("Do Not Disturb")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.Wrap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Display all priorities over fullscreen apps")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.Wrap
|
||||
DankToggle {
|
||||
id: dndToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SessionData.doNotDisturb
|
||||
onToggled: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Notification Timeouts")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
text: I18n.tr("Low Priority")
|
||||
description: I18n.tr("Timeout for low priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutLow", timeoutOptions[i].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: overlayToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationOverlayEnabled
|
||||
onToggled: toggled => SettingsData.set("notificationOverlayEnabled", toggled)
|
||||
DankDropdown {
|
||||
text: I18n.tr("Normal Priority")
|
||||
description: I18n.tr("Timeout for normal priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutNormal", timeoutOptions[i].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(privacyRow.implicitHeight, privacyToggle.implicitHeight) + Theme.spacingS
|
||||
DankDropdown {
|
||||
text: I18n.tr("Critical Priority")
|
||||
description: I18n.tr("Timeout for critical priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let i = 0; i < timeoutOptions.length; i++) {
|
||||
if (timeoutOptions[i].text === value) {
|
||||
SettingsData.set("notificationTimeoutCritical", timeoutOptions[i].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: privacyRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: privacyToggle.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "privacy_tip"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationPopupPrivacyMode ? Theme.primary : Theme.surfaceText
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(overlayRow.implicitHeight, overlayToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: overlayRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: overlayToggle.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "notifications_active"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationOverlayEnabled ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: overlayRow.width - Theme.iconSizeSmall - Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Notification Overlay")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Display all priorities over fullscreen apps")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
DankToggle {
|
||||
id: overlayToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: privacyRow.width - Theme.iconSizeSmall - Theme.spacingM
|
||||
checked: SettingsData.notificationOverlayEnabled
|
||||
onToggled: toggled => SettingsData.set("notificationOverlayEnabled", toggled)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(privacyRow.implicitHeight, privacyToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: privacyRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: privacyToggle.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "privacy_tip"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationPopupPrivacyMode ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: privacyRow.width - Theme.iconSizeSmall - Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Privacy Mode")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Hide notification content until expanded")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: privacyToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationPopupPrivacyMode
|
||||
onToggled: toggled => SettingsData.set("notificationPopupPrivacyMode", toggled)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("History Settings")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(lowRow.implicitHeight, lowToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: lowRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "low_priority"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationHistorySaveLow ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Privacy Mode")
|
||||
text: I18n.tr("Low Priority")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.Wrap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: lowToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationHistorySaveLow
|
||||
onToggled: toggled => SettingsData.set("notificationHistorySaveLow", toggled)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(normalRow.implicitHeight, normalToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: normalRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "notifications"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationHistorySaveNormal ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Hide notification content until expanded")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.Wrap
|
||||
text: I18n.tr("Normal Priority")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: privacyToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationPopupPrivacyMode
|
||||
onToggled: toggled => SettingsData.set("notificationPopupPrivacyMode", toggled)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("History Settings")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(lowRow.implicitHeight, lowToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: lowRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "low_priority"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationHistorySaveLow ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Low Priority")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
DankToggle {
|
||||
id: normalToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationHistorySaveNormal
|
||||
onToggled: toggled => SettingsData.set("notificationHistorySaveNormal", toggled)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: lowToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationHistorySaveLow
|
||||
onToggled: toggled => SettingsData.set("notificationHistorySaveLow", toggled)
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(criticalRow.implicitHeight, criticalToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: criticalRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "priority_high"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationHistorySaveCritical ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Critical Priority")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: criticalToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationHistorySaveCritical
|
||||
onToggled: toggled => SettingsData.set("notificationHistorySaveCritical", toggled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(normalRow.implicitHeight, normalToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: normalRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "notifications"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationHistorySaveNormal ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Normal Priority")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: normalToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationHistorySaveNormal
|
||||
onToggled: toggled => SettingsData.set("notificationHistorySaveNormal", toggled)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(criticalRow.implicitHeight, criticalToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: criticalRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "priority_high"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationHistorySaveCritical ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Critical Priority")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: criticalToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationHistorySaveCritical
|
||||
onToggled: toggled => SettingsData.set("notificationHistorySaveCritical", toggled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,40 @@ import qs.Widgets
|
||||
PanelWindow {
|
||||
id: win
|
||||
|
||||
readonly property bool connectedFrameMode: SettingsData.frameEnabled && Theme.isConnectedEffect && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
|
||||
readonly property string notifBarSide: {
|
||||
const pos = SettingsData.notificationPopupPosition;
|
||||
if (pos === -1)
|
||||
return "top";
|
||||
switch (pos) {
|
||||
case SettingsData.Position.Top:
|
||||
return "right";
|
||||
case SettingsData.Position.Left:
|
||||
return "left";
|
||||
case SettingsData.Position.BottomCenter:
|
||||
return "bottom";
|
||||
case SettingsData.Position.Right:
|
||||
return "right";
|
||||
case SettingsData.Position.Bottom:
|
||||
return "left";
|
||||
default:
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
readonly property int inlineExpandDuration: Theme.notificationInlineExpandDuration
|
||||
readonly property int inlineCollapseDuration: Theme.notificationInlineCollapseDuration
|
||||
property bool inlineHeightAnimating: false
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: win
|
||||
blurX: content.x + content.cardInset + swipeTx.x + tx.x
|
||||
blurY: content.y + content.cardInset + swipeTx.y + tx.y
|
||||
blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
|
||||
blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
readonly property real s: Math.min(1, content.scale) * Math.max(0, content.opacity)
|
||||
readonly property real innerW: Math.max(0, content.width - content.cardInset * 2)
|
||||
readonly property real innerH: Math.max(0, content.height - content.cardInset * 2)
|
||||
blurX: content.x + content.cardInset + swipeTx.x + tx.x + innerW * (1 - s) * 0.5
|
||||
blurY: content.y + content.cardInset + swipeTx.y + tx.y + innerH * (1 - s) * 0.5
|
||||
blurWidth: !win._finalized && !win.connectedFrameMode ? innerW * s : 0
|
||||
blurHeight: !win._finalized && !win.connectedFrameMode ? innerH * s : 0
|
||||
blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:notification-popup"
|
||||
@@ -25,6 +52,15 @@ PanelWindow {
|
||||
required property string notificationId
|
||||
readonly property bool hasValidData: notificationData && notificationData.notification
|
||||
readonly property alias hovered: cardHoverHandler.hovered
|
||||
readonly property alias swipeActive: content.swipeActive
|
||||
readonly property alias swipeDismissing: content.swipeDismissing
|
||||
readonly property bool swipeDismissTowardEdge: {
|
||||
if (content.swipeDismissing)
|
||||
return _swipeDismissesTowardFrameEdge();
|
||||
if (content.swipeActive)
|
||||
return content.swipeOffset * _frameEdgeSwipeDirection() > 0;
|
||||
return false;
|
||||
}
|
||||
property int screenY: 0
|
||||
property bool exiting: false
|
||||
property bool _isDestroying: false
|
||||
@@ -32,18 +68,36 @@ PanelWindow {
|
||||
property real _lastReportedAlignedHeight: -1
|
||||
property real _storedTopMargin: 0
|
||||
property real _storedBottomMargin: 0
|
||||
property bool _inlineGeometryReady: false
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real entryTravel: {
|
||||
const base = Math.abs(Theme.effectAnimOffset);
|
||||
if (directionalEffect) {
|
||||
if (isCenterPosition)
|
||||
return Math.max(base, Math.round(content.height * 1.1));
|
||||
return Math.max(base, Math.round(content.width * 0.95));
|
||||
}
|
||||
if (depthEffect)
|
||||
return Math.max(base, 44);
|
||||
return base;
|
||||
}
|
||||
readonly property real exitTravel: {
|
||||
if (directionalEffect) {
|
||||
if (isCenterPosition)
|
||||
return Math.max(1, content.height);
|
||||
return Math.max(1, content.width);
|
||||
}
|
||||
if (depthEffect)
|
||||
return Math.round(entryTravel * 1.35);
|
||||
return Anims.slidePx;
|
||||
}
|
||||
readonly property string clearText: I18n.tr("Dismiss")
|
||||
property bool descriptionExpanded: false
|
||||
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
|
||||
onDescriptionExpandedChanged: {
|
||||
popupHeightChanged();
|
||||
}
|
||||
onImplicitHeightChanged: {
|
||||
const aligned = Theme.px(implicitHeight, dpr);
|
||||
if (Math.abs(aligned - _lastReportedAlignedHeight) < 0.5)
|
||||
return;
|
||||
_lastReportedAlignedHeight = aligned;
|
||||
popupHeightChanged();
|
||||
if (connectedFrameMode)
|
||||
popupChromeGeometryChanged();
|
||||
}
|
||||
|
||||
readonly property bool compactMode: SettingsData.notificationCompactMode
|
||||
@@ -61,6 +115,7 @@ PanelWindow {
|
||||
signal exitStarted
|
||||
signal exitFinished
|
||||
signal popupHeightChanged
|
||||
signal popupChromeGeometryChanged
|
||||
|
||||
function startExit() {
|
||||
if (exiting || _isDestroying) {
|
||||
@@ -68,6 +123,7 @@ PanelWindow {
|
||||
}
|
||||
exiting = true;
|
||||
exitStarted();
|
||||
popupChromeGeometryChanged();
|
||||
exitAnim.restart();
|
||||
exitWatchdog.restart();
|
||||
if (NotificationService.removeFromVisibleNotifications)
|
||||
@@ -132,22 +188,84 @@ PanelWindow {
|
||||
return basePopupHeightPrivacy;
|
||||
if (!descriptionExpanded)
|
||||
return basePopupHeight;
|
||||
const bodyTextHeight = bodyText.contentHeight || 0;
|
||||
const bodyTextHeight = expandedBodyMeasure.contentHeight || bodyText.contentHeight || 0;
|
||||
const collapsedBodyHeight = Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2);
|
||||
if (bodyTextHeight > collapsedBodyHeight + 2)
|
||||
return basePopupHeight + bodyTextHeight - collapsedBodyHeight;
|
||||
return basePopupHeight;
|
||||
}
|
||||
readonly property real targetAlignedHeight: Theme.px(Math.max(0, contentImplicitHeight), dpr)
|
||||
property real renderedAlignedHeight: targetAlignedHeight
|
||||
property real allocatedAlignedHeight: targetAlignedHeight
|
||||
readonly property bool inlineGeometryGrowing: targetAlignedHeight >= renderedAlignedHeight
|
||||
readonly property bool contentAnchorsTop: isTopCenter || SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Left
|
||||
readonly property real renderedContentOffsetY: contentAnchorsTop ? 0 : Math.max(0, allocatedAlignedHeight - renderedAlignedHeight)
|
||||
implicitWidth: contentImplicitWidth + (windowShadowPad * 2)
|
||||
implicitHeight: contentImplicitHeight + (windowShadowPad * 2)
|
||||
implicitHeight: allocatedAlignedHeight + (windowShadowPad * 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: !exiting && !_isDestroying
|
||||
function inlineMotionDuration(growing) {
|
||||
return growing ? inlineExpandDuration : inlineCollapseDuration;
|
||||
}
|
||||
|
||||
function syncInlineTargetHeight() {
|
||||
const target = Math.max(0, Number(targetAlignedHeight));
|
||||
if (isNaN(target))
|
||||
return;
|
||||
|
||||
if (!_inlineGeometryReady) {
|
||||
renderedHeightAnim.stop();
|
||||
renderedAlignedHeight = target;
|
||||
allocatedAlignedHeight = target;
|
||||
_lastReportedAlignedHeight = target;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRendered = Math.max(0, Number(renderedAlignedHeight));
|
||||
const nextAllocation = Math.max(target, currentRendered, allocatedAlignedHeight);
|
||||
if (Math.abs(nextAllocation - allocatedAlignedHeight) >= 0.5)
|
||||
allocatedAlignedHeight = nextAllocation;
|
||||
|
||||
if (Math.abs(target - renderedAlignedHeight) < 0.5) {
|
||||
finishInlineHeightAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
renderedAlignedHeight = target;
|
||||
if (connectedFrameMode)
|
||||
popupChromeGeometryChanged();
|
||||
if (inlineMotionDuration(target >= currentRendered) <= 0)
|
||||
Qt.callLater(() => finishInlineHeightAnimation());
|
||||
}
|
||||
|
||||
function finishInlineHeightAnimation() {
|
||||
const target = Math.max(0, Number(targetAlignedHeight));
|
||||
if (isNaN(target))
|
||||
return;
|
||||
if (Math.abs(renderedAlignedHeight - target) >= 0.5)
|
||||
renderedAlignedHeight = target;
|
||||
if (Math.abs(allocatedAlignedHeight - target) >= 0.5)
|
||||
allocatedAlignedHeight = target;
|
||||
_lastReportedAlignedHeight = renderedAlignedHeight;
|
||||
popupHeightChanged();
|
||||
if (connectedFrameMode)
|
||||
popupChromeGeometryChanged();
|
||||
}
|
||||
|
||||
onTargetAlignedHeightChanged: syncInlineTargetHeight()
|
||||
onAllocatedAlignedHeightChanged: {
|
||||
if (connectedFrameMode)
|
||||
popupChromeGeometryChanged();
|
||||
}
|
||||
|
||||
Behavior on renderedAlignedHeight {
|
||||
enabled: !win.exiting && !win._isDestroying
|
||||
NumberAnimation {
|
||||
id: implicitHeightAnim
|
||||
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
|
||||
id: renderedHeightAnim
|
||||
duration: win.inlineMotionDuration(win.inlineGeometryGrowing)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: win.inlineGeometryGrowing ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
onRunningChanged: win.inlineHeightAnimating = running
|
||||
onFinished: win.finishInlineHeightAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +275,11 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
_lastReportedAlignedHeight = Theme.px(implicitHeight, dpr);
|
||||
renderedHeightAnim.stop();
|
||||
renderedAlignedHeight = targetAlignedHeight;
|
||||
allocatedAlignedHeight = targetAlignedHeight;
|
||||
_inlineGeometryReady = true;
|
||||
_lastReportedAlignedHeight = renderedAlignedHeight;
|
||||
_storedTopMargin = getTopMargin();
|
||||
_storedBottomMargin = getBottomMargin();
|
||||
if (SettingsData.notificationPopupPrivacyMode)
|
||||
@@ -195,7 +317,8 @@ PanelWindow {
|
||||
readonly property real maxPopupShadowBlurPx: Math.max((Theme.elevationLevel3 && Theme.elevationLevel3.blurPx !== undefined) ? Theme.elevationLevel3.blurPx : 12, (Theme.elevationLevel4 && Theme.elevationLevel4.blurPx !== undefined) ? Theme.elevationLevel4.blurPx : 16)
|
||||
readonly property real maxPopupShadowOffsetXPx: Math.max(Math.abs(Theme.elevationOffsetX(Theme.elevationLevel3)), Math.abs(Theme.elevationOffsetX(Theme.elevationLevel4)))
|
||||
readonly property real maxPopupShadowOffsetYPx: Math.max(Math.abs(Theme.elevationOffsetY(Theme.elevationLevel3, 6)), Math.abs(Theme.elevationOffsetY(Theme.elevationLevel4, 8)))
|
||||
readonly property real windowShadowPad: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled ? Theme.snap(Math.max(16, maxPopupShadowBlurPx + Math.max(maxPopupShadowOffsetXPx, maxPopupShadowOffsetYPx) + 8), dpr) : 0
|
||||
readonly property bool popupWindowShadowActive: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled && !connectedFrameMode
|
||||
readonly property real windowShadowPad: popupWindowShadowActive ? Theme.snap(Math.max(16, maxPopupShadowBlurPx + Math.max(maxPopupShadowOffsetXPx, maxPopupShadowOffsetYPx) + 8), dpr) : 0
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
@@ -240,12 +363,32 @@ PanelWindow {
|
||||
});
|
||||
}
|
||||
|
||||
function _frameEdgeInset(side) {
|
||||
if (!screen)
|
||||
return 0;
|
||||
const raw = SettingsData.frameEdgeInsetForSide(screen, side);
|
||||
return Math.max(0, Math.round(Theme.px(raw, dpr)));
|
||||
}
|
||||
|
||||
readonly property bool frameOnlyNoConnected: SettingsData.frameEnabled && !connectedFrameMode && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences)
|
||||
|
||||
// Frame ON + Connected OFF. frameEdgeInset is the full bar/frame inset
|
||||
function _frameGapMargin(side) {
|
||||
return _frameEdgeInset(side) + Theme.popupDistance;
|
||||
}
|
||||
|
||||
function getTopMargin() {
|
||||
const popupPos = SettingsData.notificationPopupPosition;
|
||||
const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
|
||||
if (!isTop)
|
||||
return 0;
|
||||
|
||||
if (connectedFrameMode) {
|
||||
const cornerClear = (isCenterPosition || SettingsData.frameCloseGaps) ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
|
||||
return _frameEdgeInset("top") + cornerClear + screenY;
|
||||
}
|
||||
if (frameOnlyNoConnected)
|
||||
return _frameGapMargin("top") + screenY;
|
||||
const barInfo = getBarInfo();
|
||||
const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance;
|
||||
return base + screenY;
|
||||
@@ -257,6 +400,12 @@ PanelWindow {
|
||||
if (!isBottom)
|
||||
return 0;
|
||||
|
||||
if (connectedFrameMode) {
|
||||
const cornerClear = (isCenterPosition || SettingsData.frameCloseGaps) ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
|
||||
return _frameEdgeInset("bottom") + cornerClear + screenY;
|
||||
}
|
||||
if (frameOnlyNoConnected)
|
||||
return _frameGapMargin("bottom") + screenY;
|
||||
const barInfo = getBarInfo();
|
||||
const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
|
||||
return base + screenY;
|
||||
@@ -271,6 +420,10 @@ PanelWindow {
|
||||
if (!isLeft)
|
||||
return 0;
|
||||
|
||||
if (connectedFrameMode)
|
||||
return _frameEdgeInset("left");
|
||||
if (frameOnlyNoConnected)
|
||||
return _frameGapMargin("left");
|
||||
const barInfo = getBarInfo();
|
||||
return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
|
||||
}
|
||||
@@ -284,6 +437,10 @@ PanelWindow {
|
||||
if (!isRight)
|
||||
return 0;
|
||||
|
||||
if (connectedFrameMode)
|
||||
return _frameEdgeInset("right");
|
||||
if (frameOnlyNoConnected)
|
||||
return _frameGapMargin("right");
|
||||
const barInfo = getBarInfo();
|
||||
return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
|
||||
}
|
||||
@@ -303,7 +460,7 @@ PanelWindow {
|
||||
return Theme.snap(screen.width - alignedWidth - barRight, dpr);
|
||||
}
|
||||
|
||||
function getContentY() {
|
||||
function getAllocatedContentY() {
|
||||
if (!screen)
|
||||
return 0;
|
||||
|
||||
@@ -313,7 +470,11 @@ PanelWindow {
|
||||
const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
|
||||
if (isTop)
|
||||
return Theme.snap(barTop, dpr);
|
||||
return Theme.snap(screen.height - alignedHeight - barBottom, dpr);
|
||||
return Theme.snap(screen.height - allocatedAlignedHeight - barBottom, dpr);
|
||||
}
|
||||
|
||||
function getContentY() {
|
||||
return Theme.snap(getAllocatedContentY() + renderedContentOffsetY, dpr);
|
||||
}
|
||||
|
||||
function getWindowLeftMargin() {
|
||||
@@ -325,23 +486,107 @@ PanelWindow {
|
||||
function getWindowTopMargin() {
|
||||
if (!screen)
|
||||
return 0;
|
||||
return Theme.snap(getContentY() - windowShadowPad, dpr);
|
||||
return Theme.snap(getAllocatedContentY() - windowShadowPad, dpr);
|
||||
}
|
||||
|
||||
function _swipeDismissTarget() {
|
||||
return (content.swipeDismissDirection < 0 ? -1 : 1) * content.width;
|
||||
}
|
||||
|
||||
function _frameEdgeSwipeDirection() {
|
||||
const popupPos = SettingsData.notificationPopupPosition;
|
||||
return (popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom) ? -1 : 1;
|
||||
}
|
||||
|
||||
function _swipeDismissesTowardFrameEdge() {
|
||||
return content.swipeDismissDirection === _frameEdgeSwipeDirection();
|
||||
}
|
||||
|
||||
function popupChromeMotionActive() {
|
||||
return popupChromeOpenProgress() < 1 || exiting || content.swipeActive || content.swipeDismissing || Math.abs(content.swipeOffset) > 0.5;
|
||||
}
|
||||
|
||||
function popupLayoutReservesSlot() {
|
||||
return !content.swipeDismissing;
|
||||
}
|
||||
|
||||
function popupChromeReservesSlot() {
|
||||
return !content.swipeDismissing;
|
||||
}
|
||||
|
||||
function _chromeMotionOffset() {
|
||||
return isCenterPosition ? tx.y : tx.x;
|
||||
}
|
||||
|
||||
function _chromeCardTravel() {
|
||||
return Math.max(1, isCenterPosition ? alignedHeight : alignedWidth);
|
||||
}
|
||||
|
||||
function popupChromeOpenProgress() {
|
||||
if (exiting || content.swipeDismissing)
|
||||
return 1;
|
||||
return Math.max(0, Math.min(1, 1 - Math.abs(_chromeMotionOffset()) / _chromeCardTravel()));
|
||||
}
|
||||
|
||||
function popupChromeReleaseProgress() {
|
||||
if (exiting) {
|
||||
const exitRel = Math.max(0, Math.min(1, Math.abs(_chromeMotionOffset()) / _chromeCardTravel()));
|
||||
if (content.swipeDismissing) {
|
||||
const swipeRel = Math.max(0, Math.min(1, Math.abs(content.swipeOffset) / Math.max(1, content.swipeTravelDistance)));
|
||||
return Math.max(exitRel, swipeRel);
|
||||
}
|
||||
return exitRel;
|
||||
}
|
||||
if (content.swipeDismissing)
|
||||
return Math.max(0, Math.min(1, Math.abs(content.swipeOffset) / Math.max(1, content.swipeTravelDistance)));
|
||||
if (content.swipeActive && content.swipeOffset * _frameEdgeSwipeDirection() > 0)
|
||||
return Math.max(0, Math.min(1, Math.abs(content.swipeOffset) / Math.max(1, content.swipeTravelDistance)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
function popupChromeFollowsCardMotion() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function popupChromeMotionX() {
|
||||
if (!popupChromeMotionActive() || isCenterPosition)
|
||||
return 0;
|
||||
const motion = content.swipeOffset + tx.x;
|
||||
if (content.swipeDismissing && !_swipeDismissesTowardFrameEdge())
|
||||
return exiting ? Theme.snap(tx.x, dpr) : 0;
|
||||
if (content.swipeActive && motion * _frameEdgeSwipeDirection() < 0)
|
||||
return 0;
|
||||
return Theme.snap(motion, dpr);
|
||||
}
|
||||
|
||||
function popupChromeMotionY() {
|
||||
return popupChromeMotionActive() ? Theme.snap(tx.y, dpr) : 0;
|
||||
}
|
||||
|
||||
readonly property bool screenValid: win.screen && !_isDestroying
|
||||
readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1
|
||||
readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr)
|
||||
readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr)
|
||||
readonly property real alignedHeight: renderedAlignedHeight
|
||||
onScreenYChanged: if (connectedFrameMode)
|
||||
popupChromeGeometryChanged()
|
||||
onScreenChanged: if (connectedFrameMode)
|
||||
popupChromeGeometryChanged()
|
||||
// Intentionally unconditional: Manager needs the signal when frame mode toggles off
|
||||
onConnectedFrameModeChanged: popupChromeGeometryChanged()
|
||||
onAlignedWidthChanged: if (connectedFrameMode)
|
||||
popupChromeGeometryChanged()
|
||||
onAlignedHeightChanged: if (connectedFrameMode)
|
||||
popupChromeGeometryChanged()
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
x: Theme.snap(windowShadowPad, dpr)
|
||||
y: Theme.snap(windowShadowPad, dpr)
|
||||
y: Theme.snap(windowShadowPad + renderedContentOffsetY, dpr)
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
visible: !win._finalized
|
||||
scale: cardHoverHandler.hovered ? 1.01 : 1.0
|
||||
visible: !win._finalized && !chromeOnlyExit
|
||||
scale: (!win.inlineHeightAnimating && cardHoverHandler.hovered) ? 1.01 : 1.0
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on scale {
|
||||
@@ -352,15 +597,27 @@ PanelWindow {
|
||||
}
|
||||
|
||||
property real swipeOffset: 0
|
||||
readonly property real dismissThreshold: isCenterPosition ? height * 0.4 : width * 0.35
|
||||
property real swipeDismissDirection: 1
|
||||
property bool chromeOnlyExit: false
|
||||
readonly property real dismissThreshold: width * 0.35
|
||||
readonly property real swipeFadeStartRatio: 0.75
|
||||
readonly property real swipeTravelDistance: isCenterPosition ? height : width
|
||||
readonly property real swipeTravelDistance: width
|
||||
readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio
|
||||
readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset)
|
||||
readonly property bool swipeActive: swipeDragHandler.active
|
||||
property bool swipeDismissing: false
|
||||
onSwipeDismissingChanged: {
|
||||
if (!win.connectedFrameMode)
|
||||
return;
|
||||
win.popupHeightChanged();
|
||||
win.popupChromeGeometryChanged();
|
||||
}
|
||||
onSwipeOffsetChanged: {
|
||||
if (win.connectedFrameMode)
|
||||
win.popupChromeGeometryChanged();
|
||||
}
|
||||
|
||||
readonly property bool shadowsAllowed: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled && !BlurService.enabled
|
||||
readonly property bool shadowsAllowed: win.popupWindowShadowActive
|
||||
readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3
|
||||
readonly property real cardInset: Theme.snap(4, win.dpr)
|
||||
readonly property real shadowRenderPadding: shadowsAllowed ? Theme.snap(Math.max(16, shadowBlurPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)) + 8), win.dpr) : 0
|
||||
@@ -370,21 +627,21 @@ PanelWindow {
|
||||
|
||||
Behavior on shadowBlurPx {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
duration: win.inlineHeightAnimating ? win.inlineExpandDuration : Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on shadowOffsetX {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
duration: win.inlineHeightAnimating ? win.inlineExpandDuration : Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on shadowOffsetY {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
duration: win.inlineHeightAnimating ? win.inlineExpandDuration : Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
@@ -399,7 +656,7 @@ PanelWindow {
|
||||
shadowOffsetX: content.shadowOffsetX
|
||||
shadowOffsetY: content.shadowOffsetY
|
||||
shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent"
|
||||
shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed
|
||||
shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed && !win.connectedFrameMode
|
||||
layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
@@ -408,38 +665,42 @@ PanelWindow {
|
||||
sourceRect.y: content.shadowRenderPadding + content.cardInset
|
||||
sourceRect.width: Math.max(0, content.width - (content.cardInset * 2))
|
||||
sourceRect.height: Math.max(0, content.height - (content.cardInset * 2))
|
||||
sourceRect.radius: Theme.cornerRadius
|
||||
sourceRect.color: Theme.readableSurface
|
||||
sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.outlineMedium
|
||||
sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 1
|
||||
sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
sourceRect.color: win.connectedFrameMode ? Theme.floatingSurface : Theme.readableSurface
|
||||
sourceRect.antialiasing: true
|
||||
sourceRect.layer.enabled: false
|
||||
sourceRect.layer.textureSize: Qt.size(0, 0)
|
||||
sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
|
||||
sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: bgShadowLayer.sourceRect.x
|
||||
y: bgShadowLayer.sourceRect.y
|
||||
width: bgShadowLayer.sourceRect.width
|
||||
height: bgShadowLayer.sourceRect.height
|
||||
radius: bgShadowLayer.sourceRect.radius
|
||||
visible: notificationData && notificationData.urgency === NotificationUrgency.Critical
|
||||
opacity: 1
|
||||
clip: true
|
||||
// Keep critical accent outside shadow rendering so connected mode still shows it.
|
||||
Rectangle {
|
||||
x: content.cardInset
|
||||
y: content.cardInset
|
||||
width: Math.max(0, content.width - content.cardInset * 2)
|
||||
height: Math.max(0, content.height - content.cardInset * 2)
|
||||
radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
visible: win.notificationData && win.notificationData.urgency === NotificationUrgency.Critical
|
||||
opacity: 1
|
||||
clip: true
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Theme.primary
|
||||
}
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 0.02
|
||||
color: Theme.primary
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.02
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 0.021
|
||||
color: "transparent"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.021
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,11 +708,10 @@ PanelWindow {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: content.cardInset
|
||||
radius: Theme.cornerRadius
|
||||
antialiasing: true
|
||||
radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||
border.color: win.connectedFrameMode ? "transparent" : BlurService.borderColor
|
||||
border.width: win.connectedFrameMode ? 0 : BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
@@ -482,10 +742,23 @@ PanelWindow {
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
StyledText {
|
||||
id: expandedBodyMeasure
|
||||
|
||||
visible: false
|
||||
width: Math.max(0, backgroundContainer.width - Theme.spacingL - (Theme.spacingL + Theme.notificationHoverRevealMargin) - popupIconSize - Theme.spacingM)
|
||||
text: notificationData ? (notificationData.htmlBody || "") : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideNone
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
maximumLineCount: -1
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
|
||||
Item {
|
||||
id: notificationContent
|
||||
|
||||
readonly property real expandedTextHeight: bodyText.contentHeight || 0
|
||||
readonly property real expandedTextHeight: expandedBodyMeasure.contentHeight || bodyText.contentHeight || 0
|
||||
readonly property real collapsedBodyHeight: Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)
|
||||
readonly property real effectiveCollapsedHeight: (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) ? win.privacyCollapsedContentHeight : win.collapsedContentHeight
|
||||
readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedBodyHeight + 2) ? (expandedTextHeight - collapsedBodyHeight) : 0
|
||||
@@ -653,7 +926,7 @@ PanelWindow {
|
||||
win.descriptionExpanded = !win.descriptionExpanded;
|
||||
}
|
||||
|
||||
propagateComposedEvents: true
|
||||
propagateComposedEvents: false
|
||||
onPressed: mouse => {
|
||||
if (parent.hoveredLink)
|
||||
mouse.accepted = false;
|
||||
@@ -849,14 +1122,15 @@ PanelWindow {
|
||||
DragHandler {
|
||||
id: swipeDragHandler
|
||||
target: null
|
||||
xAxis.enabled: !isCenterPosition
|
||||
yAxis.enabled: isCenterPosition
|
||||
xAxis.enabled: true
|
||||
yAxis.enabled: false
|
||||
|
||||
onActiveChanged: {
|
||||
if (active || win.exiting || content.swipeDismissing)
|
||||
return;
|
||||
|
||||
if (Math.abs(content.swipeOffset) > content.dismissThreshold) {
|
||||
content.swipeDismissDirection = content.swipeOffset < 0 ? -1 : 1;
|
||||
content.swipeDismissing = true;
|
||||
swipeDismissAnim.start();
|
||||
} else {
|
||||
@@ -865,18 +1139,10 @@ PanelWindow {
|
||||
}
|
||||
|
||||
onTranslationChanged: {
|
||||
if (win.exiting)
|
||||
if (win.exiting || content.swipeDismissing)
|
||||
return;
|
||||
|
||||
const raw = isCenterPosition ? translation.y : translation.x;
|
||||
if (isTopCenter) {
|
||||
content.swipeOffset = Math.min(0, raw);
|
||||
} else if (isBottomCenter) {
|
||||
content.swipeOffset = Math.max(0, raw);
|
||||
} else {
|
||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||
content.swipeOffset = isLeft ? Math.min(0, raw) : Math.max(0, raw);
|
||||
}
|
||||
content.swipeOffset = translation.x;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,7 +1155,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !content.swipeActive
|
||||
enabled: !content.swipeActive && !content.swipeDismissing
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
@@ -907,20 +1173,28 @@ PanelWindow {
|
||||
id: swipeDismissAnim
|
||||
target: content
|
||||
property: "swipeOffset"
|
||||
to: isTopCenter ? -content.height : isBottomCenter ? content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width)
|
||||
to: win._swipeDismissTarget()
|
||||
duration: Theme.notificationExitDuration
|
||||
easing.type: Easing.OutCubic
|
||||
onStopped: {
|
||||
NotificationService.dismissNotification(notificationData);
|
||||
win.forceExit();
|
||||
const inwardConnectedExit = win.connectedFrameMode && !win.isCenterPosition && !win._swipeDismissesTowardFrameEdge();
|
||||
if (inwardConnectedExit)
|
||||
content.chromeOnlyExit = true;
|
||||
if (win.connectedFrameMode) {
|
||||
win.startExit();
|
||||
NotificationService.dismissNotification(notificationData);
|
||||
} else {
|
||||
NotificationService.dismissNotification(notificationData);
|
||||
win.forceExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transform: [
|
||||
Translate {
|
||||
id: swipeTx
|
||||
x: isCenterPosition ? 0 : content.swipeOffset
|
||||
y: isCenterPosition ? content.swipeOffset : 0
|
||||
x: content.swipeOffset
|
||||
y: 0
|
||||
},
|
||||
Translate {
|
||||
id: tx
|
||||
@@ -928,9 +1202,17 @@ PanelWindow {
|
||||
if (isCenterPosition)
|
||||
return 0;
|
||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||
return isLeft ? -entryTravel : entryTravel;
|
||||
}
|
||||
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
|
||||
onXChanged: {
|
||||
if (win.connectedFrameMode)
|
||||
win.popupChromeGeometryChanged();
|
||||
}
|
||||
onYChanged: {
|
||||
if (win.connectedFrameMode)
|
||||
win.popupChromeGeometryChanged();
|
||||
}
|
||||
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -942,16 +1224,16 @@ PanelWindow {
|
||||
property: isCenterPosition ? "y" : "x"
|
||||
from: {
|
||||
if (isTopCenter)
|
||||
return -Anims.slidePx;
|
||||
return -entryTravel;
|
||||
if (isBottomCenter)
|
||||
return Anims.slidePx;
|
||||
return entryTravel;
|
||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||
return isLeft ? -entryTravel : entryTravel;
|
||||
}
|
||||
to: 0
|
||||
duration: Theme.notificationEnterDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantPopoutEnterCurve
|
||||
onStopped: {
|
||||
if (!win.exiting && !win._isDestroying) {
|
||||
if (isCenterPosition) {
|
||||
@@ -976,35 +1258,33 @@ PanelWindow {
|
||||
from: 0
|
||||
to: {
|
||||
if (isTopCenter)
|
||||
return -Anims.slidePx;
|
||||
return -exitTravel;
|
||||
if (isBottomCenter)
|
||||
return Anims.slidePx;
|
||||
return exitTravel;
|
||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||
return isLeft ? -exitTravel : exitTravel;
|
||||
}
|
||||
duration: Theme.notificationExitDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
to: Theme.isDirectionalEffect ? 1 : 0
|
||||
duration: Theme.notificationExitDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standardAccel
|
||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.98
|
||||
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
|
||||
duration: Theme.notificationExitDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -8,23 +10,51 @@ QtObject {
|
||||
property var modelData
|
||||
property int topMargin: 0
|
||||
readonly property bool compactMode: SettingsData.notificationCompactMode
|
||||
readonly property bool notificationConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && SettingsData.isScreenInPreferences(manager.modelData, SettingsData.frameScreenPreferences)
|
||||
readonly property bool closeGapNotifications: notificationConnectedMode && SettingsData.frameCloseGaps
|
||||
readonly property string notifBarSide: {
|
||||
const pos = SettingsData.notificationPopupPosition;
|
||||
if (pos === -1)
|
||||
return "top";
|
||||
switch (pos) {
|
||||
case SettingsData.Position.Top:
|
||||
return "right";
|
||||
case SettingsData.Position.Left:
|
||||
return "left";
|
||||
case SettingsData.Position.BottomCenter:
|
||||
return "bottom";
|
||||
case SettingsData.Position.Right:
|
||||
return "right";
|
||||
case SettingsData.Position.Bottom:
|
||||
return "left";
|
||||
default:
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
|
||||
readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
|
||||
readonly property real actionButtonHeight: compactMode ? 20 : 24
|
||||
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
|
||||
readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS
|
||||
readonly property real popupSpacing: notificationConnectedMode ? 0 : (compactMode ? 0 : Theme.spacingXS)
|
||||
readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
|
||||
readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
|
||||
property var popupWindows: []
|
||||
property var destroyingWindows: new Set()
|
||||
property var pendingDestroys: []
|
||||
property int destroyDelayMs: 100
|
||||
property bool _chromeSyncPending: false
|
||||
property bool _syncingVisibleNotifications: false
|
||||
readonly property real chromeOpenProgressThreshold: 0.10
|
||||
readonly property real chromeReleaseTailStart: 0.90
|
||||
readonly property real chromeReleaseDropProgress: 0.995
|
||||
property Component popupComponent
|
||||
|
||||
popupComponent: Component {
|
||||
NotificationPopup {
|
||||
onExitFinished: manager._onPopupExitFinished(this)
|
||||
onExitStarted: manager._onPopupExitStarted(this)
|
||||
onPopupHeightChanged: manager._onPopupHeightChanged(this)
|
||||
onPopupChromeGeometryChanged: manager._onPopupChromeGeometryChanged(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +138,29 @@ QtObject {
|
||||
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
||||
}
|
||||
|
||||
function _layoutWindows() {
|
||||
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting && (!p.popupLayoutReservesSlot || p.popupLayoutReservesSlot()));
|
||||
}
|
||||
|
||||
function _chromeWindows() {
|
||||
return popupWindows.filter(p => {
|
||||
if (!p || p.status === Component.Null || !p.visible || p._finalized || !p.hasValidData)
|
||||
return false;
|
||||
if (!p.notificationData?.popup && !p.exiting)
|
||||
return false;
|
||||
if (p.exiting && p.notificationData?.removedByLimit && _layoutWindows().length > 0)
|
||||
return true;
|
||||
if (!p.exiting && p.popupChromeOpenProgress && p.popupChromeOpenProgress() < chromeOpenProgressThreshold)
|
||||
return false;
|
||||
// Keep the connected shell until the card is almost fully closed.
|
||||
if (p.exiting && !p.swipeActive && p.popupChromeReleaseProgress) {
|
||||
if (p.popupChromeReleaseProgress() > chromeReleaseDropProgress)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function _isFocusedScreen() {
|
||||
if (!SettingsData.notificationFocusedMonitor)
|
||||
return true;
|
||||
@@ -116,27 +169,34 @@ QtObject {
|
||||
}
|
||||
|
||||
function _sync(newWrappers) {
|
||||
let needsReposition = false;
|
||||
_syncingVisibleNotifications = true;
|
||||
for (const p of popupWindows.slice()) {
|
||||
if (!_isValidWindow(p) || p.exiting)
|
||||
continue;
|
||||
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) {
|
||||
p.notificationData.removedByLimit = true;
|
||||
p.notificationData.popup = false;
|
||||
needsReposition = true;
|
||||
}
|
||||
}
|
||||
for (const w of newWrappers) {
|
||||
if (w && !_hasWindowFor(w) && _isFocusedScreen())
|
||||
_insertAtTop(w);
|
||||
if (w && !_hasWindowFor(w) && _isFocusedScreen()) {
|
||||
needsReposition = _insertAtTop(w, true) || needsReposition;
|
||||
}
|
||||
}
|
||||
_syncingVisibleNotifications = false;
|
||||
if (needsReposition)
|
||||
_repositionAll();
|
||||
}
|
||||
|
||||
function _popupHeight(p) {
|
||||
return (p.alignedHeight || p.implicitHeight || (baseNotificationHeight - popupSpacing)) + popupSpacing;
|
||||
}
|
||||
|
||||
function _insertAtTop(wrapper) {
|
||||
function _insertAtTop(wrapper, deferReposition) {
|
||||
if (!wrapper)
|
||||
return;
|
||||
return false;
|
||||
const notificationId = wrapper?.notification ? wrapper.notification.id : "";
|
||||
const win = popupComponent.createObject(null, {
|
||||
"notificationData": wrapper,
|
||||
@@ -145,19 +205,21 @@ QtObject {
|
||||
"screen": manager.modelData
|
||||
});
|
||||
if (!win)
|
||||
return;
|
||||
return false;
|
||||
if (!win.hasValidData) {
|
||||
win.destroy();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
popupWindows.unshift(win);
|
||||
_repositionAll();
|
||||
if (!deferReposition)
|
||||
_repositionAll();
|
||||
if (!sweeper.running)
|
||||
sweeper.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
function _repositionAll() {
|
||||
const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting);
|
||||
const active = _layoutWindows();
|
||||
|
||||
const pinnedSlots = [];
|
||||
for (const p of active) {
|
||||
@@ -181,6 +243,315 @@ QtObject {
|
||||
win.screenY = currentY;
|
||||
currentY += _popupHeight(win);
|
||||
}
|
||||
_scheduleNotificationChromeSync();
|
||||
}
|
||||
|
||||
function _scheduleNotificationChromeSync() {
|
||||
if (_chromeSyncPending)
|
||||
return;
|
||||
_chromeSyncPending = true;
|
||||
Qt.callLater(() => {
|
||||
_chromeSyncPending = false;
|
||||
_syncNotificationChromeState();
|
||||
});
|
||||
}
|
||||
|
||||
function _clamp01(value) {
|
||||
return Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
function _clipRectFromBarSide(rect, visibleFraction) {
|
||||
const fraction = _clamp01(visibleFraction);
|
||||
const w = Math.max(0, rect.right - rect.x);
|
||||
const h = Math.max(0, rect.bottom - rect.y);
|
||||
|
||||
if (notifBarSide === "right") {
|
||||
rect.x = rect.right - w * fraction;
|
||||
} else if (notifBarSide === "left") {
|
||||
rect.right = rect.x + w * fraction;
|
||||
} else if (notifBarSide === "bottom") {
|
||||
rect.y = rect.bottom - h * fraction;
|
||||
} else {
|
||||
rect.bottom = rect.y + h * fraction;
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
function _popupChromeVisibleFraction(p) {
|
||||
if (p.popupChromeReleaseProgress) {
|
||||
const rel = p.popupChromeReleaseProgress();
|
||||
if (p.exiting)
|
||||
return Math.max(0, 1 - rel);
|
||||
if (rel > 0)
|
||||
return p.swipeDismissTowardEdge ? Math.max(0, 1 - rel) : 1 - _chromeReleaseTailProgress(rel);
|
||||
}
|
||||
if (p.popupChromeOpenProgress)
|
||||
return _clamp01(p.popupChromeOpenProgress());
|
||||
return 1;
|
||||
}
|
||||
|
||||
function _popupChromeRect(p, useMotionOffset) {
|
||||
if (!p || !p.screen)
|
||||
return null;
|
||||
const x = p.getContentX ? p.getContentX() : 0;
|
||||
const y = p.getContentY ? p.getContentY() : 0;
|
||||
const w = p.alignedWidth || 0;
|
||||
const h = Math.max(p.alignedHeight || 0, baseNotificationHeight);
|
||||
if (w <= 0 || h <= 0)
|
||||
return null;
|
||||
const rect = {
|
||||
x: x,
|
||||
y: y,
|
||||
right: x + w,
|
||||
bottom: y + h
|
||||
};
|
||||
|
||||
if (!useMotionOffset)
|
||||
return rect;
|
||||
|
||||
if (p.popupChromeFollowsCardMotion && p.popupChromeFollowsCardMotion()) {
|
||||
const motionX = p.popupChromeMotionX ? p.popupChromeMotionX() : 0;
|
||||
const motionY = p.popupChromeMotionY ? p.popupChromeMotionY() : 0;
|
||||
rect.x += motionX;
|
||||
rect.y += motionY;
|
||||
rect.right += motionX;
|
||||
rect.bottom += motionY;
|
||||
return rect;
|
||||
}
|
||||
|
||||
return _clipRectFromBarSide(rect, _popupChromeVisibleFraction(p));
|
||||
}
|
||||
|
||||
function _chromeReleaseTailProgress(rawProgress) {
|
||||
const progress = Math.max(0, Math.min(1, rawProgress));
|
||||
if (progress <= chromeReleaseTailStart)
|
||||
return 0;
|
||||
return Math.max(0, Math.min(1, (progress - chromeReleaseTailStart) / Math.max(0.001, 1 - chromeReleaseTailStart)));
|
||||
}
|
||||
|
||||
function _popupChromeBoundsRect(p, trailing, useMotionOffset) {
|
||||
const rect = _popupChromeRect(p, useMotionOffset);
|
||||
if (!rect || p !== trailing || !p.popupChromeReleaseProgress)
|
||||
return rect;
|
||||
|
||||
// Keep maxed-stack chrome anchored while a replacement tail exits.
|
||||
if (p.exiting && p.notificationData?.removedByLimit && _layoutWindows().length > 0)
|
||||
return rect;
|
||||
|
||||
const progress = _chromeReleaseTailProgress(p.popupChromeReleaseProgress());
|
||||
if (progress <= 0)
|
||||
return rect;
|
||||
|
||||
const anchorsTop = _stackAnchorsTop();
|
||||
const h = Math.max(0, rect.bottom - rect.y);
|
||||
const shrink = h * progress;
|
||||
if (anchorsTop)
|
||||
rect.bottom = Math.max(rect.y, rect.bottom - shrink);
|
||||
else
|
||||
rect.y = Math.min(rect.bottom, rect.y + shrink);
|
||||
return rect;
|
||||
}
|
||||
|
||||
function _stackAnchorsTop() {
|
||||
const pos = SettingsData.notificationPopupPosition;
|
||||
return pos === -1 || pos === SettingsData.Position.Top || pos === SettingsData.Position.Left;
|
||||
}
|
||||
|
||||
function _frameEdgeInset(side) {
|
||||
if (!manager.modelData)
|
||||
return 0;
|
||||
const edges = SettingsData.getActiveBarEdgesForScreen(manager.modelData);
|
||||
const raw = edges.includes(side) ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
||||
const dpr = CompositorService.getScreenScale(manager.modelData);
|
||||
return Math.max(0, Math.round(Theme.px(raw, dpr)));
|
||||
}
|
||||
|
||||
function _closeGapChromeAnchorEdge(anchorsTop) {
|
||||
if (!closeGapNotifications || !manager.modelData)
|
||||
return null;
|
||||
if (anchorsTop)
|
||||
return _frameEdgeInset("top") + topMargin;
|
||||
return manager.modelData.height - _frameEdgeInset("bottom") - topMargin;
|
||||
}
|
||||
|
||||
function _trailingChromeWindow(candidates) {
|
||||
const anchorsTop = _stackAnchorsTop();
|
||||
let trailing = null;
|
||||
let edge = anchorsTop ? -Infinity : Infinity;
|
||||
for (const p of candidates) {
|
||||
const rect = _popupChromeRect(p, false);
|
||||
if (!rect)
|
||||
continue;
|
||||
const candidateEdge = anchorsTop ? rect.bottom : rect.y;
|
||||
if ((anchorsTop && candidateEdge > edge) || (!anchorsTop && candidateEdge < edge)) {
|
||||
edge = candidateEdge;
|
||||
trailing = p;
|
||||
}
|
||||
}
|
||||
return trailing;
|
||||
}
|
||||
|
||||
function _chromeWindowReservesSlot(p, trailing) {
|
||||
if (p === trailing)
|
||||
return true;
|
||||
return !p.popupChromeReservesSlot || p.popupChromeReservesSlot();
|
||||
}
|
||||
|
||||
function _stackAnchoredChromeEdge(candidates) {
|
||||
const anchorsTop = _stackAnchorsTop();
|
||||
let edge = anchorsTop ? Infinity : -Infinity;
|
||||
for (const p of candidates) {
|
||||
const rect = _popupChromeRect(p, false);
|
||||
if (!rect)
|
||||
continue;
|
||||
if (anchorsTop && rect.y < edge)
|
||||
edge = rect.y;
|
||||
if (!anchorsTop && rect.bottom > edge)
|
||||
edge = rect.bottom;
|
||||
}
|
||||
if (edge === Infinity || edge === -Infinity)
|
||||
return null;
|
||||
return {
|
||||
anchorsTop: anchorsTop,
|
||||
edge: edge
|
||||
};
|
||||
}
|
||||
|
||||
function _filledMaxStackChromeEdge(candidates, stackEdge) {
|
||||
const layoutWindows = _layoutWindows();
|
||||
if (layoutWindows.length < NotificationService.maxVisibleNotifications)
|
||||
return null;
|
||||
const anchorsTop = _stackAnchorsTop();
|
||||
const layoutAnchorEdge = _stackAnchoredChromeEdge(layoutWindows);
|
||||
const anchorEdge = layoutAnchorEdge !== null ? layoutAnchorEdge : (stackEdge !== null ? stackEdge : _stackAnchoredChromeEdge(candidates));
|
||||
if (anchorEdge === null)
|
||||
return null;
|
||||
let span = 0;
|
||||
for (const p of layoutWindows) {
|
||||
const rect = _popupChromeRect(p, false);
|
||||
if (!rect)
|
||||
continue;
|
||||
span += Math.max(0, rect.bottom - rect.y);
|
||||
}
|
||||
if (span <= 0)
|
||||
return null;
|
||||
if (layoutWindows.length > 1)
|
||||
span += popupSpacing * (layoutWindows.length - 1);
|
||||
return {
|
||||
anchorsTop: anchorsTop,
|
||||
startEdge: anchorEdge.edge,
|
||||
edge: anchorsTop ? anchorEdge.edge + span : anchorEdge.edge - span
|
||||
};
|
||||
}
|
||||
|
||||
function _syncNotificationChromeState() {
|
||||
const screenName = manager.modelData?.name || "";
|
||||
if (!screenName)
|
||||
return;
|
||||
if (!notificationConnectedMode) {
|
||||
ConnectedModeState.clearNotificationState(screenName);
|
||||
return;
|
||||
}
|
||||
const chromeCandidates = _chromeWindows();
|
||||
if (chromeCandidates.length === 0) {
|
||||
ConnectedModeState.clearNotificationState(screenName);
|
||||
return;
|
||||
}
|
||||
|
||||
const trailing = chromeCandidates.length > 1 ? _trailingChromeWindow(chromeCandidates) : null;
|
||||
let active = chromeCandidates;
|
||||
if (chromeCandidates.length > 1) {
|
||||
const reserving = chromeCandidates.filter(p => _chromeWindowReservesSlot(p, trailing));
|
||||
if (reserving.length > 0)
|
||||
active = reserving;
|
||||
}
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxXEnd = -Infinity;
|
||||
let maxYEnd = -Infinity;
|
||||
const useMotionOffset = active.length === 1 && active[0].popupChromeMotionActive && active[0].popupChromeMotionActive();
|
||||
for (const p of active) {
|
||||
const rect = _popupChromeBoundsRect(p, trailing, useMotionOffset);
|
||||
if (!rect)
|
||||
continue;
|
||||
if (rect.x < minX)
|
||||
minX = rect.x;
|
||||
if (rect.y < minY)
|
||||
minY = rect.y;
|
||||
if (rect.right > maxXEnd)
|
||||
maxXEnd = rect.right;
|
||||
if (rect.bottom > maxYEnd)
|
||||
maxYEnd = rect.bottom;
|
||||
}
|
||||
const stackEdge = _stackAnchoredChromeEdge(chromeCandidates);
|
||||
if (stackEdge !== null) {
|
||||
if (stackEdge.anchorsTop && stackEdge.edge < minY)
|
||||
minY = stackEdge.edge;
|
||||
if (!stackEdge.anchorsTop && stackEdge.edge > maxYEnd)
|
||||
maxYEnd = stackEdge.edge;
|
||||
}
|
||||
const filledMaxStackEdge = _filledMaxStackChromeEdge(chromeCandidates, stackEdge);
|
||||
if (filledMaxStackEdge !== null) {
|
||||
if (filledMaxStackEdge.anchorsTop) {
|
||||
minY = filledMaxStackEdge.startEdge;
|
||||
maxYEnd = filledMaxStackEdge.edge;
|
||||
} else {
|
||||
minY = filledMaxStackEdge.edge;
|
||||
maxYEnd = filledMaxStackEdge.startEdge;
|
||||
}
|
||||
}
|
||||
const anchorsTop = stackEdge !== null ? stackEdge.anchorsTop : _stackAnchorsTop();
|
||||
const closeGapAnchorEdge = _closeGapChromeAnchorEdge(anchorsTop);
|
||||
if (closeGapAnchorEdge !== null) {
|
||||
if (anchorsTop)
|
||||
minY = closeGapAnchorEdge;
|
||||
else
|
||||
maxYEnd = closeGapAnchorEdge;
|
||||
}
|
||||
if (minX === Infinity || minY === Infinity || maxXEnd <= minX || maxYEnd <= minY) {
|
||||
ConnectedModeState.clearNotificationState(screenName);
|
||||
return;
|
||||
}
|
||||
ConnectedModeState.setNotificationState(screenName, {
|
||||
visible: true,
|
||||
barSide: notifBarSide,
|
||||
bodyX: minX,
|
||||
bodyY: minY,
|
||||
bodyW: maxXEnd - minX,
|
||||
bodyH: maxYEnd - minY,
|
||||
omitStartConnector: _notificationOmitStartConnector(),
|
||||
omitEndConnector: _notificationOmitEndConnector()
|
||||
});
|
||||
}
|
||||
|
||||
function _notificationOmitStartConnector() {
|
||||
return closeGapNotifications && (SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Left);
|
||||
}
|
||||
|
||||
function _notificationOmitEndConnector() {
|
||||
return closeGapNotifications && (SettingsData.notificationPopupPosition === SettingsData.Position.Right || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom);
|
||||
}
|
||||
|
||||
function _onPopupChromeGeometryChanged(p) {
|
||||
if (!p || popupWindows.indexOf(p) === -1)
|
||||
return;
|
||||
_scheduleNotificationChromeSync();
|
||||
}
|
||||
|
||||
// Coalesce resize repositioning; exit-path moves remain immediate.
|
||||
property bool _repositionPending: false
|
||||
|
||||
function _queueReposition() {
|
||||
if (_repositionPending)
|
||||
return;
|
||||
_repositionPending = true;
|
||||
Qt.callLater(_flushReposition);
|
||||
}
|
||||
|
||||
function _flushReposition() {
|
||||
_repositionPending = false;
|
||||
_repositionAll();
|
||||
}
|
||||
|
||||
function _onPopupHeightChanged(p) {
|
||||
@@ -188,6 +559,14 @@ QtObject {
|
||||
return;
|
||||
if (popupWindows.indexOf(p) === -1)
|
||||
return;
|
||||
_queueReposition();
|
||||
}
|
||||
|
||||
function _onPopupExitStarted(p) {
|
||||
if (!p || popupWindows.indexOf(p) === -1)
|
||||
return;
|
||||
if (_syncingVisibleNotifications)
|
||||
return;
|
||||
_repositionAll();
|
||||
}
|
||||
|
||||
@@ -227,8 +606,16 @@ QtObject {
|
||||
}
|
||||
popupWindows = [];
|
||||
destroyingWindows.clear();
|
||||
_chromeSyncPending = false;
|
||||
_syncNotificationChromeState();
|
||||
}
|
||||
|
||||
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
|
||||
onCloseGapNotificationsChanged: _scheduleNotificationChromeSync()
|
||||
onNotifBarSideChanged: _scheduleNotificationChromeSync()
|
||||
onModelDataChanged: _scheduleNotificationChromeSync()
|
||||
onTopMarginChanged: _repositionAll()
|
||||
|
||||
onPopupWindowsChanged: {
|
||||
if (popupWindows.length > 0 && !sweeper.running) {
|
||||
sweeper.start();
|
||||
|
||||
@@ -27,6 +27,7 @@ Item {
|
||||
const pos = selectedBarConfig?.position ?? SettingsData.Position.Top;
|
||||
return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right;
|
||||
}
|
||||
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
|
||||
|
||||
Timer {
|
||||
id: horizontalBarChangeDebounce
|
||||
@@ -695,6 +696,8 @@ Item {
|
||||
|
||||
SettingsToggleRow {
|
||||
visible: CompositorService.isNiri
|
||||
enabled: !SettingsData.frameEnabled
|
||||
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||
text: I18n.tr("Show on Overview")
|
||||
checked: selectedBarConfig?.openOnOverview ?? false
|
||||
onToggled: toggled => {
|
||||
@@ -705,11 +708,18 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: SettingsData.frameEnabled
|
||||
parentModal: dankBarTab.parentModal
|
||||
settingLabel: I18n.tr("Bar spacing and size")
|
||||
reason: I18n.tr("Managed by Frame")
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
iconName: "space_bar"
|
||||
title: I18n.tr("Spacing")
|
||||
settingKey: "barSpacing"
|
||||
visible: selectedBarConfig?.enabled
|
||||
visible: (selectedBarConfig?.enabled ?? false) && !SettingsData.frameEnabled
|
||||
|
||||
SettingsSliderRow {
|
||||
id: edgeSpacingSlider
|
||||
@@ -860,6 +870,7 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
id: barTransparencySlider
|
||||
visible: !SettingsData.frameEnabled
|
||||
text: I18n.tr("Bar Transparency")
|
||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||
minimum: 0
|
||||
@@ -901,6 +912,13 @@ Item {
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: SettingsData.frameEnabled
|
||||
parentModal: dankBarTab.parentModal
|
||||
settingLabel: I18n.tr("Bar transparency")
|
||||
reason: I18n.tr("Managed by Frame")
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderCard {
|
||||
@@ -961,8 +979,16 @@ Item {
|
||||
expanded: false
|
||||
visible: selectedBarConfig?.enabled
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: SettingsData.frameEnabled
|
||||
parentModal: dankBarTab.parentModal
|
||||
settingLabel: I18n.tr("Bar corners and background")
|
||||
reason: I18n.tr("Managed by Frame")
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Square Corners")
|
||||
visible: !SettingsData.frameEnabled
|
||||
checked: selectedBarConfig?.squareCorners ?? false
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
squareCorners: checked
|
||||
@@ -971,6 +997,7 @@ Item {
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("No Background")
|
||||
visible: !SettingsData.frameEnabled
|
||||
checked: selectedBarConfig?.noBackground ?? false
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
noBackground: checked
|
||||
@@ -1010,6 +1037,7 @@ Item {
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Goth Corners")
|
||||
visible: !SettingsData.frameEnabled
|
||||
checked: selectedBarConfig?.gothCornersEnabled ?? false
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
gothCornersEnabled: checked
|
||||
@@ -1345,6 +1373,13 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: dankBarTab.connectedFrameModeActive
|
||||
parentModal: dankBarTab.parentModal
|
||||
settingLabel: I18n.tr("Bar shadow, border, and corners")
|
||||
reason: I18n.tr("Managed by Frame in Connected Mode")
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: shadowCard
|
||||
iconName: "layers"
|
||||
@@ -1352,7 +1387,7 @@ Item {
|
||||
settingKey: "barShadow"
|
||||
collapsible: true
|
||||
expanded: false
|
||||
visible: selectedBarConfig?.enabled
|
||||
visible: (selectedBarConfig?.enabled ?? false) && !dankBarTab.connectedFrameModeActive
|
||||
|
||||
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom"
|
||||
|
||||
@@ -8,6 +8,9 @@ import qs.Modules.Settings.Widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var parentModal: null
|
||||
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
|
||||
|
||||
FileBrowserModal {
|
||||
id: dockLogoFileBrowser
|
||||
browserTitle: I18n.tr("Select Dock Launcher Logo")
|
||||
@@ -604,6 +607,7 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
text: I18n.tr("Exclusive Zone Offset")
|
||||
visible: !root.connectedFrameModeActive
|
||||
value: SettingsData.dockBottomGap
|
||||
minimum: -100
|
||||
maximum: 100
|
||||
@@ -613,6 +617,7 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
text: I18n.tr("Margin")
|
||||
visible: !root.connectedFrameModeActive
|
||||
value: SettingsData.dockMargin
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -621,11 +626,19 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: root.connectedFrameModeActive
|
||||
parentModal: root.parentModal
|
||||
settingLabel: I18n.tr("Dock spacing, transparency, and border")
|
||||
reason: I18n.tr("Managed by Frame in Connected Mode")
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "opacity"
|
||||
title: I18n.tr("Transparency")
|
||||
settingKey: "dockTransparency"
|
||||
visible: !root.connectedFrameModeActive
|
||||
|
||||
SettingsSliderRow {
|
||||
text: I18n.tr("Dock Transparency")
|
||||
@@ -645,6 +658,7 @@ Item {
|
||||
settingKey: "dockBorder"
|
||||
collapsible: true
|
||||
expanded: false
|
||||
visible: !root.connectedFrameModeActive
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Border")
|
||||
|
||||
370
quickshell/Modules/Settings/FrameTab.qml
Normal file
370
quickshell/Modules/Settings/FrameTab.qml
Normal file
@@ -0,0 +1,370 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingXL
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
topPadding: 4
|
||||
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "frame_source"
|
||||
title: I18n.tr("Frame")
|
||||
settingKey: "frameEnabled"
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "frameEnable"
|
||||
tags: ["frame", "border", "outline", "display"]
|
||||
text: I18n.tr("Enable Frame")
|
||||
description: I18n.tr("Draw a connected picture-frame border around the entire display")
|
||||
checked: SettingsData.frameEnabled
|
||||
onToggled: checked => SettingsData.set("frameEnabled", checked)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "tune"
|
||||
title: I18n.tr("Mode")
|
||||
settingKey: "frameMode"
|
||||
visible: SettingsData.frameEnabled
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "frameModeSelector"
|
||||
tags: ["frame", "mode", "connected", "separate", "popout"]
|
||||
text: I18n.tr("Surface Behavior")
|
||||
description: SettingsData.frameMode === "connected" ? I18n.tr("Surfaces emerge flush from the bar") : I18n.tr("Surfaces float independently of the frame")
|
||||
model: [I18n.tr("Separate"), I18n.tr("Connected")]
|
||||
currentIndex: SettingsData.frameMode === "connected" ? 1 : 0
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
switch (index) {
|
||||
case 1:
|
||||
SettingsData.set("frameMode", "connected");
|
||||
break;
|
||||
default:
|
||||
SettingsData.set("frameMode", "separate");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "border_outer"
|
||||
title: I18n.tr("Border")
|
||||
settingKey: "frameBorder"
|
||||
collapsible: true
|
||||
visible: SettingsData.frameEnabled
|
||||
|
||||
SettingsSliderRow {
|
||||
id: roundingSlider
|
||||
settingKey: "frameRounding"
|
||||
tags: ["frame", "border", "rounding", "radius", "corner"]
|
||||
text: I18n.tr("Border Radius")
|
||||
unit: "px"
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
step: 1
|
||||
defaultValue: 23
|
||||
value: SettingsData.frameRounding
|
||||
onSliderDragFinished: v => SettingsData.set("frameRounding", v)
|
||||
|
||||
Binding {
|
||||
target: roundingSlider
|
||||
property: "value"
|
||||
value: SettingsData.frameRounding
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: thicknessSlider
|
||||
settingKey: "frameThickness"
|
||||
tags: ["frame", "border", "thickness", "size", "width"]
|
||||
text: I18n.tr("Border Width")
|
||||
unit: "px"
|
||||
minimum: 2
|
||||
maximum: 100
|
||||
step: 1
|
||||
defaultValue: 16
|
||||
value: SettingsData.frameThickness
|
||||
onSliderDragFinished: v => SettingsData.set("frameThickness", v)
|
||||
|
||||
Binding {
|
||||
target: thicknessSlider
|
||||
property: "value"
|
||||
value: SettingsData.frameThickness
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: barThicknessSlider
|
||||
settingKey: "frameBarSize"
|
||||
tags: ["frame", "bar", "thickness", "size", "height", "width"]
|
||||
text: I18n.tr("Size")
|
||||
description: I18n.tr("Horizontal and vertical bar thickness")
|
||||
unit: "px"
|
||||
minimum: 24
|
||||
maximum: 100
|
||||
step: 1
|
||||
defaultValue: 40
|
||||
value: SettingsData.frameBarSize
|
||||
onSliderDragFinished: v => SettingsData.set("frameBarSize", v)
|
||||
|
||||
Binding {
|
||||
target: barThicknessSlider
|
||||
property: "value"
|
||||
value: SettingsData.frameBarSize
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: opacitySlider
|
||||
settingKey: "frameOpacity"
|
||||
tags: ["frame", "border", "surface", "popup", "opacity", "transparency"]
|
||||
text: I18n.tr("Surface Opacity")
|
||||
unit: "%"
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
defaultValue: 100
|
||||
value: SettingsData.frameOpacity * 100
|
||||
onSliderDragFinished: v => SettingsData.set("frameOpacity", v / 100)
|
||||
|
||||
Binding {
|
||||
target: opacitySlider
|
||||
property: "value"
|
||||
value: SettingsData.frameOpacity * 100
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
id: frameBlurToggle
|
||||
settingKey: "frameBlurEnabled"
|
||||
tags: ["frame", "blur", "background", "glass", "transparency", "frosted"]
|
||||
text: I18n.tr("Frame Blur")
|
||||
description: !BlurService.available ? I18n.tr("Requires a newer version of Quickshell") : I18n.tr("Apply compositor blur behind the frame border")
|
||||
checked: SettingsData.frameBlurEnabled
|
||||
onToggled: checked => SettingsData.set("frameBlurEnabled", checked)
|
||||
enabled: BlurService.available && SettingsData.blurEnabled
|
||||
opacity: enabled ? 1.0 : 0.5
|
||||
visible: BlurService.available
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: BlurService.available && !SettingsData.blurEnabled
|
||||
width: parent.width
|
||||
height: blurToggleNote.height + Theme.spacingM * 2
|
||||
|
||||
Row {
|
||||
id: blurToggleNote
|
||||
x: Theme.spacingM
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "blur_on"
|
||||
size: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Frame Blur follows Background Blur in Theme & Colors")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "frameColor"
|
||||
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
|
||||
text: I18n.tr("Border Color")
|
||||
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
||||
currentIndex: {
|
||||
const fc = SettingsData.frameColor;
|
||||
if (!fc || fc === "default")
|
||||
return 0;
|
||||
switch (fc) {
|
||||
case "primary":
|
||||
return 1;
|
||||
case "surface":
|
||||
return 2;
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
switch (index) {
|
||||
case 0:
|
||||
SettingsData.set("frameColor", "");
|
||||
break;
|
||||
case 1:
|
||||
SettingsData.set("frameColor", "primary");
|
||||
break;
|
||||
case 2:
|
||||
SettingsData.set("frameColor", "surface");
|
||||
break;
|
||||
case 3:
|
||||
const cur = SettingsData.frameColor;
|
||||
const isPreset = !cur || cur === "primary" || cur === "surface";
|
||||
if (isPreset)
|
||||
SettingsData.set("frameColor", "#2a2a2a");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: {
|
||||
const fc = SettingsData.frameColor;
|
||||
return !!(fc && fc !== "primary" && fc !== "surface");
|
||||
}
|
||||
width: parent.width
|
||||
height: customColorRow.height + Theme.spacingM * 2
|
||||
|
||||
Row {
|
||||
id: customColorRow
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
x: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Custom Color")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: SettingsData.effectiveFrameColor
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
PopoutService.colorPickerModal.selectedColor = SettingsData.effectiveFrameColor;
|
||||
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Frame Border Color");
|
||||
PopoutService.colorPickerModal.onColorSelectedCallback = function (color) {
|
||||
SettingsData.set("frameColor", color.toString());
|
||||
};
|
||||
PopoutService.colorPickerModal.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "blur_linear"
|
||||
title: I18n.tr("Connected Options")
|
||||
settingKey: "frameConnectedOptions"
|
||||
collapsible: true
|
||||
expanded: true
|
||||
visible: SettingsData.frameEnabled && SettingsData.frameMode === "connected"
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "frameCloseGaps"
|
||||
tags: ["frame", "connected", "gap", "edge", "curves", "arcs", "expose", "popout", "notification"]
|
||||
text: I18n.tr("Expose the Arcs")
|
||||
description: I18n.tr("Reveal the arcs where surfaces meet the frame")
|
||||
checked: !SettingsData.frameCloseGaps
|
||||
onToggled: checked => SettingsData.set("frameCloseGaps", !checked)
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "frameLauncherEmergeSide"
|
||||
tags: ["frame", "connected", "launcher", "modal", "emerge", "direction", "bottom", "top"]
|
||||
text: I18n.tr("Launcher Emerge Side")
|
||||
description: I18n.tr("Edge the launcher slides from")
|
||||
model: [I18n.tr("Bottom"), I18n.tr("Top")]
|
||||
currentIndex: SettingsData.frameLauncherEmergeSide === "top" ? 1 : 0
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.set("frameLauncherEmergeSide", index === 1 ? "top" : "bottom");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "frameLauncherArcExtender"
|
||||
tags: ["frame", "connected", "launcher", "arc", "extender", "center"]
|
||||
text: I18n.tr("Arc Extender")
|
||||
description: I18n.tr("Use the extended surface for launcher content")
|
||||
checked: SettingsData.frameLauncherArcExtender
|
||||
onToggled: checked => SettingsData.set("frameLauncherArcExtender", checked)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "toolbar"
|
||||
title: I18n.tr("Integrations")
|
||||
settingKey: "frameBarIntegration"
|
||||
collapsible: true
|
||||
expanded: true
|
||||
visible: SettingsData.frameEnabled && CompositorService.isNiri
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "frameShowOnOverview"
|
||||
tags: ["frame", "overview", "show", "hide", "niri"]
|
||||
text: I18n.tr("Show on Overview")
|
||||
description: I18n.tr("Show during Niri overview")
|
||||
checked: SettingsData.frameShowOnOverview
|
||||
onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "monitor"
|
||||
title: I18n.tr("Display Assignment")
|
||||
settingKey: "frameDisplays"
|
||||
collapsible: true
|
||||
expanded: false
|
||||
visible: SettingsData.frameEnabled
|
||||
|
||||
SettingsDisplayPicker {
|
||||
displayPreferences: SettingsData.frameScreenPreferences
|
||||
onPreferencesChanged: prefs => SettingsData.set("frameScreenPreferences", prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,11 +344,7 @@ Item {
|
||||
return I18n.tr("%1 exists but is not included in config. Custom keybinds will not work until this is fixed.").arg(bindsFile);
|
||||
if (warningBox.showWarning) {
|
||||
const count = warningBox.status.overriddenBy;
|
||||
return I18n.ntr(
|
||||
"%1 DMS bind may be overridden by config binds that come after the include.",
|
||||
"%1 DMS binds may be overridden by config binds that come after the include.",
|
||||
count
|
||||
).arg(count);
|
||||
return I18n.ntr("%1 DMS bind may be overridden by config binds that come after the include.", "%1 DMS binds may be overridden by config binds that come after the include.", count).arg(count);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -543,13 +539,11 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (KeybindsService.loading)
|
||||
return I18n.tr("Shortcuts");
|
||||
const count = keybindsTab._filteredBinds.length;
|
||||
return count === 1
|
||||
? I18n.tr("Shortcut (%1)").arg(count)
|
||||
: I18n.tr("Shortcuts (%1)").arg(count);
|
||||
}
|
||||
if (KeybindsService.loading)
|
||||
return I18n.tr("Shortcuts");
|
||||
const count = keybindsTab._filteredBinds.length;
|
||||
return count === 1 ? I18n.tr("Shortcut (%1)").arg(count) : I18n.tr("Shortcuts (%1)").arg(count);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
@@ -569,7 +563,7 @@ Item {
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
RotationAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
|
||||
@@ -36,36 +36,36 @@ Item {
|
||||
|
||||
function normalizePinList(value) {
|
||||
if (Array.isArray(value))
|
||||
return value.filter(v => v)
|
||||
return value.filter(v => v);
|
||||
if (typeof value === "string" && value.length > 0)
|
||||
return [value]
|
||||
return []
|
||||
return [value];
|
||||
return [];
|
||||
}
|
||||
|
||||
function getPinnedWifiNetworks() {
|
||||
const pins = SettingsData.wifiNetworkPins || {}
|
||||
return normalizePinList(pins["preferredWifi"])
|
||||
const pins = SettingsData.wifiNetworkPins || {};
|
||||
return normalizePinList(pins["preferredWifi"]);
|
||||
}
|
||||
|
||||
function toggleWifiPin(ssid) {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}))
|
||||
let pinnedList = normalizePinList(pins["preferredWifi"])
|
||||
const pinIndex = pinnedList.indexOf(ssid)
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
|
||||
let pinnedList = normalizePinList(pins["preferredWifi"]);
|
||||
const pinIndex = pinnedList.indexOf(ssid);
|
||||
|
||||
if (pinIndex !== -1) {
|
||||
pinnedList.splice(pinIndex, 1)
|
||||
pinnedList.splice(pinIndex, 1);
|
||||
} else {
|
||||
pinnedList.unshift(ssid)
|
||||
pinnedList.unshift(ssid);
|
||||
if (pinnedList.length > maxPinnedWifiNetworks)
|
||||
pinnedList = pinnedList.slice(0, maxPinnedWifiNetworks)
|
||||
pinnedList = pinnedList.slice(0, maxPinnedWifiNetworks);
|
||||
}
|
||||
|
||||
if (pinnedList.length > 0)
|
||||
pins["preferredWifi"] = pinnedList
|
||||
pins["preferredWifi"] = pinnedList;
|
||||
else
|
||||
delete pins["preferredWifi"]
|
||||
delete pins["preferredWifi"];
|
||||
|
||||
SettingsData.set("wifiNetworkPins", pins)
|
||||
SettingsData.set("wifiNetworkPins", pins);
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
@@ -340,9 +340,7 @@ Item {
|
||||
if (devices.length === 0)
|
||||
return I18n.tr("No adapters");
|
||||
if (connected === 0)
|
||||
return devices.length === 1
|
||||
? I18n.tr("%1 adapter, none connected").arg(devices.length)
|
||||
: I18n.tr("%1 adapters, none connected").arg(devices.length);
|
||||
return devices.length === 1 ? I18n.tr("%1 adapter, none connected").arg(devices.length) : I18n.tr("%1 adapters, none connected").arg(devices.length);
|
||||
return I18n.tr("%1 connected").arg(connected);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
@@ -658,16 +656,14 @@ Item {
|
||||
SequentialAnimation {
|
||||
running: NetworkService.networkWiredInfoLoading
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
OpacityAnimator {
|
||||
target: wiredLoadIcon
|
||||
property: "opacity"
|
||||
to: 0.3
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
OpacityAnimator {
|
||||
target: wiredLoadIcon
|
||||
property: "opacity"
|
||||
to: 1.0
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
@@ -1046,16 +1042,14 @@ Item {
|
||||
SequentialAnimation {
|
||||
running: NetworkService.isScanning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
OpacityAnimator {
|
||||
target: scanningIcon
|
||||
property: "opacity"
|
||||
to: 0.3
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
OpacityAnimator {
|
||||
target: scanningIcon
|
||||
property: "opacity"
|
||||
to: 1.0
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
@@ -1087,14 +1081,14 @@ Item {
|
||||
|
||||
let sorted = [...networks];
|
||||
sorted.sort((a, b) => {
|
||||
const aPinnedIndex = pinnedList.indexOf(a.ssid)
|
||||
const bPinnedIndex = pinnedList.indexOf(b.ssid)
|
||||
const aPinnedIndex = pinnedList.indexOf(a.ssid);
|
||||
const bPinnedIndex = pinnedList.indexOf(b.ssid);
|
||||
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
|
||||
if (aPinnedIndex === -1)
|
||||
return 1
|
||||
return 1;
|
||||
if (bPinnedIndex === -1)
|
||||
return -1
|
||||
return aPinnedIndex - bPinnedIndex
|
||||
return -1;
|
||||
return aPinnedIndex - bPinnedIndex;
|
||||
}
|
||||
if (a.ssid === ssid)
|
||||
return -1;
|
||||
@@ -1297,7 +1291,7 @@ Item {
|
||||
buttonSize: 28
|
||||
iconColor: isPinned ? Theme.primary : Theme.surfaceVariantText
|
||||
onClicked: {
|
||||
networkTab.toggleWifiPin(modelData.ssid)
|
||||
networkTab.toggleWifiPin(modelData.ssid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1375,16 +1369,14 @@ Item {
|
||||
SequentialAnimation {
|
||||
running: NetworkService.networkInfoLoading
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
OpacityAnimator {
|
||||
target: wifiInfoLoadIcon
|
||||
property: "opacity"
|
||||
to: 0.3
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
OpacityAnimator {
|
||||
target: wifiInfoLoadIcon
|
||||
property: "opacity"
|
||||
to: 1.0
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
@@ -1866,16 +1858,14 @@ Item {
|
||||
SequentialAnimation {
|
||||
running: VPNService.configLoading
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
OpacityAnimator {
|
||||
target: vpnLoadIcon
|
||||
property: "opacity"
|
||||
to: 0.3
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
OpacityAnimator {
|
||||
target: vpnLoadIcon
|
||||
property: "opacity"
|
||||
to: 1.0
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
@@ -1984,7 +1974,9 @@ Item {
|
||||
checked: configData ? (configData.autoconnect || false) : false
|
||||
visible: !VPNService.configLoading && configData !== null
|
||||
onToggled: checked => {
|
||||
VPNService.updateConfig(modelData.uuid, {autoconnect: checked});
|
||||
VPNService.updateConfig(modelData.uuid, {
|
||||
autoconnect: checked
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -458,7 +458,7 @@ Item {
|
||||
enabled: !CupsService.loadingDevices
|
||||
onClicked: CupsService.getDevices()
|
||||
|
||||
RotationAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: CupsService.loadingDevices
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
@@ -736,7 +736,7 @@ Item {
|
||||
enabled: !CupsService.loadingPPDs
|
||||
onClicked: CupsService.getPPDs()
|
||||
|
||||
RotationAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: CupsService.loadingPPDs
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
|
||||
@@ -11,6 +11,8 @@ import qs.Modules.Settings.Widgets
|
||||
Item {
|
||||
id: themeColorsTab
|
||||
|
||||
property var parentModal: null
|
||||
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
|
||||
property var cachedIconThemes: SettingsData.availableIconThemes
|
||||
property var cachedCursorThemes: SettingsData.availableCursorThemes
|
||||
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
|
||||
@@ -1613,12 +1615,20 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: themeColorsTab.connectedFrameModeActive
|
||||
parentModal: themeColorsTab.parentModal
|
||||
settingLabel: I18n.tr("Surface Opacity")
|
||||
reason: I18n.tr("Managed by Frame in Connected Mode")
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
tab: "theme"
|
||||
tags: ["popup", "transparency", "opacity", "modal"]
|
||||
tags: ["surface", "popup", "transparency", "opacity", "modal"]
|
||||
settingKey: "popupTransparency"
|
||||
text: I18n.tr("Popup Transparency")
|
||||
text: I18n.tr("Surface Opacity")
|
||||
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
|
||||
visible: !themeColorsTab.connectedFrameModeActive
|
||||
value: Math.round(SettingsData.popupTransparency * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -1837,7 +1847,7 @@ Item {
|
||||
tags: ["blur", "background", "transparency", "glass", "frosted"]
|
||||
settingKey: "blurEnabled"
|
||||
text: I18n.tr("Background Blur")
|
||||
description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell")
|
||||
description: !BlurService.available ? I18n.tr("Requires a newer version of Quickshell") : I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.")
|
||||
checked: SettingsData.blurEnabled ?? false
|
||||
enabled: BlurService.available
|
||||
onToggled: checked => SettingsData.set("blurEnabled", checked)
|
||||
@@ -2240,12 +2250,20 @@ Item {
|
||||
settingKey: "modalBackground"
|
||||
iconName: "layers"
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: themeColorsTab.connectedFrameModeActive
|
||||
parentModal: themeColorsTab.parentModal
|
||||
settingLabel: I18n.tr("Darken Modal Background")
|
||||
reason: I18n.tr("Managed by Frame in Connected Mode")
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "theme"
|
||||
tags: ["modal", "darken", "background", "overlay"]
|
||||
settingKey: "modalDarkenBackground"
|
||||
text: I18n.tr("Darken Modal Background")
|
||||
description: I18n.tr("Show darkened overlay behind modal dialogs")
|
||||
visible: !themeColorsTab.connectedFrameModeActive
|
||||
checked: SettingsData.modalDarkenBackground
|
||||
onToggled: checked => SettingsData.set("modalDarkenBackground", checked)
|
||||
}
|
||||
|
||||
@@ -693,7 +693,7 @@ Item {
|
||||
onTriggered: refreshButton.isRefreshing = false
|
||||
}
|
||||
|
||||
NumberAnimation on rotation {
|
||||
RotationAnimator on rotation {
|
||||
running: refreshButton.isRefreshing
|
||||
from: 0
|
||||
to: 360
|
||||
|
||||
@@ -55,6 +55,144 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
SettingsCard {
|
||||
tab: "typography"
|
||||
tags: ["animation", "variant", "style", "slide", "fluent", "dynamic", "motion"]
|
||||
title: I18n.tr("Animation Style")
|
||||
settingKey: "animationVariant"
|
||||
iconName: "auto_awesome_motion"
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: animVariantGroup.implicitHeight
|
||||
clip: true
|
||||
|
||||
DankButtonGroup {
|
||||
id: animVariantGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
|
||||
minButtonWidth: parent.width < 480 ? 64 : 96
|
||||
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
model: [I18n.tr("Material"), I18n.tr("Fluent"), I18n.tr("Dynamic")]
|
||||
selectionMode: "single"
|
||||
currentIndex: SettingsData.animationVariant
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.set("animationVariant", index);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onAnimationVariantChanged() {
|
||||
animVariantGroup.currentIndex = SettingsData.animationVariant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: variantDescription.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
StyledText {
|
||||
id: variantDescription
|
||||
x: Theme.spacingM
|
||||
y: Theme.spacingS
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
text: {
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return I18n.tr("Fluent: Smooth cubic deceleration in, quick snap out — clean, elegant curves.");
|
||||
case 2:
|
||||
return I18n.tr("Dynamic: Spring bezier with overshoot — entry briefly exceeds its target then settles. Expressive and alive.");
|
||||
default:
|
||||
return I18n.tr("Material: Material Design 3 Expressive bezier curves. The DMS default feel.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "typography"
|
||||
tags: ["animation", "motion", "effect", "slide", "directional", "depth", "spring", "physics"]
|
||||
title: I18n.tr("Motion Effects")
|
||||
settingKey: "motionEffect"
|
||||
iconName: "motion_photos_on"
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: motionEffectGroup.implicitHeight
|
||||
clip: true
|
||||
|
||||
DankButtonGroup {
|
||||
id: motionEffectGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
|
||||
minButtonWidth: parent.width < 480 ? 64 : 96
|
||||
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
model: [I18n.tr("Standard"), I18n.tr("Directional"), I18n.tr("Depth")]
|
||||
selectionMode: "single"
|
||||
currentIndex: SettingsData.motionEffect
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.set("motionEffect", index);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onMotionEffectChanged() {
|
||||
motionEffectGroup.currentIndex = SettingsData.motionEffect;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: motionEffectDescription.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
StyledText {
|
||||
id: motionEffectDescription
|
||||
x: Theme.spacingM
|
||||
y: Theme.spacingS
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
text: {
|
||||
switch (SettingsData.motionEffect) {
|
||||
case 1:
|
||||
return I18n.tr("Directional: Panels glide in from a larger distance at full size — no scale change, pure clean motion.");
|
||||
case 2:
|
||||
return I18n.tr("Depth: Panels scale up from small as they slide in — a dramatic pop-forward depth effect.");
|
||||
default:
|
||||
return I18n.tr("Standard: Classic Material Design 3 — panels rise from below with a subtle scale. The DMS default.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "typography"
|
||||
tags: ["font", "family", "text", "typography"]
|
||||
@@ -285,12 +423,6 @@ Item {
|
||||
description: I18n.tr("Popouts and Modals follow global Animation Speed (disable to customize independently)")
|
||||
checked: SettingsData.syncComponentAnimationSpeeds
|
||||
onToggled: checked => SettingsData.set("syncComponentAnimationSpeeds", checked)
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onSyncComponentAnimationSpeedsChanged() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property string settingLabel: ""
|
||||
property string reason: ""
|
||||
property var parentModal: null
|
||||
|
||||
width: parent?.width ?? 0
|
||||
height: contentRow.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.primary, 0.08)
|
||||
border.color: Theme.withAlpha(Theme.primary, 0.18)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "frame_source"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - Theme.iconSize - openButton.width - Theme.spacingM * 2
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: root.settingLabel
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.reason
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.reason !== ""
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: openButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Open Frame")
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.primaryText
|
||||
buttonHeight: 32
|
||||
horizontalPadding: Theme.spacingM
|
||||
onClicked: {
|
||||
if (!root.parentModal)
|
||||
return;
|
||||
root.parentModal.showWithTabName("frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,6 @@ Item {
|
||||
description: modelData.width + "×" + modelData.height
|
||||
checked: localChecked
|
||||
onToggled: isChecked => {
|
||||
localChecked = isChecked;
|
||||
var prefs = JSON.parse(JSON.stringify(root.displayPreferences));
|
||||
if (!Array.isArray(prefs) || prefs.includes("all"))
|
||||
prefs = [];
|
||||
@@ -94,6 +93,11 @@ Item {
|
||||
model: modelData.model || ""
|
||||
});
|
||||
}
|
||||
if (prefs.length === 0) {
|
||||
localChecked = true;
|
||||
return;
|
||||
}
|
||||
localChecked = isChecked;
|
||||
root.preferencesChanged(prefs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,244 +48,256 @@ Scope {
|
||||
bottom: true
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [root]
|
||||
active: false
|
||||
property bool hasBeenActivated: false
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
hasBeenActivated = true
|
||||
}
|
||||
}
|
||||
onCleared: () => {
|
||||
if (hasBeenActivated && overviewScope.overviewOpen) {
|
||||
overviewScope.overviewOpen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: overviewScope
|
||||
function onOverviewOpenChanged() {
|
||||
if (overviewScope.overviewOpen) {
|
||||
grab.hasBeenActivated = false
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
delayedGrabTimer.start()
|
||||
} else {
|
||||
delayedGrabTimer.stop()
|
||||
grab.active = false
|
||||
grab.hasBeenActivated = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onMonitorIsFocusedChanged() {
|
||||
if (!CompositorService.useHyprlandFocusGrab)
|
||||
return;
|
||||
if (overviewScope.overviewOpen && root.monitorIsFocused && !grab.active) {
|
||||
grab.hasBeenActivated = false
|
||||
grab.active = true
|
||||
} else if (overviewScope.overviewOpen && !root.monitorIsFocused && grab.active) {
|
||||
grab.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: delayedGrabTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (CompositorService.useHyprlandFocusGrab && overviewScope.overviewOpen && root.monitorIsFocused) {
|
||||
grab.active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: Theme.expressiveDurations.expressiveDefaultSpatial + 120
|
||||
onTriggered: {
|
||||
root.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: overviewScope.overviewOpen ? 0.5 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: mouse => {
|
||||
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
||||
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
||||
overviewScope.overviewOpen = false
|
||||
closeTimer.restart()
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [root]
|
||||
active: false
|
||||
property bool hasBeenActivated: false
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
hasBeenActivated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 100
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
|
||||
opacity: overviewScope.overviewOpen ? 1 : 0
|
||||
transform: [scaleTransform, motionTransform]
|
||||
|
||||
Scale {
|
||||
id: scaleTransform
|
||||
origin.x: contentContainer.width / 2
|
||||
origin.y: contentContainer.height / 2
|
||||
xScale: overviewScope.overviewOpen ? 1 : 0.96
|
||||
yScale: overviewScope.overviewOpen ? 1 : 0.96
|
||||
|
||||
Behavior on xScale {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on yScale {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
onCleared: () => {
|
||||
if (hasBeenActivated && overviewScope.overviewOpen) {
|
||||
overviewScope.overviewOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Translate {
|
||||
id: motionTransform
|
||||
x: 0
|
||||
y: overviewScope.overviewOpen ? 0 : Theme.spacingL
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overviewLoader
|
||||
active: overviewScope.overviewOpen
|
||||
asynchronous: false
|
||||
|
||||
sourceComponent: OverviewWidget {
|
||||
panelWindow: root
|
||||
overviewOpen: overviewScope.overviewOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
anchors.fill: parent
|
||||
visible: overviewScope.overviewOpen
|
||||
focus: overviewScope.overviewOpen && root.monitorIsFocused
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
if (!root.monitorIsFocused) return
|
||||
overviewScope.overviewOpen = false
|
||||
closeTimer.restart()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (!root.monitorIsFocused) return
|
||||
|
||||
if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
|
||||
if (!overviewLoader.item) return
|
||||
|
||||
const thisMonitorWorkspaceIds = overviewLoader.item.thisMonitorWorkspaceIds
|
||||
if (thisMonitorWorkspaceIds.length === 0) return
|
||||
|
||||
const currentId = root.monitor.activeWorkspace?.id ?? thisMonitorWorkspaceIds[0]
|
||||
const currentIndex = thisMonitorWorkspaceIds.indexOf(currentId)
|
||||
|
||||
let targetIndex
|
||||
if (event.key === Qt.Key_Left) {
|
||||
targetIndex = currentIndex - 1
|
||||
if (targetIndex < 0) targetIndex = thisMonitorWorkspaceIds.length - 1
|
||||
Connections {
|
||||
target: overviewScope
|
||||
function onOverviewOpenChanged() {
|
||||
if (overviewScope.overviewOpen) {
|
||||
grab.hasBeenActivated = false;
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
delayedGrabTimer.start();
|
||||
} else {
|
||||
targetIndex = currentIndex + 1
|
||||
if (targetIndex >= thisMonitorWorkspaceIds.length) targetIndex = 0
|
||||
delayedGrabTimer.stop();
|
||||
grab.active = false;
|
||||
grab.hasBeenActivated = false;
|
||||
}
|
||||
|
||||
const targetId = thisMonitorWorkspaceIds[targetIndex]
|
||||
Hyprland.dispatch("workspace " + targetId)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && overviewScope.overviewOpen && root.monitorIsFocused) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onMonitorIsFocusedChanged() {
|
||||
if (root.monitorIsFocused && overviewScope.overviewOpen) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||
if (!CompositorService.useHyprlandFocusGrab)
|
||||
return;
|
||||
if (overviewScope.overviewOpen && root.monitorIsFocused && !grab.active) {
|
||||
grab.hasBeenActivated = false;
|
||||
grab.active = true;
|
||||
} else if (overviewScope.overviewOpen && !root.monitorIsFocused && grab.active) {
|
||||
grab.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: delayedGrabTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (CompositorService.useHyprlandFocusGrab && overviewScope.overviewOpen && root.monitorIsFocused) {
|
||||
grab.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: Theme.expressiveDurations.expressiveDefaultSpatial + 120
|
||||
onTriggered: {
|
||||
root.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: overviewScope.overviewOpen ? 0.5 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: mouse => {
|
||||
const localPos = mapToItem(contentAnchor, mouse.x, mouse.y);
|
||||
if (localPos.x < 0 || localPos.x > contentAnchor.width || localPos.y < 0 || localPos.y > contentAnchor.height) {
|
||||
overviewScope.overviewOpen = false;
|
||||
closeTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentAnchor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 100
|
||||
width: contentContainer.width
|
||||
height: contentContainer.height
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
transformOrigin: Item.Center
|
||||
|
||||
opacity: overviewScope.overviewOpen ? 1 : 0
|
||||
scale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
|
||||
x: {
|
||||
if (overviewScope.overviewOpen)
|
||||
return 0;
|
||||
if (Theme.isDepthEffect)
|
||||
return Theme.effectAnimOffset * 0.25;
|
||||
return 0;
|
||||
}
|
||||
y: {
|
||||
if (overviewScope.overviewOpen)
|
||||
return 0;
|
||||
if (Theme.isDirectionalEffect)
|
||||
return -Math.max(contentContainer.height * 0.8, Theme.effectAnimOffset * 1.1);
|
||||
if (Theme.isDepthEffect)
|
||||
return Math.max(Theme.effectAnimOffset * 0.85, 28);
|
||||
return Theme.effectAnimOffset;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overviewLoader
|
||||
active: overviewScope.overviewOpen
|
||||
asynchronous: false
|
||||
|
||||
sourceComponent: OverviewWidget {
|
||||
panelWindow: root
|
||||
overviewOpen: overviewScope.overviewOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
anchors.fill: parent
|
||||
visible: overviewScope.overviewOpen
|
||||
focus: overviewScope.overviewOpen && root.monitorIsFocused
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
if (!root.monitorIsFocused)
|
||||
return;
|
||||
overviewScope.overviewOpen = false;
|
||||
closeTimer.restart();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (!root.monitorIsFocused)
|
||||
return;
|
||||
if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
|
||||
if (!overviewLoader.item)
|
||||
return;
|
||||
const thisMonitorWorkspaceIds = overviewLoader.item.thisMonitorWorkspaceIds;
|
||||
if (thisMonitorWorkspaceIds.length === 0)
|
||||
return;
|
||||
const currentId = root.monitor.activeWorkspace?.id ?? thisMonitorWorkspaceIds[0];
|
||||
const currentIndex = thisMonitorWorkspaceIds.indexOf(currentId);
|
||||
|
||||
let targetIndex;
|
||||
if (event.key === Qt.Key_Left) {
|
||||
targetIndex = currentIndex - 1;
|
||||
if (targetIndex < 0)
|
||||
targetIndex = thisMonitorWorkspaceIds.length - 1;
|
||||
} else {
|
||||
targetIndex = currentIndex + 1;
|
||||
if (targetIndex >= thisMonitorWorkspaceIds.length)
|
||||
targetIndex = 0;
|
||||
}
|
||||
|
||||
const targetId = thisMonitorWorkspaceIds[targetIndex];
|
||||
Hyprland.dispatch("workspace " + targetId);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && overviewScope.overviewOpen && root.monitorIsFocused) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onMonitorIsFocusedChanged() {
|
||||
if (root.monitorIsFocused && overviewScope.overviewOpen) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && overviewScope.overviewOpen) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus());
|
||||
} else if (!visible) {
|
||||
grab.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: overviewScope
|
||||
function onOverviewOpenChanged() {
|
||||
if (overviewScope.overviewOpen) {
|
||||
closeTimer.stop();
|
||||
root.visible = true;
|
||||
Qt.callLater(() => focusScope.forceActiveFocus());
|
||||
} else {
|
||||
closeTimer.restart();
|
||||
grab.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && overviewScope.overviewOpen) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||
} else if (!visible) {
|
||||
grab.active = false
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: overviewScope
|
||||
function onOverviewOpenChanged() {
|
||||
if (overviewScope.overviewOpen) {
|
||||
closeTimer.stop()
|
||||
root.visible = true
|
||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||
} else {
|
||||
closeTimer.restart()
|
||||
grab.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@ Scope {
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: overlayWindow
|
||||
// Track the container's scale so blur shrinks with the content
|
||||
// during exit — otherwise blur pops away one frame after content.
|
||||
readonly property real s: Math.min(1, spotlightContainer.scale)
|
||||
readonly property bool active: overlayWindow.shouldShowSpotlight && spotlightContainer.opacity > 0
|
||||
blurX: spotlightContainer.x + spotlightContainer.width * (1 - s) * 0.5
|
||||
@@ -228,8 +230,26 @@ Scope {
|
||||
|
||||
Item {
|
||||
id: spotlightContainer
|
||||
|
||||
// Connected-frame mode: dock flush against the emerge-side frame
|
||||
// edge and slide in from beyond that edge. In any other mode the
|
||||
// spotlight stays centered — identical to master.
|
||||
readonly property string connectedEmergeSide: SettingsData.frameLauncherEmergeSide || "bottom"
|
||||
readonly property real _centerY: (parent.height - height) / 2
|
||||
readonly property real _connectedRestY: {
|
||||
if (!Theme.isConnectedEffect || !overlayWindow.screen)
|
||||
return _centerY;
|
||||
const inset = SettingsData.frameEdgeInsetForSide(overlayWindow.screen, connectedEmergeSide);
|
||||
return connectedEmergeSide === "top" ? inset : parent.height - height - inset;
|
||||
}
|
||||
readonly property real _connectedCollapsedY: connectedEmergeSide === "top" ? -height : parent.height
|
||||
|
||||
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
|
||||
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
|
||||
y: {
|
||||
if (!Theme.isConnectedEffect)
|
||||
return Theme.snap(_centerY, overlayWindow.dpr);
|
||||
return Theme.snap(overlayWindow.shouldShowSpotlight ? _connectedRestY : _connectedCollapsedY, overlayWindow.dpr);
|
||||
}
|
||||
|
||||
readonly property int baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
@@ -260,8 +280,8 @@ Scope {
|
||||
|
||||
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
||||
|
||||
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
|
||||
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
|
||||
scale: Theme.isConnectedEffect ? 1.0 : (overlayWindow.shouldShowSpotlight ? 1.0 : 0.96)
|
||||
opacity: Theme.isConnectedEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0)
|
||||
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
||||
enabled: overlayWindow.shouldShowSpotlight
|
||||
|
||||
@@ -270,6 +290,22 @@ Scope {
|
||||
layer.textureSize: layer.enabled ? Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr)) : Qt.size(0, 0)
|
||||
|
||||
Behavior on scale {
|
||||
id: scaleAnimation
|
||||
enabled: !Theme.isConnectedEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
onRunningChanged: {
|
||||
if (running || !spotlightContainer.animatingOut)
|
||||
return;
|
||||
niriOverviewScope.resetState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isConnectedEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.type: Easing.BezierSpline
|
||||
@@ -277,11 +313,20 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
// Connected-mode slide — only animates in full connected-frame mode.
|
||||
// Drives resetState when the slide-out finishes (scale/opacity are
|
||||
// static in connected mode so their onRunningChanged never fires).
|
||||
Behavior on y {
|
||||
enabled: Theme.isConnectedEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
duration: Theme.variantDuration(Theme.popoutAnimationDuration, overlayWindow.shouldShowSpotlight)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
easing.bezierCurve: overlayWindow.shouldShowSpotlight ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
onRunningChanged: {
|
||||
if (running || !spotlightContainer.animatingOut)
|
||||
return;
|
||||
niriOverviewScope.resetState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,30 +62,30 @@ Item {
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,16 +124,16 @@ Item {
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,10 +202,9 @@ Singleton {
|
||||
}
|
||||
|
||||
function unloadDankDash() {
|
||||
if (!dankDashPopoutLoader)
|
||||
return;
|
||||
dankDashPopout = null;
|
||||
dankDashPopoutLoader.active = false;
|
||||
// DankDash is intentionally kept alive after first use. Destroying this
|
||||
// lazy popout during its close signal can invalidate connected overlay
|
||||
// bindings while Qt is still unwinding the signal stack.
|
||||
}
|
||||
|
||||
function toggleDankDash(tabIndex, x, y, width, section, screen) {
|
||||
|
||||
151
quickshell/Widgets/ConnectedCorner.qml
Normal file
151
quickshell/Widgets/ConnectedCorner.qml
Normal file
@@ -0,0 +1,151 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import "../Common/ConnectorGeometry.js" as ConnectorGeometry
|
||||
|
||||
// Concave arc connector filling the gap between a bar corner and an adjacent surface.
|
||||
//
|
||||
// NOTE: FrameWindow now uses ConnectedShape.qml for frame-owned connected chrome
|
||||
// (unified single-path rendering). This component is still used by DankPopout's
|
||||
// own shadow source for non-frame-owned chrome (popouts on non-frame screens).
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string barSide: "top"
|
||||
property string placement: "left"
|
||||
property real spacing: 4
|
||||
property real connectorRadius: 12
|
||||
property color color: "transparent"
|
||||
property real edgeStrokeWidth: 0
|
||||
property color edgeStrokeColor: color
|
||||
property real dpr: 1
|
||||
|
||||
readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom"
|
||||
readonly property bool isPlacementLeft: placement === "left"
|
||||
readonly property real _edgeStrokeWidth: Math.max(0, edgeStrokeWidth)
|
||||
readonly property string arcCorner: ConnectorGeometry.arcCorner(barSide, placement)
|
||||
readonly property real pathStartX: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
return width;
|
||||
case "topRight":
|
||||
case "bottomLeft":
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real pathStartY: {
|
||||
switch (arcCorner) {
|
||||
case "bottomRight":
|
||||
return height;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real firstLineX: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
case "bottomLeft":
|
||||
return width;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real firstLineY: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
return height;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real secondLineX: {
|
||||
switch (arcCorner) {
|
||||
case "topRight":
|
||||
case "bottomLeft":
|
||||
case "bottomRight":
|
||||
return width;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real secondLineY: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
case "bottomLeft":
|
||||
return height;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real arcCenterX: arcCorner === "topRight" || arcCorner === "bottomRight" ? width : 0
|
||||
readonly property real arcCenterY: arcCorner === "bottomLeft" || arcCorner === "bottomRight" ? height : 0
|
||||
readonly property real arcStartAngle: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
return 90;
|
||||
case "bottomLeft":
|
||||
return 0;
|
||||
default:
|
||||
return -90;
|
||||
}
|
||||
}
|
||||
readonly property real arcSweepAngle: {
|
||||
switch (arcCorner) {
|
||||
case "topRight":
|
||||
return 90;
|
||||
default:
|
||||
return -90;
|
||||
}
|
||||
}
|
||||
|
||||
width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius)
|
||||
height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius
|
||||
|
||||
Shape {
|
||||
x: -root._edgeStrokeWidth
|
||||
y: -root._edgeStrokeWidth
|
||||
width: root.width + root._edgeStrokeWidth * 2
|
||||
height: root.height + root._edgeStrokeWidth * 2
|
||||
asynchronous: false
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeColor: root._edgeStrokeWidth > 0 ? root.edgeStrokeColor : "transparent"
|
||||
strokeWidth: root._edgeStrokeWidth * 2
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillRule: ShapePath.WindingFill
|
||||
startX: root.pathStartX + root._edgeStrokeWidth
|
||||
startY: root.pathStartY + root._edgeStrokeWidth
|
||||
|
||||
PathLine {
|
||||
x: root.firstLineX + root._edgeStrokeWidth
|
||||
y: root.firstLineY + root._edgeStrokeWidth
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.secondLineX + root._edgeStrokeWidth
|
||||
y: root.secondLineY + root._edgeStrokeWidth
|
||||
}
|
||||
|
||||
PathAngleArc {
|
||||
centerX: root.arcCenterX + root._edgeStrokeWidth
|
||||
centerY: root.arcCenterY + root._edgeStrokeWidth
|
||||
radiusX: root.connectorRadius
|
||||
radiusY: root.connectorRadius
|
||||
startAngle: root.arcStartAngle
|
||||
sweepAngle: root.arcSweepAngle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
414
quickshell/Widgets/ConnectedShape.qml
Normal file
414
quickshell/Widgets/ConnectedShape.qml
Normal file
@@ -0,0 +1,414 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
|
||||
// Unified connected silhouette: body + near/far concave arcs as one ShapePath.
|
||||
// Keeping the connected chrome in one path avoids sibling alignment seams.
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string barSide: "top"
|
||||
|
||||
property real bodyWidth: 0
|
||||
property real bodyHeight: 0
|
||||
|
||||
property real connectorRadius: 12
|
||||
property real startConnectorRadius: connectorRadius
|
||||
property real endConnectorRadius: connectorRadius
|
||||
property real farStartConnectorRadius: 0
|
||||
property real farEndConnectorRadius: 0
|
||||
|
||||
property real surfaceRadius: 12
|
||||
|
||||
property color fillColor: "transparent"
|
||||
|
||||
readonly property bool _horiz: barSide === "top" || barSide === "bottom"
|
||||
readonly property real _sc: Math.max(0, startConnectorRadius)
|
||||
readonly property real _ec: Math.max(0, endConnectorRadius)
|
||||
readonly property real _fsc: Math.max(0, farStartConnectorRadius)
|
||||
readonly property real _fec: Math.max(0, farEndConnectorRadius)
|
||||
readonly property real _firstCr: barSide === "left" ? _sc : _ec
|
||||
readonly property real _secondCr: barSide === "left" ? _ec : _sc
|
||||
readonly property real _firstFarCr: barSide === "left" ? _fsc : _fec
|
||||
readonly property real _secondFarCr: barSide === "left" ? _fec : _fsc
|
||||
readonly property real _farExtent: Math.max(_fsc, _fec)
|
||||
readonly property real _sr: Math.max(0, Math.min(surfaceRadius, (_horiz ? bodyWidth : bodyHeight) / 2, (_horiz ? bodyHeight : bodyWidth) / 2))
|
||||
readonly property real _firstSr: _firstFarCr > 0 ? 0 : _sr
|
||||
readonly property real _secondSr: _secondFarCr > 0 ? 0 : _sr
|
||||
readonly property real _firstFarInset: _firstFarCr > 0 ? _firstFarCr : _firstSr
|
||||
readonly property real _secondFarInset: _secondFarCr > 0 ? _secondFarCr : _secondSr
|
||||
|
||||
// Root-level aliases — PathArc/PathLine elements can't use `parent`.
|
||||
readonly property real _bw: bodyWidth
|
||||
readonly property real _bh: bodyHeight
|
||||
readonly property real _bodyLeft: _horiz ? _sc : (barSide === "right" ? _farExtent : 0)
|
||||
readonly property real _bodyRight: _bodyLeft + _bw
|
||||
readonly property real _bodyTop: _horiz ? (barSide === "bottom" ? _farExtent : 0) : _sc
|
||||
readonly property real _bodyBottom: _bodyTop + _bh
|
||||
readonly property real _totalW: _horiz ? _bw + _sc + _ec : _bw + _farExtent
|
||||
readonly property real _totalH: _horiz ? _bh + _farExtent : _bh + _sc + _ec
|
||||
|
||||
width: _totalW
|
||||
height: _totalH
|
||||
|
||||
readonly property real bodyX: root._bodyLeft
|
||||
readonly property real bodyY: root._bodyTop
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
antialiasing: true
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.fillColor
|
||||
strokeWidth: -1
|
||||
fillRule: ShapePath.WindingFill
|
||||
|
||||
// CW path: bar edge → concave arc → body → convex arc → far edge → convex arc → body → concave arc
|
||||
|
||||
startX: root.barSide === "right" ? root._totalW : 0
|
||||
startY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._totalH;
|
||||
case "left":
|
||||
return root._totalH;
|
||||
case "right":
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Bar edge
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return 0;
|
||||
case "right":
|
||||
return root._totalW;
|
||||
default:
|
||||
return root._totalW;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._totalH;
|
||||
case "left":
|
||||
return 0;
|
||||
case "right":
|
||||
return root._totalH;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concave arc 1
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._firstCr;
|
||||
case "right":
|
||||
return -root._firstCr;
|
||||
default:
|
||||
return -root._firstCr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return -root._firstCr;
|
||||
case "left":
|
||||
return root._firstCr;
|
||||
case "right":
|
||||
return -root._firstCr;
|
||||
default:
|
||||
return root._firstCr;
|
||||
}
|
||||
}
|
||||
radiusX: root._firstCr
|
||||
radiusY: root._firstCr
|
||||
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
// Body edge to first convex corner
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._bodyRight - root._firstSr;
|
||||
case "right":
|
||||
return root._bodyLeft + root._firstSr;
|
||||
default:
|
||||
return root._bodyRight;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._bodyTop + root._firstSr;
|
||||
case "left":
|
||||
return root._bodyTop;
|
||||
case "right":
|
||||
return root._bodyBottom;
|
||||
default:
|
||||
return root._bodyBottom - root._firstSr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convex arc 1
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._firstSr;
|
||||
case "right":
|
||||
return -root._firstSr;
|
||||
default:
|
||||
return -root._firstSr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return -root._firstSr;
|
||||
case "left":
|
||||
return root._firstSr;
|
||||
case "right":
|
||||
return -root._firstSr;
|
||||
default:
|
||||
return root._firstSr;
|
||||
}
|
||||
}
|
||||
radiusX: root._firstSr
|
||||
radiusY: root._firstSr
|
||||
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
|
||||
}
|
||||
|
||||
// Opposite-side connector 1
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._firstFarCr > 0 ? root._bodyRight + root._firstFarCr : root._bodyRight;
|
||||
case "right":
|
||||
return root._firstFarCr > 0 ? root._bodyLeft - root._firstFarCr : root._bodyLeft;
|
||||
default:
|
||||
return root._firstFarCr > 0 ? root._bodyRight : root._bodyRight - root._firstSr;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._firstFarCr > 0 ? root._bodyTop - root._firstFarCr : root._bodyTop;
|
||||
case "left":
|
||||
return root._firstFarCr > 0 ? root._bodyTop : root._bodyTop + root._firstSr;
|
||||
case "right":
|
||||
return root._firstFarCr > 0 ? root._bodyBottom : root._bodyBottom - root._firstSr;
|
||||
default:
|
||||
return root._firstFarCr > 0 ? root._bodyBottom + root._firstFarCr : root._bodyBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return -root._firstFarCr;
|
||||
case "right":
|
||||
return root._firstFarCr;
|
||||
default:
|
||||
return -root._firstFarCr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._firstFarCr;
|
||||
case "left":
|
||||
return root._firstFarCr;
|
||||
case "right":
|
||||
return -root._firstFarCr;
|
||||
default:
|
||||
return -root._firstFarCr;
|
||||
}
|
||||
}
|
||||
radiusX: root._firstFarCr
|
||||
radiusY: root._firstFarCr
|
||||
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
// Far edge
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._bodyRight;
|
||||
case "right":
|
||||
return root._bodyLeft;
|
||||
default:
|
||||
return root._bodyLeft + root._secondFarInset;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._bodyTop;
|
||||
case "left":
|
||||
return root._bodyBottom - root._secondFarInset;
|
||||
case "right":
|
||||
return root._bodyTop + root._secondFarInset;
|
||||
default:
|
||||
return root._bodyBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Opposite-side connector 2
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._secondFarCr;
|
||||
case "right":
|
||||
return -root._secondFarCr;
|
||||
default:
|
||||
return -root._secondFarCr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return -root._secondFarCr;
|
||||
case "left":
|
||||
return root._secondFarCr;
|
||||
case "right":
|
||||
return -root._secondFarCr;
|
||||
default:
|
||||
return root._secondFarCr;
|
||||
}
|
||||
}
|
||||
radiusX: root._secondFarCr
|
||||
radiusY: root._secondFarCr
|
||||
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._secondFarCr > 0 ? root._bodyRight : root._bodyRight;
|
||||
case "right":
|
||||
return root._secondFarCr > 0 ? root._bodyLeft : root._bodyLeft;
|
||||
default:
|
||||
return root._secondFarCr > 0 ? root._bodyLeft : root._bodyLeft + root._secondSr;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._secondFarCr > 0 ? root._bodyTop : root._bodyTop;
|
||||
case "left":
|
||||
return root._secondFarCr > 0 ? root._bodyBottom : root._bodyBottom - root._secondSr;
|
||||
case "right":
|
||||
return root._secondFarCr > 0 ? root._bodyTop : root._bodyTop + root._secondSr;
|
||||
default:
|
||||
return root._secondFarCr > 0 ? root._bodyBottom : root._bodyBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convex arc 2
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return -root._secondSr;
|
||||
case "right":
|
||||
return root._secondSr;
|
||||
default:
|
||||
return -root._secondSr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._secondSr;
|
||||
case "left":
|
||||
return root._secondSr;
|
||||
case "right":
|
||||
return -root._secondSr;
|
||||
default:
|
||||
return -root._secondSr;
|
||||
}
|
||||
}
|
||||
radiusX: root._secondSr
|
||||
radiusY: root._secondSr
|
||||
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
|
||||
}
|
||||
|
||||
// Body edge to second concave arc
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._bodyLeft + root._ec;
|
||||
case "right":
|
||||
return root._bodyRight - root._sc;
|
||||
default:
|
||||
return root._bodyLeft;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._bodyBottom - root._sc;
|
||||
case "left":
|
||||
return root._bodyBottom;
|
||||
case "right":
|
||||
return root._bodyTop;
|
||||
default:
|
||||
return root._bodyTop + root._sc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concave arc 2
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return -root._secondCr;
|
||||
case "right":
|
||||
return root._secondCr;
|
||||
default:
|
||||
return -root._secondCr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._secondCr;
|
||||
case "left":
|
||||
return root._secondCr;
|
||||
case "right":
|
||||
return -root._secondCr;
|
||||
default:
|
||||
return -root._secondCr;
|
||||
}
|
||||
}
|
||||
radiusX: root._secondCr
|
||||
radiusY: root._secondCr
|
||||
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,8 @@ Rectangle {
|
||||
|
||||
Behavior on scale {
|
||||
enabled: enableScaleAnimation && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
DankAnim {
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: 100
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ ColumnLayout {
|
||||
|
||||
Behavior on rotation {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
DankAnim {
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.shortDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
@@ -88,7 +89,8 @@ ColumnLayout {
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
DankAnim {
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.shortDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
@@ -108,7 +110,8 @@ ColumnLayout {
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
DankAnim {
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.shortDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
@@ -9,11 +7,8 @@ Item {
|
||||
readonly property var log: Log.scoped("DankPopout")
|
||||
|
||||
property string layerNamespace: "dms:popout"
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property Component content: null
|
||||
property Component overlayContent: null
|
||||
property alias overlayLoader: overlayLoader
|
||||
readonly property alias backgroundWindow: backgroundWindow
|
||||
property real popupWidth: 400
|
||||
property real popupHeight: 300
|
||||
property real triggerX: 0
|
||||
@@ -22,10 +17,10 @@ Item {
|
||||
property string triggerSection: ""
|
||||
property string positioning: "center"
|
||||
property int animationDuration: Theme.popoutAnimationDuration
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||
property real animationOffset: Theme.effectAnimOffset
|
||||
property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve
|
||||
property list<real> animationExitCurve: Theme.variantPopoutExitCurve
|
||||
property bool suspendShadowWhileResizing: false
|
||||
property bool shouldBeVisible: false
|
||||
property var customKeyboardFocus: null
|
||||
@@ -33,9 +28,6 @@ Item {
|
||||
property bool contentHandlesKeys: false
|
||||
property bool fullHeightSurface: false
|
||||
property bool _primeContent: false
|
||||
property bool _resizeActive: false
|
||||
property real _surfaceMarginLeft: 0
|
||||
property real _surfaceW: 0
|
||||
|
||||
property real storedBarThickness: Theme.barHeight - 4
|
||||
property real storedBarSpacing: 4
|
||||
@@ -47,90 +39,118 @@ Item {
|
||||
"rightBar": 0
|
||||
})
|
||||
property var screen: null
|
||||
|
||||
readonly property real effectiveBarThickness: {
|
||||
const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4;
|
||||
return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing;
|
||||
}
|
||||
|
||||
readonly property var barBounds: {
|
||||
if (!screen)
|
||||
return {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"wingSize": 0
|
||||
};
|
||||
return SettingsData.getBarBounds(screen, effectiveBarThickness, effectiveBarPosition, storedBarConfig);
|
||||
}
|
||||
|
||||
readonly property real barX: barBounds.x
|
||||
readonly property real barY: barBounds.y
|
||||
readonly property real barWidth: barBounds.width
|
||||
readonly property real barHeight: barBounds.height
|
||||
readonly property real barWingSize: barBounds.wingSize
|
||||
property int effectiveBarPosition: 0
|
||||
property real effectiveBarBottomGap: 0
|
||||
|
||||
signal opened
|
||||
signal popoutClosed
|
||||
signal backgroundClicked
|
||||
|
||||
property var _lastOpenedScreen: null
|
||||
readonly property var contentLoader: impl.item ? impl.item.contentLoader : _fallbackContentLoader
|
||||
readonly property var overlayLoader: impl.item ? impl.item.overlayLoader : _fallbackOverlayLoader
|
||||
readonly property var backgroundWindow: impl.item ? impl.item.backgroundWindow : null
|
||||
|
||||
property int effectiveBarPosition: 0
|
||||
property real effectiveBarBottomGap: 0
|
||||
readonly property string autoBarShadowDirection: {
|
||||
const section = triggerSection || "center";
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Top:
|
||||
if (section === "left")
|
||||
return "topLeft";
|
||||
if (section === "right")
|
||||
return "topRight";
|
||||
return "top";
|
||||
case SettingsData.Position.Bottom:
|
||||
if (section === "left")
|
||||
return "bottomLeft";
|
||||
if (section === "right")
|
||||
return "bottomRight";
|
||||
return "bottom";
|
||||
case SettingsData.Position.Left:
|
||||
if (section === "left")
|
||||
return "topLeft";
|
||||
if (section === "right")
|
||||
return "bottomLeft";
|
||||
return "left";
|
||||
case SettingsData.Position.Right:
|
||||
if (section === "left")
|
||||
return "topRight";
|
||||
if (section === "right")
|
||||
return "bottomRight";
|
||||
return "right";
|
||||
default:
|
||||
return "top";
|
||||
Loader {
|
||||
id: _fallbackContentLoader
|
||||
active: false
|
||||
}
|
||||
Loader {
|
||||
id: _fallbackOverlayLoader
|
||||
active: false
|
||||
}
|
||||
readonly property bool isClosing: impl.item ? (impl.item.isClosing ?? false) : false
|
||||
readonly property real dpr: impl.item ? impl.item.dpr : 1
|
||||
readonly property real screenWidth: impl.item ? impl.item.screenWidth : 0
|
||||
readonly property real screenHeight: impl.item ? impl.item.screenHeight : 0
|
||||
readonly property real alignedX: impl.item ? impl.item.alignedX : 0
|
||||
readonly property real alignedY: impl.item ? impl.item.alignedY : 0
|
||||
readonly property real alignedWidth: impl.item ? impl.item.alignedWidth : 0
|
||||
readonly property real alignedHeight: impl.item ? impl.item.alignedHeight : 0
|
||||
readonly property real maskX: impl.item ? impl.item.maskX : 0
|
||||
readonly property real maskY: impl.item ? impl.item.maskY : 0
|
||||
readonly property real maskWidth: impl.item ? impl.item.maskWidth : 0
|
||||
readonly property real maskHeight: impl.item ? impl.item.maskHeight : 0
|
||||
readonly property real barX: impl.item ? impl.item.barX : 0
|
||||
readonly property real barY: impl.item ? impl.item.barY : 0
|
||||
readonly property real barWidth: impl.item ? impl.item.barWidth : 0
|
||||
readonly property real barHeight: impl.item ? impl.item.barHeight : 0
|
||||
readonly property bool useConnectedBackend: _usesConnectedBackendForScreen(screen)
|
||||
property var _resolvedBackend: null
|
||||
property bool _pendingOpen: false
|
||||
|
||||
Timer {
|
||||
id: _pendingOpenTimer
|
||||
interval: 0
|
||||
onTriggered: {
|
||||
if (!root._pendingOpen || !impl.item)
|
||||
return;
|
||||
root._pendingOpen = false;
|
||||
impl.item.open();
|
||||
}
|
||||
}
|
||||
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
|
||||
|
||||
// Snapshot mask geometry to prevent background damage on bar updates
|
||||
property real _frozenMaskX: 0
|
||||
property real _frozenMaskY: 0
|
||||
property real _frozenMaskWidth: 0
|
||||
property real _frozenMaskHeight: 0
|
||||
onUseConnectedBackendChanged: _maybeResolveBackend()
|
||||
Component.onCompleted: _resolvedBackend = _backendForScreen(screen)
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onConnectedFrameModeActiveChanged() {
|
||||
root._maybeResolveBackend();
|
||||
}
|
||||
function onFrameScreenPreferencesChanged() {
|
||||
root._maybeResolveBackend();
|
||||
}
|
||||
}
|
||||
|
||||
function _usesConnectedBackendForScreen(targetScreen) {
|
||||
return SettingsData.connectedFrameModeActive && !!targetScreen && SettingsData.isScreenInPreferences(targetScreen, SettingsData.frameScreenPreferences);
|
||||
}
|
||||
|
||||
function _backendForScreen(targetScreen) {
|
||||
return _usesConnectedBackendForScreen(targetScreen) ? connectedComp : standaloneComp;
|
||||
}
|
||||
|
||||
// Defer Loader source-component swap until impl is fully closed; avoids
|
||||
// tearing down a popout mid-animation when frame mode is toggled.
|
||||
function _maybeResolveBackend() {
|
||||
_resolveBackendForScreen(screen);
|
||||
}
|
||||
|
||||
function _resolveBackendForScreen(targetScreen) {
|
||||
const backend = _backendForScreen(targetScreen);
|
||||
if (_resolvedBackend === backend)
|
||||
return;
|
||||
if (impl.item && (impl.item.shouldBeVisible || impl.item.isClosing))
|
||||
return;
|
||||
_resolvedBackend = backend;
|
||||
}
|
||||
|
||||
function open() {
|
||||
_maybeResolveBackend();
|
||||
if (impl.item) {
|
||||
_pendingOpen = false;
|
||||
impl.item.open();
|
||||
return;
|
||||
}
|
||||
_pendingOpen = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
_pendingOpen = false;
|
||||
_pendingOpenTimer.stop();
|
||||
if (impl.item)
|
||||
impl.item.close();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
(shouldBeVisible || _pendingOpen) ? close() : open();
|
||||
}
|
||||
|
||||
function setBarContext(position, bottomGap) {
|
||||
effectiveBarPosition = position !== undefined ? position : 0;
|
||||
effectiveBarBottomGap = bottomGap !== undefined ? bottomGap : 0;
|
||||
}
|
||||
|
||||
function primeContent() {
|
||||
_primeContent = true;
|
||||
}
|
||||
|
||||
function clearPrimedContent() {
|
||||
_primeContent = false;
|
||||
}
|
||||
|
||||
function setTriggerPosition(x, y, width, section, targetScreen, barPosition, barThickness, barSpacing, barConfig) {
|
||||
triggerX = x;
|
||||
triggerY = y;
|
||||
@@ -147,477 +167,112 @@ Item {
|
||||
|
||||
adjacentBarInfo = SettingsData.getAdjacentBarInfo(targetScreen, pos, barConfig);
|
||||
setBarContext(pos, bottomGap);
|
||||
_resolveBackendForScreen(targetScreen);
|
||||
}
|
||||
|
||||
readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab
|
||||
|
||||
function updateSurfacePosition() {
|
||||
if (useBackgroundWindow && shouldBeVisible) {
|
||||
_surfaceMarginLeft = alignedX - shadowBuffer;
|
||||
_surfaceW = alignedWidth + shadowBuffer * 2;
|
||||
}
|
||||
if (impl.item && typeof impl.item.updateSurfacePosition === "function")
|
||||
impl.item.updateSurfacePosition();
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!screen)
|
||||
Loader {
|
||||
id: impl
|
||||
active: root.screen !== null
|
||||
sourceComponent: root._resolvedBackend
|
||||
onItemChanged: if (item)
|
||||
root._wireBackend(item)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: standaloneComp
|
||||
DankPopoutStandalone {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: connectedComp
|
||||
DankPopoutConnected {}
|
||||
}
|
||||
|
||||
function _wireBackend(it) {
|
||||
if (!it)
|
||||
return;
|
||||
closeTimer.stop();
|
||||
|
||||
// Snapshot mask geometry
|
||||
_frozenMaskX = maskX;
|
||||
_frozenMaskY = maskY;
|
||||
_frozenMaskWidth = maskWidth;
|
||||
_frozenMaskHeight = maskHeight;
|
||||
it.popoutHandle = root;
|
||||
it.layerNamespace = Qt.binding(() => root.layerNamespace);
|
||||
it.content = Qt.binding(() => root.content);
|
||||
it.overlayContent = Qt.binding(() => root.overlayContent);
|
||||
it.popupWidth = Qt.binding(() => root.popupWidth);
|
||||
it.popupHeight = Qt.binding(() => root.popupHeight);
|
||||
it.triggerX = Qt.binding(() => root.triggerX);
|
||||
it.triggerY = Qt.binding(() => root.triggerY);
|
||||
it.triggerWidth = Qt.binding(() => root.triggerWidth);
|
||||
it.triggerSection = Qt.binding(() => root.triggerSection);
|
||||
it.positioning = Qt.binding(() => root.positioning);
|
||||
it.animationDuration = Qt.binding(() => root.animationDuration);
|
||||
it.animationScaleCollapsed = Qt.binding(() => root.animationScaleCollapsed);
|
||||
it.animationOffset = Qt.binding(() => root.animationOffset);
|
||||
it.animationEnterCurve = Qt.binding(() => root.animationEnterCurve);
|
||||
it.animationExitCurve = Qt.binding(() => root.animationExitCurve);
|
||||
it.suspendShadowWhileResizing = Qt.binding(() => root.suspendShadowWhileResizing);
|
||||
it.customKeyboardFocus = Qt.binding(() => root.customKeyboardFocus);
|
||||
it.backgroundInteractive = Qt.binding(() => root.backgroundInteractive);
|
||||
it.contentHandlesKeys = Qt.binding(() => root.contentHandlesKeys);
|
||||
it.fullHeightSurface = Qt.binding(() => root.fullHeightSurface);
|
||||
it.storedBarThickness = Qt.binding(() => root.storedBarThickness);
|
||||
it.storedBarSpacing = Qt.binding(() => root.storedBarSpacing);
|
||||
it.storedBarConfig = Qt.binding(() => root.storedBarConfig);
|
||||
it.adjacentBarInfo = Qt.binding(() => root.adjacentBarInfo);
|
||||
it.screen = Qt.binding(() => root.screen);
|
||||
it.effectiveBarPosition = Qt.binding(() => root.effectiveBarPosition);
|
||||
it.effectiveBarBottomGap = Qt.binding(() => root.effectiveBarBottomGap);
|
||||
|
||||
if (_lastOpenedScreen !== null && _lastOpenedScreen !== screen) {
|
||||
contentWindow.visible = false;
|
||||
if (useBackgroundWindow)
|
||||
backgroundWindow.visible = false;
|
||||
}
|
||||
_lastOpenedScreen = screen;
|
||||
|
||||
shouldBeVisible = true;
|
||||
if (useBackgroundWindow) {
|
||||
_surfaceMarginLeft = alignedX - shadowBuffer;
|
||||
_surfaceW = alignedWidth + shadowBuffer * 2;
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
if (shouldBeVisible && screen) {
|
||||
if (useBackgroundWindow)
|
||||
backgroundWindow.visible = true;
|
||||
contentWindow.visible = true;
|
||||
PopoutManager.showPopout(root);
|
||||
opened();
|
||||
}
|
||||
});
|
||||
it.shouldBeVisible = root.shouldBeVisible;
|
||||
if (root._primeContent && typeof it.primeContent === "function")
|
||||
it.primeContent();
|
||||
if (_pendingOpen)
|
||||
_pendingOpenTimer.restart();
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false;
|
||||
function primeContent() {
|
||||
_primeContent = true;
|
||||
if (impl.item)
|
||||
impl.item.primeContent();
|
||||
}
|
||||
|
||||
function clearPrimedContent() {
|
||||
_primeContent = false;
|
||||
PopoutManager.popoutChanged();
|
||||
closeTimer.restart();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
shouldBeVisible ? close() : open();
|
||||
if (impl.item)
|
||||
impl.item.clearPrimedContent();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (!shouldBeVisible || !screen)
|
||||
return;
|
||||
const currentScreenName = screen.name;
|
||||
let screenStillExists = false;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === currentScreenName) {
|
||||
screenStillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!screenStillExists) {
|
||||
close();
|
||||
}
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (impl.item && impl.item.shouldBeVisible !== root.shouldBeVisible)
|
||||
impl.item.shouldBeVisible = root.shouldBeVisible;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration
|
||||
onTriggered: {
|
||||
if (!shouldBeVisible) {
|
||||
contentWindow.visible = false;
|
||||
if (useBackgroundWindow)
|
||||
backgroundWindow.visible = false;
|
||||
PopoutManager.hidePopout(root);
|
||||
popoutClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: impl.item
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
readonly property real screenWidth: screen ? screen.width : 0
|
||||
readonly property real screenHeight: screen ? screen.height : 0
|
||||
readonly property real dpr: screen ? screen.devicePixelRatio : 1
|
||||
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowMotionPadding: Math.max(0, animationOffset)
|
||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
||||
|
||||
onAlignedHeightChanged: {
|
||||
if (!suspendShadowWhileResizing || !shouldBeVisible)
|
||||
return;
|
||||
_resizeActive = true;
|
||||
resizeSettleTimer.restart();
|
||||
}
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible) {
|
||||
_resizeActive = false;
|
||||
resizeSettleTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resizeSettleTimer
|
||||
interval: 80
|
||||
repeat: false
|
||||
onTriggered: root._resizeActive = false
|
||||
}
|
||||
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
||||
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
||||
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
||||
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Left:
|
||||
return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX));
|
||||
case SettingsData.Position.Right:
|
||||
return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX - popupWidth));
|
||||
default:
|
||||
const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
|
||||
const minX = adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : popupGap;
|
||||
const maxX = screenWidth - popupWidth - (adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : popupGap);
|
||||
return Math.max(minX, Math.min(maxX, rawX));
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
readonly property real alignedY: Theme.snap((() => {
|
||||
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
||||
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
||||
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
||||
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Bottom:
|
||||
return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY - popupHeight));
|
||||
case SettingsData.Position.Top:
|
||||
return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY));
|
||||
default:
|
||||
const rawY = triggerY - (popupHeight / 2);
|
||||
const minY = adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : popupGap;
|
||||
const maxY = screenHeight - popupHeight - (adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : popupGap);
|
||||
return Math.max(minY, Math.min(maxY, rawY));
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
readonly property real triggeringBarLeftExclusion: (effectiveBarPosition === SettingsData.Position.Left && barWidth > 0) ? Math.max(0, barX + barWidth) : 0
|
||||
readonly property real triggeringBarTopExclusion: (effectiveBarPosition === SettingsData.Position.Top && barHeight > 0) ? Math.max(0, barY + barHeight) : 0
|
||||
readonly property real triggeringBarRightExclusion: (effectiveBarPosition === SettingsData.Position.Right && barWidth > 0) ? Math.max(0, screenWidth - barX) : 0
|
||||
readonly property real triggeringBarBottomExclusion: (effectiveBarPosition === SettingsData.Position.Bottom && barHeight > 0) ? Math.max(0, screenHeight - barY) : 0
|
||||
|
||||
readonly property real maskX: {
|
||||
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
|
||||
return Math.max(triggeringBarLeftExclusion, adjacentLeftBar);
|
||||
}
|
||||
|
||||
readonly property real maskY: {
|
||||
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
|
||||
return Math.max(triggeringBarTopExclusion, adjacentTopBar);
|
||||
}
|
||||
|
||||
readonly property real maskWidth: {
|
||||
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
|
||||
const rightExclusion = Math.max(triggeringBarRightExclusion, adjacentRightBar);
|
||||
return Math.max(100, screenWidth - maskX - rightExclusion);
|
||||
}
|
||||
|
||||
readonly property real maskHeight: {
|
||||
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
|
||||
const bottomExclusion = Math.max(triggeringBarBottomExclusion, adjacentBottomBar);
|
||||
return Math.max(100, screenHeight - maskY - bottomExclusion);
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: backgroundWindow
|
||||
screen: root.screen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
Component.onCompleted: {
|
||||
if (typeof updatesEnabled !== "undefined" && !root.overlayContent)
|
||||
updatesEnabled = false;
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (impl.item && root.shouldBeVisible !== impl.item.shouldBeVisible)
|
||||
root.shouldBeVisible = impl.item.shouldBeVisible;
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace + ":background"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
function onOpened() {
|
||||
root.opened();
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: maskRect
|
||||
function onPopoutClosed() {
|
||||
root.popoutClosed();
|
||||
root._maybeResolveBackend();
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: maskRect
|
||||
visible: false
|
||||
color: "transparent"
|
||||
x: root._frozenMaskX
|
||||
y: root._frozenMaskY
|
||||
width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0
|
||||
height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: root._frozenMaskX
|
||||
y: root._frozenMaskY
|
||||
width: root._frozenMaskWidth
|
||||
height: root._frozenMaskHeight
|
||||
hoverEnabled: false
|
||||
enabled: shouldBeVisible && backgroundInteractive
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
const clickX = mouse.x + root._frozenMaskX;
|
||||
const clickY = mouse.y + root._frozenMaskY;
|
||||
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
|
||||
|
||||
if (!outsideContent)
|
||||
return;
|
||||
backgroundClicked();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overlayLoader
|
||||
anchors.fill: parent
|
||||
active: root.overlayContent !== null && backgroundWindow.visible
|
||||
sourceComponent: root.overlayContent
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
screen: root.screen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WindowBlur {
|
||||
id: popoutBlur
|
||||
targetWindow: contentWindow
|
||||
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
||||
blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
|
||||
blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
|
||||
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0
|
||||
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_POPOUT_LAYER")) {
|
||||
case "bottom":
|
||||
log.warn("'bottom' layer is not valid for popouts. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
log.warn("'background' layer is not valid for popouts. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (customKeyboardFocus !== null)
|
||||
return customKeyboardFocus;
|
||||
if (!shouldBeVisible)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
|
||||
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: !useBackgroundWindow
|
||||
bottom: _fullHeight || !useBackgroundWindow
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: useBackgroundWindow ? root._surfaceMarginLeft : 0
|
||||
top: (useBackgroundWindow && !_fullHeight) ? (root.alignedY - shadowBuffer) : 0
|
||||
}
|
||||
|
||||
implicitWidth: useBackgroundWindow ? root._surfaceW : 0
|
||||
implicitHeight: (useBackgroundWindow && !_fullHeight) ? (root.alignedHeight + shadowBuffer * 2) : 0
|
||||
|
||||
mask: useBackgroundWindow ? contentInputMask : null
|
||||
|
||||
Region {
|
||||
id: contentInputMask
|
||||
item: contentMaskRect
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentMaskRect
|
||||
visible: false
|
||||
x: contentContainer.x
|
||||
y: contentContainer.y
|
||||
width: shouldBeVisible ? root.alignedWidth : 0
|
||||
height: shouldBeVisible ? root.alignedHeight : 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !useBackgroundWindow && shouldBeVisible && backgroundInteractive
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
z: -1
|
||||
onClicked: mouse => {
|
||||
const clickX = mouse.x;
|
||||
const clickY = mouse.y;
|
||||
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
|
||||
if (!outsideContent)
|
||||
return;
|
||||
backgroundClicked();
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
x: useBackgroundWindow ? shadowBuffer : root.alignedX
|
||||
y: (useBackgroundWindow && !contentWindow._fullHeight) ? shadowBuffer : root.alignedY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top
|
||||
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
|
||||
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
|
||||
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
|
||||
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
||||
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
|
||||
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr);
|
||||
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: shadowSource
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
level: root.shadowLevel
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: Theme.cornerRadius
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) && !BlurService.enabled
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
layer.enabled: contentWrapper.opacity < 1
|
||||
layer.smooth: false
|
||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root._primeContent || shouldBeVisible || contentWindow.visible
|
||||
asynchronous: false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
visible: contentWrapper.visible
|
||||
radius: Theme.cornerRadius
|
||||
antialiasing: true
|
||||
color: "transparent"
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||
z: 100
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusHelper
|
||||
parent: contentContainer
|
||||
anchors.fill: parent
|
||||
visible: !root.contentHandlesKeys
|
||||
enabled: !root.contentHandlesKeys
|
||||
focus: !root.contentHandlesKeys
|
||||
Keys.onPressed: event => {
|
||||
if (root.contentHandlesKeys)
|
||||
return;
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
close();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
function onBackgroundClicked() {
|
||||
root.backgroundClicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1142
quickshell/Widgets/DankPopoutConnected.qml
Normal file
1142
quickshell/Widgets/DankPopoutConnected.qml
Normal file
File diff suppressed because it is too large
Load Diff
893
quickshell/Widgets/DankPopoutStandalone.qml
Normal file
893
quickshell/Widgets/DankPopoutStandalone.qml
Normal file
@@ -0,0 +1,893 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property var log: Log.scoped("DankPopoutStandalone")
|
||||
|
||||
property var popoutHandle: root
|
||||
property string layerNamespace: "dms:popout"
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property Component overlayContent: null
|
||||
property alias overlayLoader: overlayLoader
|
||||
readonly property alias backgroundWindow: backgroundWindow
|
||||
property real popupWidth: 400
|
||||
property real popupHeight: 300
|
||||
property real triggerX: 0
|
||||
property real triggerY: 0
|
||||
property real triggerWidth: 40
|
||||
property string triggerSection: ""
|
||||
property string positioning: "center"
|
||||
property int animationDuration: Theme.popoutAnimationDuration
|
||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||
property real animationOffset: Theme.effectAnimOffset
|
||||
property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve
|
||||
property list<real> animationExitCurve: Theme.variantPopoutExitCurve
|
||||
property bool suspendShadowWhileResizing: false
|
||||
property bool shouldBeVisible: false
|
||||
property bool isClosing: false
|
||||
property bool animationsEnabled: true
|
||||
property var customKeyboardFocus: null
|
||||
property bool backgroundInteractive: true
|
||||
property bool contentHandlesKeys: false
|
||||
property bool fullHeightSurface: false
|
||||
property bool _primeContent: false
|
||||
property bool _resizeActive: false
|
||||
property real _surfaceMarginLeft: 0
|
||||
property real _surfaceMarginTop: 0
|
||||
property real _surfaceW: 0
|
||||
property real _surfaceH: 0
|
||||
property real _surfaceBodyX: 0
|
||||
property real _surfaceBodyY: 0
|
||||
property real _surfaceBodyW: 0
|
||||
property real _surfaceBodyH: 0
|
||||
|
||||
property real storedBarThickness: Theme.barHeight - 4
|
||||
property real storedBarSpacing: 4
|
||||
property var storedBarConfig: null
|
||||
property var adjacentBarInfo: ({
|
||||
"topBar": 0,
|
||||
"bottomBar": 0,
|
||||
"leftBar": 0,
|
||||
"rightBar": 0
|
||||
})
|
||||
property var screen: null
|
||||
readonly property bool frameOnlyNoConnected: SettingsData.frameEnabled && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences)
|
||||
readonly property bool fluidStandaloneActive: Theme.isDirectionalEffect
|
||||
readonly property bool backgroundDismissWindowRequired: backgroundInteractive
|
||||
readonly property bool backgroundWindowRequired: backgroundDismissWindowRequired || root.overlayContent !== null
|
||||
readonly property bool _fullHeight: fullHeightSurface
|
||||
|
||||
function _frameEdgeInset(side) {
|
||||
if (!screen)
|
||||
return 0;
|
||||
return SettingsData.frameEdgeInsetForSide(screen, side);
|
||||
}
|
||||
|
||||
function _frameGapMargin(side) {
|
||||
return _frameEdgeInset(side) + Theme.popupDistance;
|
||||
}
|
||||
|
||||
function _edgeClearance(side, popupGap, adjacentInset) {
|
||||
if (frameOnlyNoConnected)
|
||||
return Math.max(adjacentInset, _frameGapMargin(side));
|
||||
return adjacentInset > 0 ? adjacentInset : popupGap;
|
||||
}
|
||||
|
||||
readonly property real effectiveBarThickness: {
|
||||
const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4;
|
||||
return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing;
|
||||
}
|
||||
|
||||
readonly property var barBounds: {
|
||||
if (!screen)
|
||||
return {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"wingSize": 0
|
||||
};
|
||||
return SettingsData.getBarBounds(screen, effectiveBarThickness, effectiveBarPosition, storedBarConfig);
|
||||
}
|
||||
|
||||
readonly property real barX: barBounds.x
|
||||
readonly property real barY: barBounds.y
|
||||
readonly property real barWidth: barBounds.width
|
||||
readonly property real barHeight: barBounds.height
|
||||
readonly property real barWingSize: barBounds.wingSize
|
||||
|
||||
signal opened
|
||||
signal popoutClosed
|
||||
signal backgroundClicked
|
||||
|
||||
property var _lastOpenedScreen: null
|
||||
|
||||
property int effectiveBarPosition: 0
|
||||
property real effectiveBarBottomGap: 0
|
||||
readonly property string autoBarShadowDirection: {
|
||||
const section = triggerSection || "center";
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Top:
|
||||
if (section === "left")
|
||||
return "topLeft";
|
||||
if (section === "right")
|
||||
return "topRight";
|
||||
return "top";
|
||||
case SettingsData.Position.Bottom:
|
||||
if (section === "left")
|
||||
return "bottomLeft";
|
||||
if (section === "right")
|
||||
return "bottomRight";
|
||||
return "bottom";
|
||||
case SettingsData.Position.Left:
|
||||
if (section === "left")
|
||||
return "topLeft";
|
||||
if (section === "right")
|
||||
return "bottomLeft";
|
||||
return "left";
|
||||
case SettingsData.Position.Right:
|
||||
if (section === "left")
|
||||
return "topRight";
|
||||
if (section === "right")
|
||||
return "bottomRight";
|
||||
return "right";
|
||||
default:
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
|
||||
|
||||
// Snapshot mask geometry to prevent background damage on bar updates
|
||||
property real _frozenMaskX: 0
|
||||
property real _frozenMaskY: 0
|
||||
property real _frozenMaskWidth: 0
|
||||
property real _frozenMaskHeight: 0
|
||||
|
||||
function setBarContext(position, bottomGap) {
|
||||
effectiveBarPosition = position !== undefined ? position : 0;
|
||||
effectiveBarBottomGap = bottomGap !== undefined ? bottomGap : 0;
|
||||
}
|
||||
|
||||
function primeContent() {
|
||||
_primeContent = true;
|
||||
}
|
||||
|
||||
function clearPrimedContent() {
|
||||
_primeContent = false;
|
||||
}
|
||||
|
||||
function setTriggerPosition(x, y, width, section, targetScreen, barPosition, barThickness, barSpacing, barConfig) {
|
||||
triggerX = x;
|
||||
triggerY = y;
|
||||
triggerWidth = width;
|
||||
triggerSection = section;
|
||||
screen = targetScreen;
|
||||
|
||||
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
|
||||
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4;
|
||||
storedBarConfig = barConfig;
|
||||
|
||||
const pos = barPosition !== undefined ? barPosition : 0;
|
||||
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0;
|
||||
|
||||
adjacentBarInfo = SettingsData.getAdjacentBarInfo(targetScreen, pos, barConfig);
|
||||
setBarContext(pos, bottomGap);
|
||||
}
|
||||
|
||||
// Briefly forces backgroundWindow.updatesEnabled true while the surface
|
||||
// body changes, so the contentHoleRect mask carve-out commits to the
|
||||
// compositor — otherwise the input region stays stuck at the popup's
|
||||
// initial size and clicks in any newly-grown area dismiss the popup.
|
||||
// Cleared by the frameSwapped Connections below as soon as the dirty
|
||||
// frame ships, so the bg window goes back to skipping buffer updates.
|
||||
property bool _bgCommitWindow: false
|
||||
|
||||
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
||||
const newX = Theme.snap(bodyX, dpr);
|
||||
const newY = Theme.snap(bodyY, dpr);
|
||||
const newW = Theme.snap(bodyW, dpr);
|
||||
const newH = Theme.snap(bodyH, dpr);
|
||||
const changed = newX !== _surfaceBodyX || newY !== _surfaceBodyY || newW !== _surfaceBodyW || newH !== _surfaceBodyH;
|
||||
_surfaceBodyX = newX;
|
||||
_surfaceBodyY = newY;
|
||||
_surfaceBodyW = newW;
|
||||
_surfaceBodyH = newH;
|
||||
_surfaceMarginLeft = _surfaceBodyX - shadowBuffer;
|
||||
_surfaceMarginTop = _surfaceBodyY - shadowBuffer;
|
||||
_surfaceW = _surfaceBodyW + shadowBuffer * 2;
|
||||
_surfaceH = _surfaceBodyH + shadowBuffer * 2;
|
||||
if (changed && backgroundWindow.visible) {
|
||||
_bgCommitWindow = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: backgroundWindow
|
||||
ignoreUnknownSignals: true
|
||||
function onFrameSwapped() {
|
||||
if (root._bgCommitWindow)
|
||||
root._bgCommitWindow = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Forces contentWindow to render a frame so Quickshell ships the updated
|
||||
// WindowBlur region to the compositor. WindowBlur's property updates
|
||||
// don't dirty the QML scene graph by themselves, so when the popup grows,
|
||||
// shrinks, or closes without an animation running, the blur state can
|
||||
// get stuck at its previous size. Called from the existing
|
||||
// onAligned*Changed / onShouldBeVisibleChanged handlers.
|
||||
function _kickBlurCommit() {
|
||||
if (typeof contentWindow.update === "function")
|
||||
contentWindow.update();
|
||||
}
|
||||
|
||||
function _setSettledSurfaceGeometry() {
|
||||
if (shouldBeVisible) {
|
||||
_setSurfaceGeometry(alignedX, alignedY, alignedWidth, alignedHeight);
|
||||
}
|
||||
}
|
||||
|
||||
function _setAnimatedSurfaceEnvelope() {
|
||||
if (!shouldBeVisible)
|
||||
return;
|
||||
if (_fullHeight) {
|
||||
_setSettledSurfaceGeometry();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentY = renderedAlignedY;
|
||||
const currentBottom = renderedAlignedY + renderedAlignedHeight;
|
||||
const targetY = alignedY;
|
||||
const targetBottom = alignedY + alignedHeight;
|
||||
const existingY = _surfaceBodyH > 0 ? _surfaceBodyY : currentY;
|
||||
const existingBottom = _surfaceBodyH > 0 ? _surfaceBodyY + _surfaceBodyH : currentBottom;
|
||||
const envelopeY = Math.min(currentY, targetY, existingY);
|
||||
const envelopeBottom = Math.max(currentBottom, targetBottom, existingBottom);
|
||||
_setSurfaceGeometry(alignedX, envelopeY, alignedWidth, Math.max(0, envelopeBottom - envelopeY));
|
||||
surfaceSettleTimer.restart();
|
||||
}
|
||||
|
||||
function updateSurfacePosition() {
|
||||
_setSettledSurfaceGeometry();
|
||||
}
|
||||
|
||||
onAlignedXChanged: {
|
||||
if (shouldBeVisible)
|
||||
_setAnimatedSurfaceEnvelope();
|
||||
_kickBlurCommit();
|
||||
}
|
||||
|
||||
onAlignedYChanged: {
|
||||
if (shouldBeVisible)
|
||||
_setAnimatedSurfaceEnvelope();
|
||||
_kickBlurCommit();
|
||||
}
|
||||
|
||||
onAlignedWidthChanged: {
|
||||
if (shouldBeVisible)
|
||||
_setAnimatedSurfaceEnvelope();
|
||||
_kickBlurCommit();
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!screen)
|
||||
return;
|
||||
closeTimer.stop();
|
||||
isClosing = false;
|
||||
animationsEnabled = false;
|
||||
_primeContent = true;
|
||||
|
||||
_frozenMaskX = maskX;
|
||||
_frozenMaskY = maskY;
|
||||
_frozenMaskWidth = maskWidth;
|
||||
_frozenMaskHeight = maskHeight;
|
||||
|
||||
if (_lastOpenedScreen !== null && _lastOpenedScreen !== screen) {
|
||||
contentWindow.visible = false;
|
||||
backgroundWindow.visible = false;
|
||||
}
|
||||
_lastOpenedScreen = screen;
|
||||
|
||||
if (contentContainer) {
|
||||
// animationsEnabled is false here, so this snaps to closed without animating.
|
||||
morph.openProgress = 0;
|
||||
}
|
||||
|
||||
_setSurfaceGeometry(alignedX, alignedY, alignedWidth, alignedHeight);
|
||||
if (backgroundWindowRequired)
|
||||
backgroundWindow.visible = true;
|
||||
contentWindow.visible = true;
|
||||
|
||||
animationsEnabled = true;
|
||||
shouldBeVisible = true;
|
||||
if (screen) {
|
||||
PopoutManager.showPopout(popoutHandle);
|
||||
opened();
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
isClosing = true;
|
||||
shouldBeVisible = false;
|
||||
_primeContent = false;
|
||||
PopoutManager.popoutChanged();
|
||||
closeTimer.restart();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
shouldBeVisible ? close() : open();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (!shouldBeVisible || !screen)
|
||||
return;
|
||||
const currentScreenName = screen.name;
|
||||
let screenStillExists = false;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === currentScreenName) {
|
||||
screenStillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!screenStillExists) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: Theme.variantCloseInterval(animationDuration)
|
||||
onTriggered: {
|
||||
if (!shouldBeVisible) {
|
||||
isClosing = false;
|
||||
contentWindow.visible = false;
|
||||
backgroundWindow.visible = false;
|
||||
PopoutManager.hidePopout(popoutHandle);
|
||||
popoutClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real screenWidth: screen ? screen.width : 0
|
||||
readonly property real screenHeight: screen ? screen.height : 0
|
||||
readonly property real dpr: screen ? screen.devicePixelRatio : 1
|
||||
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowMotionPadding: fluidStandaloneActive ? 0 : Math.max(0, animationOffset)
|
||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
||||
property real renderedAlignedY: alignedY
|
||||
property real renderedAlignedHeight: alignedHeight
|
||||
readonly property bool renderedGeometryGrowing: alignedHeight >= renderedAlignedHeight
|
||||
|
||||
Behavior on renderedAlignedY {
|
||||
enabled: root.animationsEnabled && contentWindow.visible && root.shouldBeVisible
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on renderedAlignedHeight {
|
||||
enabled: root.animationsEnabled && contentWindow.visible && root.shouldBeVisible
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
onAlignedHeightChanged: {
|
||||
if (shouldBeVisible)
|
||||
_setAnimatedSurfaceEnvelope();
|
||||
_kickBlurCommit();
|
||||
if (!suspendShadowWhileResizing || !shouldBeVisible)
|
||||
return;
|
||||
_resizeActive = true;
|
||||
resizeSettleTimer.restart();
|
||||
}
|
||||
onShouldBeVisibleChanged: {
|
||||
_kickBlurCommit();
|
||||
if (!shouldBeVisible) {
|
||||
_resizeActive = false;
|
||||
resizeSettleTimer.stop();
|
||||
}
|
||||
}
|
||||
onBackgroundWindowRequiredChanged: {
|
||||
if (shouldBeVisible)
|
||||
backgroundWindow.visible = backgroundWindowRequired;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resizeSettleTimer
|
||||
interval: 80
|
||||
repeat: false
|
||||
onTriggered: root._resizeActive = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: surfaceSettleTimer
|
||||
interval: Math.max(0, Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing) + 32)
|
||||
repeat: false
|
||||
onTriggered: root._setSettledSurfaceGeometry()
|
||||
}
|
||||
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
||||
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
||||
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
||||
const leftGap = _edgeClearance("left", popupGap, adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : 0);
|
||||
const rightGap = _edgeClearance("right", popupGap, adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : 0);
|
||||
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Left:
|
||||
return Math.max(leftGap, Math.min(screenWidth - popupWidth - rightGap, triggerX));
|
||||
case SettingsData.Position.Right:
|
||||
return Math.max(leftGap, Math.min(screenWidth - popupWidth - rightGap, triggerX - popupWidth));
|
||||
default:
|
||||
const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
|
||||
const minX = leftGap;
|
||||
const maxX = screenWidth - popupWidth - rightGap;
|
||||
return Math.max(minX, Math.min(maxX, rawX));
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
readonly property real alignedY: Theme.snap((() => {
|
||||
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
||||
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
||||
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
||||
const topGap = _edgeClearance("top", popupGap, adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : 0);
|
||||
const bottomGap = _edgeClearance("bottom", popupGap, adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : 0);
|
||||
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Bottom:
|
||||
return Math.max(topGap, Math.min(screenHeight - popupHeight - bottomGap, triggerY - popupHeight));
|
||||
case SettingsData.Position.Top:
|
||||
return Math.max(topGap, Math.min(screenHeight - popupHeight - bottomGap, triggerY));
|
||||
default:
|
||||
const rawY = triggerY - (popupHeight / 2);
|
||||
const minY = topGap;
|
||||
const maxY = screenHeight - popupHeight - bottomGap;
|
||||
return Math.max(minY, Math.min(maxY, rawY));
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
readonly property real triggeringBarLeftExclusion: (effectiveBarPosition === SettingsData.Position.Left && barWidth > 0) ? Math.max(0, barX + barWidth) : 0
|
||||
readonly property real triggeringBarTopExclusion: (effectiveBarPosition === SettingsData.Position.Top && barHeight > 0) ? Math.max(0, barY + barHeight) : 0
|
||||
readonly property real triggeringBarRightExclusion: (effectiveBarPosition === SettingsData.Position.Right && barWidth > 0) ? Math.max(0, screenWidth - barX) : 0
|
||||
readonly property real triggeringBarBottomExclusion: (effectiveBarPosition === SettingsData.Position.Bottom && barHeight > 0) ? Math.max(0, screenHeight - barY) : 0
|
||||
|
||||
readonly property real maskX: {
|
||||
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
|
||||
return Math.max(triggeringBarLeftExclusion, adjacentLeftBar);
|
||||
}
|
||||
|
||||
readonly property real maskY: {
|
||||
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
|
||||
return Math.max(triggeringBarTopExclusion, adjacentTopBar);
|
||||
}
|
||||
|
||||
readonly property real maskWidth: {
|
||||
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
|
||||
const rightExclusion = Math.max(triggeringBarRightExclusion, adjacentRightBar);
|
||||
return Math.max(100, screenWidth - maskX - rightExclusion);
|
||||
}
|
||||
|
||||
readonly property real maskHeight: {
|
||||
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
|
||||
const bottomExclusion = Math.max(triggeringBarBottomExclusion, adjacentBottomBar);
|
||||
return Math.max(100, screenHeight - maskY - bottomExclusion);
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: backgroundWindow
|
||||
screen: root.screen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
// Skip buffer updates when there's nothing to render. Briefly flipped
|
||||
// true via _bgCommitWindow when _surfaceBodyW/H changes so the
|
||||
// contentHoleRect mask carve-out actually commits to the compositor.
|
||||
updatesEnabled: root.overlayContent !== null || root._bgCommitWindow
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace + ":background"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: maskRect
|
||||
Region {
|
||||
item: contentHoleRect
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: maskRect
|
||||
visible: false
|
||||
color: "transparent"
|
||||
x: root.backgroundDismissWindowRequired ? root._frozenMaskX : 0
|
||||
y: root.backgroundDismissWindowRequired ? root._frozenMaskY : 0
|
||||
width: (root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0
|
||||
height: (root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentHoleRect
|
||||
visible: false
|
||||
color: "transparent"
|
||||
x: root.backgroundDismissWindowRequired ? root._surfaceBodyX : 0
|
||||
y: root.backgroundDismissWindowRequired ? root._surfaceBodyY : 0
|
||||
width: (root.backgroundDismissWindowRequired && shouldBeVisible) ? root._surfaceBodyW : 0
|
||||
height: (root.backgroundDismissWindowRequired && shouldBeVisible) ? root._surfaceBodyH : 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: false
|
||||
enabled: root.backgroundDismissWindowRequired && shouldBeVisible && backgroundInteractive
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: backgroundClicked()
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overlayLoader
|
||||
anchors.fill: parent
|
||||
active: root.overlayContent !== null && backgroundWindow.visible
|
||||
sourceComponent: root.overlayContent
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
screen: root.screen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing
|
||||
|
||||
WindowBlur {
|
||||
id: popoutBlur
|
||||
targetWindow: contentWindow
|
||||
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
||||
readonly property bool trackBlurFromBarEdge: root.fluidStandaloneActive
|
||||
readonly property bool blurAlive: trackBlurFromBarEdge ? (contentContainer.revealWidth > 0 && contentContainer.revealHeight > 0) : root.shouldBeVisible
|
||||
|
||||
blurX: trackBlurFromBarEdge ? contentContainer.x + contentContainer.revealX : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
|
||||
blurY: trackBlurFromBarEdge ? contentContainer.y + contentContainer.revealY : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
|
||||
blurWidth: blurAlive ? (trackBlurFromBarEdge ? contentContainer.revealWidth : contentContainer.width * s) : 0
|
||||
blurHeight: blurAlive ? (trackBlurFromBarEdge ? contentContainer.revealHeight : contentContainer.height * s) : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_POPOUT_LAYER")) {
|
||||
case "bottom":
|
||||
root.log.warn("'bottom' layer is not valid for popouts. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
root.log.warn("'background' layer is not valid for popouts. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (customKeyboardFocus !== null)
|
||||
return customKeyboardFocus;
|
||||
if (!shouldBeVisible)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
bottom: root._fullHeight
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root._surfaceMarginLeft
|
||||
top: root._fullHeight ? 0 : root._surfaceMarginTop
|
||||
}
|
||||
|
||||
implicitWidth: root._surfaceW
|
||||
implicitHeight: root._fullHeight ? 0 : root._surfaceH
|
||||
|
||||
mask: contentInputMask
|
||||
|
||||
Region {
|
||||
id: contentInputMask
|
||||
item: contentMaskRect
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentMaskRect
|
||||
visible: false
|
||||
x: contentContainer.x
|
||||
y: contentContainer.y
|
||||
width: contentWindow.closeVisualActive ? root.alignedWidth : 0
|
||||
height: contentWindow.closeVisualActive ? root.renderedAlignedHeight : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
x: shadowBuffer + root.alignedX - root._surfaceBodyX
|
||||
y: root._fullHeight ? root.renderedAlignedY : shadowBuffer + root.renderedAlignedY - root._surfaceBodyY
|
||||
width: root.alignedWidth
|
||||
height: root.renderedAlignedHeight
|
||||
|
||||
readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top
|
||||
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
|
||||
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
|
||||
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
|
||||
readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right"))
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
|
||||
readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
|
||||
readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28)
|
||||
readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0))
|
||||
readonly property real offsetX: {
|
||||
if (directionalEffect) {
|
||||
if (barLeft)
|
||||
return -directionalTravelX;
|
||||
if (barRight)
|
||||
return directionalTravelX;
|
||||
if (barTop || barBottom)
|
||||
return 0;
|
||||
return sectionTilt * directionalTravelX * 0.2;
|
||||
}
|
||||
if (depthEffect) {
|
||||
if (barLeft)
|
||||
return -depthTravel;
|
||||
if (barRight)
|
||||
return depthTravel;
|
||||
if (barTop || barBottom)
|
||||
return 0;
|
||||
return sectionTilt * depthTravel * 0.2;
|
||||
}
|
||||
return barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0);
|
||||
}
|
||||
readonly property real offsetY: {
|
||||
if (directionalEffect) {
|
||||
if (barBottom)
|
||||
return directionalTravelY;
|
||||
if (barTop)
|
||||
return -directionalTravelY;
|
||||
if (barLeft || barRight)
|
||||
return 0;
|
||||
return directionalTravelY;
|
||||
}
|
||||
if (depthEffect) {
|
||||
if (barBottom)
|
||||
return depthTravel;
|
||||
if (barTop)
|
||||
return -depthTravel;
|
||||
if (barLeft || barRight)
|
||||
return 0;
|
||||
return depthTravel;
|
||||
}
|
||||
return barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0);
|
||||
}
|
||||
|
||||
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
|
||||
|
||||
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
|
||||
QtObject {
|
||||
id: morph
|
||||
property real openProgress: 0
|
||||
Behavior on openProgress {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real animX: contentContainer.offsetX * (1 - morph.openProgress)
|
||||
readonly property real animY: contentContainer.offsetY * (1 - morph.openProgress)
|
||||
readonly property real scaleValue: contentContainer.computedScaleCollapsed + (1.0 - contentContainer.computedScaleCollapsed) * morph.openProgress
|
||||
readonly property real clampedAnimX: Math.max(-width, Math.min(animX, width))
|
||||
readonly property real clampedAnimY: Math.max(-height, Math.min(animY, height))
|
||||
readonly property real revealWidth: {
|
||||
if (!root.fluidStandaloneActive)
|
||||
return width;
|
||||
if (barLeft)
|
||||
return Theme.snap(Math.max(0, width + clampedAnimX), root.dpr);
|
||||
if (barRight)
|
||||
return Theme.snap(Math.max(0, width - clampedAnimX), root.dpr);
|
||||
return width;
|
||||
}
|
||||
readonly property real revealHeight: {
|
||||
if (!root.fluidStandaloneActive)
|
||||
return height;
|
||||
if (barTop)
|
||||
return Theme.snap(Math.max(0, height + clampedAnimY), root.dpr);
|
||||
if (barBottom)
|
||||
return Theme.snap(Math.max(0, height - clampedAnimY), root.dpr);
|
||||
return height;
|
||||
}
|
||||
readonly property real revealX: root.fluidStandaloneActive && barRight ? Theme.snap(width - revealWidth, root.dpr) : 0
|
||||
readonly property real revealY: root.fluidStandaloneActive && barBottom ? Theme.snap(height - revealHeight, root.dpr) : 0
|
||||
|
||||
Component.onCompleted: morph.openProgress = root.shouldBeVisible ? 1 : 0
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
morph.openProgress = root.shouldBeVisible ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: directionalClipMask
|
||||
|
||||
readonly property bool shouldClip: root.fluidStandaloneActive
|
||||
|
||||
clip: shouldClip
|
||||
x: shouldClip ? contentContainer.revealX : 0
|
||||
y: shouldClip ? contentContainer.revealY : 0
|
||||
width: shouldClip ? contentContainer.revealWidth : parent.width
|
||||
height: shouldClip ? contentContainer.revealHeight : parent.height
|
||||
|
||||
Item {
|
||||
id: rollOutAdjuster
|
||||
readonly property real baseWidth: contentContainer.width
|
||||
readonly property real baseHeight: contentContainer.height
|
||||
|
||||
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
|
||||
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
|
||||
width: baseWidth
|
||||
height: baseHeight
|
||||
clip: false
|
||||
|
||||
ElevationShadow {
|
||||
id: shadowSource
|
||||
width: rollOutAdjuster.baseWidth
|
||||
height: rollOutAdjuster.baseHeight
|
||||
opacity: contentWrapper.publishedOpacity
|
||||
scale: root.fluidStandaloneActive ? 1 : contentWrapper.scale
|
||||
x: root.fluidStandaloneActive ? 0 : contentWrapper.x
|
||||
y: root.fluidStandaloneActive ? 0 : contentWrapper.y
|
||||
level: root.shadowLevel
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: Theme.cornerRadius
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
width: rollOutAdjuster.baseWidth
|
||||
height: rollOutAdjuster.baseHeight
|
||||
|
||||
// publishedOpacity tracks Item.opacity on the GUI thread so consumers (WindowBlur,
|
||||
// ElevationShadow, sibling rect) see interpolated values while the visual runs on
|
||||
// the render thread via OpacityAnimator.
|
||||
property bool _renderActive: Theme.isDirectionalEffect || shouldBeVisible
|
||||
property real publishedOpacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
|
||||
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
|
||||
visible: _renderActive
|
||||
scale: contentContainer.scaleValue
|
||||
transformOrigin: Item.Center
|
||||
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
layer.enabled: !Theme.isDirectionalEffect && publishedOpacity < 1
|
||||
layer.smooth: false
|
||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
onRunningChanged: {
|
||||
if (!running && !root.shouldBeVisible)
|
||||
contentWrapper._renderActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on publishedOpacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible)
|
||||
contentWrapper._renderActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentWindow
|
||||
function onVisibleChanged() {
|
||||
if (!contentWindow.visible)
|
||||
contentWrapper._renderActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root._primeContent || shouldBeVisible || contentWindow.visible
|
||||
asynchronous: false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: rollOutAdjuster.baseWidth
|
||||
height: rollOutAdjuster.baseHeight
|
||||
x: root.fluidStandaloneActive ? 0 : contentWrapper.x
|
||||
y: root.fluidStandaloneActive ? 0 : contentWrapper.y
|
||||
opacity: contentWrapper.publishedOpacity
|
||||
scale: root.fluidStandaloneActive ? 1 : contentWrapper.scale
|
||||
visible: contentWrapper.visible
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||
border.width: BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusHelper
|
||||
parent: contentContainer
|
||||
anchors.fill: parent
|
||||
visible: !root.contentHandlesKeys
|
||||
enabled: !root.contentHandlesKeys
|
||||
focus: !root.contentHandlesKeys
|
||||
Keys.onPressed: event => {
|
||||
if (root.contentHandlesKeys)
|
||||
return;
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
close();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
@@ -6,8 +7,6 @@ import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
@@ -25,27 +24,28 @@ PanelWindow {
|
||||
property string title: ""
|
||||
property alias container: contentContainer
|
||||
property real customTransparency: -1
|
||||
property bool mappedVisible: false
|
||||
signal aboutToHide
|
||||
|
||||
function show() {
|
||||
visible = true
|
||||
isVisible = true
|
||||
mappedVisible = true;
|
||||
Qt.callLater(() => { isVisible = true; });
|
||||
}
|
||||
|
||||
function hide() {
|
||||
aboutToHide()
|
||||
isVisible = false
|
||||
aboutToHide();
|
||||
isVisible = false;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isVisible) {
|
||||
hide()
|
||||
hide();
|
||||
} else {
|
||||
show()
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
visible: isVisible
|
||||
visible: root.mappedVisible
|
||||
screen: modelData
|
||||
|
||||
anchors.top: true
|
||||
@@ -83,15 +83,15 @@ PanelWindow {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
property real slideOffset: alignedWidth
|
||||
property real slideOffset: root.alignedWidth
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onIsVisibleChanged() {
|
||||
slideContainer.slideOffset = root.isVisible ? 0 : slideContainer.width
|
||||
slideContainer.slideOffset = root.isVisible ? 0 : slideContainer.width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ PanelWindow {
|
||||
easing.type: Easing.OutCubic
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running && !isVisible) {
|
||||
root.visible = false
|
||||
if (!running && !root.isVisible) {
|
||||
root.mappedVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ PanelWindow {
|
||||
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
|
||||
opacity: 1
|
||||
|
||||
readonly property real effectiveTransparency: customTransparency >= 0 ? customTransparency : SettingsData.popupTransparency
|
||||
readonly property real effectiveTransparency: root.customTransparency >= 0 ? root.customTransparency : SettingsData.popupTransparency
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
@@ -8,6 +9,7 @@ Item {
|
||||
|
||||
required property var targetWindow
|
||||
property var blurItem: null
|
||||
property bool blurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
property real blurX: 0
|
||||
property real blurY: 0
|
||||
property real blurWidth: 0
|
||||
@@ -17,7 +19,7 @@ Item {
|
||||
property var _region: null
|
||||
|
||||
function _apply() {
|
||||
if (!BlurService.enabled || !targetWindow) {
|
||||
if (!blurEnabled || !BlurService.enabled || !targetWindow) {
|
||||
_cleanup();
|
||||
return;
|
||||
}
|
||||
@@ -43,6 +45,8 @@ Item {
|
||||
_region = null;
|
||||
}
|
||||
|
||||
onBlurEnabledChanged: _apply()
|
||||
|
||||
Connections {
|
||||
target: BlurService
|
||||
function onEnabledChanged() {
|
||||
@@ -51,7 +55,7 @@ Item {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.targetWindow
|
||||
target: root.targetWindow ?? null
|
||||
function onVisibleChanged() {
|
||||
if (root.targetWindow && root.targetWindow.visible) {
|
||||
root._region = null;
|
||||
|
||||
@@ -3701,7 +3701,7 @@
|
||||
},
|
||||
{
|
||||
"section": "popupTransparency",
|
||||
"label": "Popup Transparency",
|
||||
"label": "Surface Opacity",
|
||||
"tabIndex": 10,
|
||||
"category": "Theme & Colors",
|
||||
"keywords": [
|
||||
@@ -3719,6 +3719,7 @@
|
||||
"popup",
|
||||
"scheme",
|
||||
"style",
|
||||
"surface",
|
||||
"their",
|
||||
"theme",
|
||||
"translucent",
|
||||
|
||||
294
scripts/format-staged.py
Executable file
294
scripts/format-staged.py
Executable file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Format staged .qml files using qmlls (the Qt QML language server).
|
||||
|
||||
Per file:
|
||||
1. Speak LSP over stdio to qmlls: initialize -> didOpen -> formatting,
|
||||
apply returned edits, save, `git add`.
|
||||
2. Run qmllint on the formatted file and warn about unused imports
|
||||
(informational only — never modifies files).
|
||||
|
||||
Refuses to run if any staged file also has unstaged changes, since `git add`
|
||||
would silently absorb those into the commit.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TAB_SIZE = 4
|
||||
QMLLS_CANDIDATES = ["qmlls6", "qmlls"]
|
||||
QMLLINT_CANDIDATES = ["/usr/lib/qt6/bin/qmllint", "qmllint6", "qmllint"]
|
||||
|
||||
|
||||
def git(*args, cwd=None):
|
||||
return subprocess.run(
|
||||
["git", *args],
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout
|
||||
|
||||
|
||||
def repo_root():
|
||||
return Path(git("rev-parse", "--show-toplevel").strip())
|
||||
|
||||
|
||||
def staged_qml_files(root):
|
||||
out = git("diff", "--cached", "--name-only", "--diff-filter=ACMR", cwd=root)
|
||||
return [root / line for line in out.splitlines() if line.endswith(".qml")]
|
||||
|
||||
|
||||
def has_unstaged_changes(root, file):
|
||||
rel = str(file.relative_to(root))
|
||||
return git("diff", "--name-only", "--", rel, cwd=root).strip() != ""
|
||||
|
||||
|
||||
def find_qmlls():
|
||||
for name in QMLLS_CANDIDATES:
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def find_qmllint():
|
||||
for candidate in QMLLINT_CANDIDATES:
|
||||
path = candidate if "/" in candidate and Path(candidate).is_file() else shutil.which(candidate)
|
||||
if not path:
|
||||
continue
|
||||
try:
|
||||
result = subprocess.run([path, "--help"], capture_output=True, text=True, timeout=5)
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
continue
|
||||
if "--json" in result.stdout:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def lint_unused_imports(qmllint, file):
|
||||
"""Return a list of (line, message, suspect) for unused-import warnings.
|
||||
|
||||
`suspect` is True when the same line also has an import-resolution failure,
|
||||
which often means the warning is a false positive (qmllint couldn't find
|
||||
the module, so its 'unused' verdict is unreliable).
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[qmllint, "--unused-imports", "warning", "--json", "-", str(file)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
files = data.get("files", [])
|
||||
if not files:
|
||||
return []
|
||||
warnings = files[0].get("warnings", [])
|
||||
|
||||
failed_lines = {w["line"] for w in warnings if w.get("id") == "import" and "line" in w}
|
||||
findings = []
|
||||
for w in warnings:
|
||||
if w.get("id") != "unused-imports" or "line" not in w:
|
||||
continue
|
||||
line = w["line"]
|
||||
findings.append((line, w.get("message", "Unused import"), line in failed_lines))
|
||||
findings.sort(key=lambda x: x[0])
|
||||
return findings
|
||||
|
||||
|
||||
class LspClient:
|
||||
def __init__(self, command):
|
||||
self.proc = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
self._next_id = 1
|
||||
|
||||
def _send(self, msg):
|
||||
body = json.dumps(msg).encode("utf-8")
|
||||
header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
|
||||
self.proc.stdin.write(header + body)
|
||||
self.proc.stdin.flush()
|
||||
|
||||
def _read(self):
|
||||
headers = {}
|
||||
while True:
|
||||
line = self.proc.stdout.readline()
|
||||
if not line:
|
||||
raise RuntimeError("qmlls closed unexpectedly")
|
||||
line = line.decode("ascii").rstrip("\r\n")
|
||||
if line == "":
|
||||
break
|
||||
key, _, value = line.partition(":")
|
||||
headers[key.strip().lower()] = value.strip()
|
||||
length = int(headers["content-length"])
|
||||
body = b""
|
||||
while len(body) < length:
|
||||
chunk = self.proc.stdout.read(length - len(body))
|
||||
if not chunk:
|
||||
raise RuntimeError("qmlls closed mid-message")
|
||||
body += chunk
|
||||
return json.loads(body)
|
||||
|
||||
def request(self, method, params):
|
||||
req_id = self._next_id
|
||||
self._next_id += 1
|
||||
self._send({"jsonrpc": "2.0", "id": req_id, "method": method, "params": params})
|
||||
while True:
|
||||
msg = self._read()
|
||||
if msg.get("id") == req_id and ("result" in msg or "error" in msg):
|
||||
if "error" in msg:
|
||||
raise RuntimeError(f"LSP {method} error: {msg['error']}")
|
||||
return msg.get("result")
|
||||
if "id" in msg and "method" in msg:
|
||||
# Server-to-client request — reply with null so it doesn't stall.
|
||||
self._send({"jsonrpc": "2.0", "id": msg["id"], "result": None})
|
||||
|
||||
def notify(self, method, params):
|
||||
self._send({"jsonrpc": "2.0", "method": method, "params": params})
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
self.request("shutdown", None)
|
||||
self.notify("exit", None)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.proc.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
|
||||
|
||||
def apply_edits(text, edits):
|
||||
"""Apply LSP TextEdits (non-overlapping) to text, end-first."""
|
||||
if not edits:
|
||||
return text
|
||||
|
||||
lines = text.splitlines(keepends=True)
|
||||
line_starts = [0]
|
||||
for line in lines:
|
||||
line_starts.append(line_starts[-1] + len(line))
|
||||
|
||||
def offset(pos):
|
||||
line = pos["line"]
|
||||
if line >= len(line_starts):
|
||||
return len(text)
|
||||
return min(line_starts[line] + pos["character"], len(text))
|
||||
|
||||
sorted_edits = sorted(
|
||||
edits,
|
||||
key=lambda e: (e["range"]["start"]["line"], e["range"]["start"]["character"]),
|
||||
reverse=True,
|
||||
)
|
||||
for edit in sorted_edits:
|
||||
start = offset(edit["range"]["start"])
|
||||
end = offset(edit["range"]["end"])
|
||||
text = text[:start] + edit["newText"] + text[end:]
|
||||
return text
|
||||
|
||||
|
||||
def main():
|
||||
root = repo_root()
|
||||
files = staged_qml_files(root)
|
||||
if not files:
|
||||
print("No staged .qml files.")
|
||||
return 0
|
||||
|
||||
dirty = [f for f in files if has_unstaged_changes(root, f)]
|
||||
if dirty:
|
||||
print("Refusing to format: staged files have unstaged changes:", file=sys.stderr)
|
||||
for f in dirty:
|
||||
print(f" {f.relative_to(root)}", file=sys.stderr)
|
||||
print("\nStash or stage those changes first.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
qmlls = find_qmlls()
|
||||
if not qmlls:
|
||||
print(f"qmlls not found (tried: {', '.join(QMLLS_CANDIDATES)})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
qmllint = find_qmllint()
|
||||
if not qmllint:
|
||||
print("warning: qmllint with --json not found; skipping unused-import checks", file=sys.stderr)
|
||||
|
||||
client = LspClient([qmlls])
|
||||
changed = 0
|
||||
unused_by_file = {}
|
||||
try:
|
||||
client.request("initialize", {
|
||||
"processId": os.getpid(),
|
||||
"rootUri": root.as_uri(),
|
||||
"workspaceFolders": [{"uri": root.as_uri(), "name": root.name}],
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"formatting": {"dynamicRegistration": False},
|
||||
"synchronization": {"dynamicRegistration": False},
|
||||
},
|
||||
},
|
||||
})
|
||||
client.notify("initialized", {})
|
||||
|
||||
for file in files:
|
||||
rel = file.relative_to(root)
|
||||
print(f" {rel} ... ", end="", flush=True)
|
||||
|
||||
original = file.read_text()
|
||||
uri = file.as_uri()
|
||||
|
||||
client.notify("textDocument/didOpen", {
|
||||
"textDocument": {
|
||||
"uri": uri,
|
||||
"languageId": "qml",
|
||||
"version": 1,
|
||||
"text": original,
|
||||
},
|
||||
})
|
||||
|
||||
edits = client.request("textDocument/formatting", {
|
||||
"textDocument": {"uri": uri},
|
||||
"options": {"tabSize": TAB_SIZE, "insertSpaces": True},
|
||||
})
|
||||
|
||||
client.notify("textDocument/didClose", {"textDocument": {"uri": uri}})
|
||||
|
||||
new_text = apply_edits(original, edits or [])
|
||||
if new_text == original:
|
||||
print("unchanged")
|
||||
continue
|
||||
|
||||
file.write_text(new_text)
|
||||
git("add", "--", str(rel), cwd=root)
|
||||
changed += 1
|
||||
print("formatted & staged")
|
||||
|
||||
if qmllint:
|
||||
for file in files:
|
||||
findings = lint_unused_imports(qmllint, file)
|
||||
if findings:
|
||||
unused_by_file[file] = findings
|
||||
|
||||
print(f"\n{changed} of {len(files)} file(s) changed.")
|
||||
|
||||
if unused_by_file:
|
||||
print("\nUnused import warnings (informational, not auto-removed):")
|
||||
for file, findings in unused_by_file.items():
|
||||
rel = file.relative_to(root)
|
||||
for line, message, suspect in findings:
|
||||
suffix = " [suspect: import didn't resolve]" if suspect else ""
|
||||
print(f" {rel}:{line} {message}{suffix}")
|
||||
|
||||
return 0
|
||||
finally:
|
||||
client.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user