1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 02:22:06 -04:00

Compare commits

...

28 Commits

Author SHA1 Message Date
bbedward
b9064d3bbb restore niri overview connected mode 2026-04-23 13:11:17 -04:00
bbedward
1905d51595 some more simplifications and bug fixes 2026-04-23 13:11:17 -04:00
bbedward
7f906128fa de-dupe and cleanup 2026-04-23 13:11:17 -04:00
bbedward
b588d2dac5 restore CC and notification standalone behavior 2026-04-23 13:11:17 -04:00
bbedward
e0abeb3708 refactor connected/standalone architecture 2026-04-23 13:11:17 -04:00
purian23
bdbbfc79c0 (frameMode): New Modal & Launcher connections 2026-04-23 13:11:17 -04:00
purian23
62926d8f0c (Notifications): Update body card expansions 2026-04-23 13:11:17 -04:00
purian23
86dc14b58d (frame): QOL Control Center & Notification updates 2026-04-23 13:11:17 -04:00
purian23
397ccad113 feat(Frame): Close the gaps 2026-04-23 13:11:17 -04:00
purian23
9036368da3 frame(Notifications): Update Arc path & Motion 2026-04-23 13:11:17 -04:00
purian23
c96136f381 (frame): Update animation sync w/Dank Popouts 2026-04-23 13:11:17 -04:00
purian23
1660e0c065 (frame): Performance round 2026-04-23 13:11:17 -04:00
purian23
7c7299d33f (frame): Update Connected blur Arcs & Enable shadow modes 2026-04-23 13:11:17 -04:00
purian23
dbf9b86956 frame(ConnectedMode): Wire up Notifications 2026-04-23 13:11:17 -04:00
purian23
7d7819c693 (frame): Update connected mode animation & motion logic 2026-04-23 13:11:17 -04:00
purian23
7b9dcab54b (frame): implement ConnectedModeState to better handle component sync 2026-04-23 13:11:17 -04:00
purian23
9946421dac (frameMode): Restore user settings when exiting frame mode
- Align blur settings in non-FrameMode motion settings
2026-04-23 13:11:17 -04:00
purian23
c66de3b5df (frame): Update connected mode with blur 2026-04-23 13:11:17 -04:00
purian23
09088bbb27 (frame): Update connected mode & opacity connection settings 2026-04-23 13:11:17 -04:00
purian23
0310b76ab5 (frameInMotion): Initial Unified Frame Connected Mode 2026-04-23 13:11:17 -04:00
purian23
e9f3e25e24 Add Directional Motion options 2026-04-23 13:11:17 -04:00
purian23
7596423d69 Initial staging for Animation & Motion effects 2026-04-23 13:11:17 -04:00
purian23
1987f2c279 (frame): Add blur support & cleanup 2026-04-23 13:11:17 -04:00
purian23
f64e0653f4 (frame): Multi-monitor support 2026-04-23 13:11:17 -04:00
purian23
ac49332d5d Connected frames & defaults 2026-04-23 13:11:17 -04:00
purian23
cf3d553ace Continue frame implementation 2026-04-23 13:11:17 -04:00
purian23
bc79b6575c Initial framework 2026-04-23 13:11:17 -04:00
bbedward
f4c11bc2ff clipboard: decode metadata only 2026-04-23 09:28:26 -04:00
59 changed files with 10252 additions and 1955 deletions

View File

@@ -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 ./...

View File

@@ -212,9 +212,10 @@ func (m *Manager) setupDataDeviceSync() {
}
var offer any
if e.Id != nil {
switch {
case e.Id != nil:
offer = e.Id
} else if e.OfferId != 0 {
case e.OfferId != 0:
m.offerMutex.RLock()
offer = m.offerRegistry[e.OfferId]
m.offerMutex.RUnlock()
@@ -224,10 +225,6 @@ func (m *Manager) setupDataDeviceSync() {
wasOwner := m.isOwner
m.ownerLock.Unlock()
if offer == nil {
return
}
if wasOwner {
return
}
@@ -236,9 +233,11 @@ func (m *Manager) setupDataDeviceSync() {
m.currentOffer = offer
if prevOffer != nil && prevOffer != offer {
m.offerMutex.Lock()
delete(m.offerMimeTypes, prevOffer)
m.offerMutex.Unlock()
m.releaseOffer(prevOffer)
}
if offer == nil {
return
}
m.offerMutex.RLock()
@@ -292,6 +291,33 @@ func (m *Manager) setupDataDeviceSync() {
log.Info("Data device setup complete")
}
func (m *Manager) releaseOffer(offer any) {
if offer == nil {
return
}
typedOffer, ok := offer.(*ext_data_control.ExtDataControlOfferV1)
if !ok {
return
}
m.offerMutex.Lock()
delete(m.offerMimeTypes, offer)
delete(m.offerRegistry, typedOffer.ID())
m.offerMutex.Unlock()
typedOffer.Destroy()
}
func (m *Manager) releaseCurrentSource() {
if m.currentSource == nil {
return
}
source, ok := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
m.currentSource = nil
if !ok {
return
}
source.Destroy()
}
func (m *Manager) readAndStore(r *os.File, mimeType string) {
defer r.Close()
@@ -395,7 +421,7 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
if extractHash(v) != hash {
continue
}
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err == nil && entry.Pinned {
continue
}
@@ -413,7 +439,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
c := b.Cursor()
var count int
for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err == nil && entry.Pinned {
continue
}
@@ -456,6 +482,14 @@ func encodeEntry(e Entry) ([]byte, error) {
}
func decodeEntry(data []byte) (Entry, error) {
return decodeEntryFields(data, true)
}
func decodeEntryMeta(data []byte) (Entry, error) {
return decodeEntryFields(data, false)
}
func decodeEntryFields(data []byte, withData bool) (Entry, error) {
buf := bytes.NewReader(data)
var e Entry
@@ -463,8 +497,15 @@ func decodeEntry(data []byte) (Entry, error) {
var dataLen uint32
binary.Read(buf, binary.BigEndian, &dataLen)
e.Data = make([]byte, dataLen)
buf.Read(e.Data)
switch {
case withData:
e.Data = make([]byte, dataLen)
buf.Read(e.Data)
default:
if _, err := buf.Seek(int64(dataLen), io.SeekCurrent); err != nil {
return e, err
}
}
var mimeLen uint32
binary.Read(buf, binary.BigEndian, &mimeLen)
@@ -668,14 +709,9 @@ func sizeStr(size int) string {
func (m *Manager) updateState() {
history := m.GetHistory()
for i := range history {
history[i].Data = nil
}
var current *Entry
if len(history) > 0 {
c := history[0]
c.Data = nil
current = &c
}
@@ -750,7 +786,7 @@ func (m *Manager) GetHistory() []Entry {
c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err != nil {
continue
}
@@ -935,7 +971,7 @@ func (m *Manager) ClearHistory() {
var toDelete [][]byte
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err != nil || !entry.Pinned {
toDelete = append(toDelete, k)
}
@@ -958,7 +994,7 @@ func (m *Manager) ClearHistory() {
if b != nil {
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, _ := decodeEntry(v)
entry, _ := decodeEntryMeta(v)
if entry.Pinned {
pinnedCount++
}
@@ -1066,6 +1102,7 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
m.ownerLock.Unlock()
})
m.releaseCurrentSource()
m.currentSource = source
m.sourceMutex.Lock()
m.sourceMimeTypes = []string{mimeType}
@@ -1145,9 +1182,11 @@ func (m *Manager) Close() {
m.subscribers = make(map[string]chan State)
m.subMutex.Unlock()
if m.currentSource != nil {
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
source.Destroy()
m.releaseCurrentSource()
if m.currentOffer != nil {
m.releaseOffer(m.currentOffer)
m.currentOffer = nil
}
if m.dataDevice != nil {
@@ -1191,11 +1230,10 @@ func (m *Manager) clearOldEntries(days int) error {
var toDelete [][]byte
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err != nil {
continue
}
// Skip pinned entries
if entry.Pinned {
continue
}
@@ -1310,7 +1348,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err != nil {
continue
}
@@ -1335,7 +1373,6 @@ func (m *Manager) Search(params SearchParams) SearchResult {
continue
}
entry.Data = nil
all = append(all, entry)
}
return nil
@@ -1510,7 +1547,7 @@ func (m *Manager) PinEntry(id uint64) error {
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err != nil || !entry.Pinned {
continue
}
@@ -1528,7 +1565,6 @@ func (m *Manager) PinEntry(id uint64) error {
return nil
}
// Check pinned count
cfg := m.getConfig()
pinnedCount := 0
if err := m.db.View(func(tx *bolt.Tx) error {
@@ -1538,7 +1574,7 @@ func (m *Manager) PinEntry(id uint64) error {
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err == nil && entry.Pinned {
pinnedCount++
}
@@ -1629,12 +1665,11 @@ func (m *Manager) GetPinnedEntries() []Entry {
c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err != nil {
continue
}
if entry.Pinned {
entry.Data = nil
pinned = append(pinned, entry)
}
}
@@ -1660,7 +1695,7 @@ func (m *Manager) GetPinnedCount() int {
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
entry, err := decodeEntryMeta(v)
if err == nil && entry.Pinned {
count++
}
@@ -1779,6 +1814,7 @@ func (m *Manager) CopyFile(filePath string) error {
m.ownerLock.Unlock()
})
m.releaseCurrentSource()
m.currentSource = source
m.ownerLock.Lock()

View 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.frameEnabled && _effect === 1 && SettingsData.directionalAnimationMode === 3
readonly property real effectScaleCollapsed: _effectScaleCollapsed[_effect] !== undefined ? _effectScaleCollapsed[_effect] : 0.96
readonly property real effectAnimOffset: _effectAnimOffsets[_effect] !== undefined ? _effectAnimOffsets[_effect] : 16
}

View File

@@ -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]
}

View File

@@ -0,0 +1,410 @@
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 setDockState(screenName, state) {
if (!screenName || !state)
return false;
const next = _cloneDict(dockStates);
next[screenName] = _normalizeDockState(state);
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 next = _cloneDict(dockSlides);
next[screenName] = {
"x": Number(x),
"y": Number(y)
};
dockSlides = next;
return true;
}
// ─── Notification state (per screen, updated by NotificationSurface) ──────
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;
}
// ─── Dock retract coordination ────────────────────────────────
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;
}
}

View 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";
}

View File

@@ -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

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton {
id: root
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"
@@ -37,6 +37,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,
@@ -168,6 +180,12 @@ 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 int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12
@@ -186,6 +204,7 @@ Singleton {
onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true
onBarElevationEnabledChanged: saveSettings()
property bool blurEnabled: false
onBlurEnabledChanged: saveSettings()
property string blurBorderColor: "outline"
@@ -198,6 +217,51 @@ 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: false
onFrameCloseGapsChanged: saveSettings()
property string frameLauncherEmergeSide: "bottom"
onFrameLauncherEmergeSideChanged: saveSettings()
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
property int previousDirectionalMode: 1
onPreviousDirectionalModeChanged: saveSettings()
property var connectedFrameBarStyleBackups: ({})
onConnectedFrameBarStyleBackupsChanged: saveSettings()
readonly property bool connectedFrameModeActive: frameEnabled && motionEffect === SettingsData.AnimationEffect.Directional && directionalAnimationMode === 3
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
@@ -1289,6 +1353,7 @@ Singleton {
_loading = false;
}
loadPluginSettings();
Qt.callLater(() => _reconcileConnectedFrameBarStyles());
}
property var _pendingMigration: null
@@ -1402,6 +1467,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));
@@ -1549,35 +1749,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
};
}
@@ -1671,7 +1873,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;
@@ -1693,7 +1897,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:
@@ -1784,7 +1988,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();
}
@@ -1796,7 +2002,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) {
@@ -1850,6 +2056,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();
}
@@ -1944,6 +2155,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);

View File

@@ -968,6 +968,47 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
}
// ─── Animation variant proxy ──────────────────────────────────────────────
// 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,
@@ -1043,6 +1084,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
@@ -1133,7 +1177,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) {
@@ -1832,6 +1882,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);
}

View File

@@ -49,6 +49,10 @@ var SPEC = {
modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
previousDirectionalMode: { def: 1 },
m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 },
@@ -432,6 +436,7 @@ var SPEC = {
displayProfileAutoSelect: { def: false },
displayShowDisconnected: { def: false },
displaySnapToEdge: { def: true },
connectedFrameBarStyleBackups: { def: {} },
barConfigs: {
def: [{
@@ -538,7 +543,19 @@ 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: false },
frameLauncherEmergeSide: { def: "bottom" }
};
function getValidKeys() {

View File

@@ -248,6 +248,10 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 6;
}
if (currentVersion < 11) {
settings.configVersion = 11;
}
return settings;
}

View File

@@ -21,6 +21,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
@@ -185,6 +186,8 @@ Item {
}
}
Frame {}
Repeater {
id: dankBarRepeater
model: ScriptModel {

View File

@@ -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();

View File

@@ -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) {

View File

@@ -1,24 +1,17 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
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"
@@ -36,7 +29,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
@@ -45,452 +37,133 @@ 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();
}
Loader {
id: impl
sourceComponent: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
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.shouldBeVisibleChanged.connect(function () {
if (root.shouldBeVisible !== it.shouldBeVisible)
root.shouldBeVisible = it.shouldBeVisible;
});
it.shouldHaveFocus = root.shouldHaveFocus;
it.shouldHaveFocusChanged.connect(function () {
if (root.shouldHaveFocus !== it.shouldHaveFocus)
root.shouldHaveFocus = it.shouldHaveFocus;
});
it.opened.connect(root.opened);
it.dialogClosed.connect(root.dialogClosed);
it.backgroundClicked.connect(root.backgroundClicked);
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;
}
}
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;
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 && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? 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":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.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.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
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"
}
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 onShouldHaveFocusChanged() {
if (impl.item && impl.item.shouldHaveFocus !== root.shouldHaveFocus)
impl.item.shouldHaveFocus = root.shouldHaveFocus;
}
}
}

View File

@@ -0,0 +1,818 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
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: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened
signal dialogClosed
signal backgroundClicked
property bool animationsEnabled: true
// ─── Connected chrome sync ────────────────────────────────────────────────
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);
}
function _flushFullSync() {
_fullSyncPending = false;
_syncModalChromeState();
}
function _queueFullSync() {
if (_fullSyncPending)
return;
_fullSyncPending = true;
Qt.callLater(() => {
if (root && typeof root._flushFullSync === "function")
root._flushFullSync();
});
}
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: _syncModalBody()
onAlignedYChanged: _syncModalBody()
onAlignedWidthChanged: _syncModalBody()
onAlignedHeightChanged: _syncModalBody()
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 (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
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 && animatedContent.opacity > 0 && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0
blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0 && !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":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.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.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 (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
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 (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
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;
}
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
onAnimXChanged: if (root.frameOwnsConnectedChrome)
root._syncModalAnim()
onAnimYChanged: if (root.frameOwnsConnectedChrome)
root._syncModalAnim()
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
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: (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
}
}
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;
}
}
}
}
}

View File

@@ -0,0 +1,502 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
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 && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? 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":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.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.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
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"
}
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;
}
}
}
}
}

View File

@@ -1,465 +1,84 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: 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 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 ?? "") : ""
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);
}
Timer {
id: closeCleanupTimer
interval: Theme.modalAnimationDuration + 50
repeat: false
onTriggered: {
isClosing = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
}
function toggleWithMode(mode) {
if (impl.item)
impl.item.toggleWithMode(mode);
}
Connections {
target: spotlightContent?.controller ?? null
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
return;
SessionData.setLauncherLastMode(mode);
}
Loader {
id: impl
sourceComponent: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
onItemChanged: if (item)
root._wireBackend(item)
}
HyprlandFocusGrab {
id: focusGrab
windows: [launcherWindow]
active: false
onCleared: {
if (spotlightOpen) {
hide();
}
}
Component {
id: standaloneComp
DankLauncherV2ModalStandalone {}
}
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && spotlightOpen) {
hide();
}
}
Component {
id: connectedComp
DankLauncherV2ModalConnected {}
}
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":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.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"
}
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 _wireBackend(it) {
if (!it)
return;
it.modalHandle = root;
it.dialogClosed.connect(root.dialogClosed);
}
}

View File

@@ -0,0 +1,841 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
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 !== ""
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 usable = Math.max(0, screenWidth - insetL - insetR);
return {
"x": insetL + Math.max(0, (usable - modalWidth) / 2),
"y": resolvedConnectedBarSide === "top" ? _frameEdgeInset("top") : screenHeight - modalHeight - _frameEdgeInset("bottom")
};
}
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)
// 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: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
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: _needsExtendedWindow ? alignedY : shadowPad
signal dialogClosed
// ─── Connected chrome sync ────────────────────────────────────────────────
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": alignedX,
"bodyY": alignedY,
"bodyW": alignedWidth,
"bodyH": alignedHeight,
"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);
}
function _flushFullSync() {
_fullSyncPending = false;
_syncModalChromeState();
}
function _queueFullSync() {
if (_fullSyncPending)
return;
_fullSyncPending = true;
Qt.callLater(() => {
if (root && typeof root._flushFullSync === "function")
root._flushFullSync();
});
}
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, alignedX, alignedY, alignedWidth, alignedHeight);
}
function _releaseModalChrome() {
if (_chromeClaimId) {
ConnectedModeState.releaseDockRetract(_chromeClaimId);
_chromeClaimId = "";
}
const screenName = _currentScreenName();
if (screenName)
ConnectedModeState.clearModalState(screenName);
}
onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
onResolvedConnectedBarSideChanged: _queueFullSync()
onSpotlightOpenChanged: _queueFullSync()
onAlignedXChanged: _syncModalBody()
onAlignedYChanged: _syncModalBody()
onAlignedWidthChanged: _syncModalBody()
onAlignedHeightChanged: _syncModalBody()
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;
});
}
}
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
PanelWindow {
id: backgroundWindow
visible: false
color: "transparent"
WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.margins {
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
}
anchors {
top: true
bottom: true
left: true
right: true
}
mask: Region {
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
}
Item {
id: bgFullScreenMask
anchors.fill: parent
}
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)
DankAnim {
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()
}
}
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
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) && contentWrapper.opacity > 0 && !root.frameOwnsConnectedChrome ? root.alignedWidth * s : 0
blurHeight: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 && !root.frameOwnsConnectedChrome ? root.alignedHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankLauncherV2Modal: '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 + contentWrapper.x
y: contentContainer.y + contentWrapper.y
width: root.alignedWidth
height: root.alignedHeight
}
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.alignedHeight
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: 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);
}
// Declarative bindings — snap applied at render layer (contentWrapper x/y)
property real animX: root._motionActive ? 0 : root._frozenMotionX
property real animY: root._motionActive ? 0 : root._frozenMotionY
property real scaleValue: root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed))
onAnimXChanged: if (root.frameOwnsConnectedChrome)
root._syncModalAnim()
onAnimYChanged: if (root.frameOwnsConnectedChrome)
root._syncModalAnim()
Behavior on animX {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
Item {
id: directionalClipMask
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
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.opacity
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
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 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)
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
DankAnim {
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
}
}
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
}

View File

@@ -0,0 +1,439 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
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 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: 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":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.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"
}
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
}
}
}
}

View File

@@ -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,7 +270,7 @@ FocusScope {
Item {
anchors.fill: parent
visible: !editMode && !(root.parentModal?.isClosing ?? false)
visible: !editMode
Item {
id: footerBar
@@ -322,13 +281,16 @@ FocusScope {
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
readonly property bool _connectedArcAtFooter: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "bottom")
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 +298,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 +370,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 +391,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 +465,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 +699,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 +739,6 @@ FocusScope {
}
function onSearchQueryRequested(query) {
searchField.text = query;
searchField.cursorPosition = query.length;
}
function onModeChanged() {
extFilterField.text = "";
@@ -980,15 +949,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
}
}
}

View File

@@ -518,5 +518,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());
}
}
}
}

View File

@@ -120,6 +120,12 @@ Rectangle {
"text": I18n.tr("Widgets"),
"icon": "widgets",
"tabIndex": 22
},
{
"id": "frame",
"text": I18n.tr("Frame"),
"icon": "frame_source",
"tabIndex": 33
}
]
},

View File

@@ -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();

View File

@@ -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 {

View File

@@ -51,6 +51,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);
}
@@ -181,7 +210,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;
@@ -198,14 +230,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: SettingsData.connectedFrameModeActive
NumberAnimation {
duration: Theme.variantDuration(Theme.popoutAnimationDuration, detailHost.active)
easing.type: Easing.BezierSpline
easing.bezierCurve: detailHost.active ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
}
}
}

View File

@@ -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: {
@@ -53,6 +87,8 @@ DankPopout {
popupWidth: 550
popupHeight: {
if (SettingsData.connectedFrameModeActive)
return targetPopupHeight;
const screenHeight = (triggerScreen?.height ?? 1080);
const maxHeight = screenHeight - 100;
const contentHeight = contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400;
@@ -95,6 +131,7 @@ DankPopout {
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
collapseAll();
queueTargetPopupHeightUpdate();
Qt.callLater(() => {
if (NetworkService.activeService)
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled;
@@ -111,6 +148,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 +181,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,91 +201,103 @@ 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
popoutContent: controlContent
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
popoutContent: controlContent
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()
}
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -133,6 +133,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;
@@ -187,6 +192,11 @@ PanelWindow {
}
}
Connections {
target: SettingsData
function onFrameEnabledChanged() { barBlur.rebuild(); }
}
Connections {
target: topBarSlide
function onXChanged() {
@@ -238,7 +248,9 @@ 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);
@@ -384,7 +396,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
@@ -395,7 +407,12 @@ 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: {
@@ -644,14 +661,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)
@@ -792,7 +809,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;
@@ -889,7 +906,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

View File

@@ -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

View File

@@ -68,23 +68,24 @@ Item {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
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 {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
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
}
}
@@ -206,23 +207,24 @@ Item {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
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 {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
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
}
}
@@ -360,23 +362,24 @@ Item {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
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 {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
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
}
}

View File

@@ -19,11 +19,12 @@ 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"
@@ -42,6 +43,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
@@ -113,13 +133,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;
}
// ─── ConnectedModeState sync ────────────────────────────────────────
// 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
@@ -130,7 +213,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;
@@ -258,7 +341,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;
}
@@ -281,6 +374,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() {
@@ -302,13 +412,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
@@ -318,17 +428,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 {
@@ -388,7 +498,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;
@@ -459,11 +569,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);
@@ -505,7 +615,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 {
@@ -517,7 +631,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 {
@@ -528,18 +646,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 {
@@ -553,33 +680,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 {
@@ -588,12 +742,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

View File

@@ -0,0 +1,16 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Variants {
id: root
model: Quickshell.screens
FrameInstance {
required property var modelData
screen: modelData
}
}

View 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
}
}
}

View 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
}
}
}

View 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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Notifications
import qs.Common
@@ -16,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
@@ -34,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"
readonly property var shadowElevation: Theme.elevationLevel1
@@ -56,6 +63,16 @@ Rectangle {
});
}
function expansionMotionDuration() {
if (isDescriptionToggleAnimation)
return descriptionExpanded ? Theme.notificationInlineExpandDuration : Theme.notificationInlineCollapseDuration;
return root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration);
}
function expansionMotionCurve() {
return root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized;
}
Behavior on scale {
enabled: listLevelScaleAnimationsEnabled
NumberAnimation {
@@ -65,6 +82,7 @@ Rectangle {
}
Behavior on shadowBlurPx {
enabled: !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -72,6 +90,7 @@ Rectangle {
}
Behavior on shadowOffsetXPx {
enabled: !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -79,6 +98,7 @@ Rectangle {
}
Behavior on shadowOffsetYPx {
enabled: !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -93,6 +113,24 @@ Rectangle {
}
}
Behavior on expandedContentOpacity {
enabled: root.connectedFrameMode && root.__initialized && root.userInitiatedExpansion && root.animateExpansion
NumberAnimation {
duration: root.expansionMotionDuration()
easing.type: Easing.BezierSpline
easing.bezierCurve: root.expansionMotionCurve()
}
}
Behavior on collapsedContentOpacity {
enabled: root.connectedFrameMode && 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;
@@ -100,6 +138,8 @@ Rectangle {
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Theme.primaryHoverLight;
}
if (connectedFrameMode)
return Theme.popupLayerColor(Theme.surfaceContainerHigh);
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
}
border.color: {
@@ -126,7 +166,31 @@ Rectangle {
}
return 0;
}
clip: false
clip: connectedFrameMode && _clipAnimatedContent
onExpandedChanged: {
if (connectedFrameMode && __initialized && userInitiatedExpansion && animateExpansion)
_clipAnimatedContent = true;
if (expanded) {
_retainedExpandedContent = false;
return;
}
if (connectedFrameMode && __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
@@ -146,7 +210,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 {
@@ -186,7 +250,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
@@ -351,6 +416,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(() => {
@@ -360,7 +426,7 @@ Rectangle {
}
}
propagateComposedEvents: true
propagateComposedEvents: false
onPressed: mouse => {
if (parent.hoveredLink)
mouse.accepted = false;
@@ -385,7 +451,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
@@ -516,7 +583,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 {
@@ -655,6 +727,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)
@@ -663,7 +736,7 @@ Rectangle {
}
}
propagateComposedEvents: true
propagateComposedEvents: false
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false;
@@ -828,7 +901,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
@@ -884,7 +958,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
@@ -915,10 +990,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
@@ -942,6 +1018,7 @@ Rectangle {
buttonSize: compactMode ? 24 : 28
onClicked: {
root.userInitiatedExpansion = true;
root.isDescriptionToggleAnimation = false;
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
}
}
@@ -959,15 +1036,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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -10,13 +10,37 @@ 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
blurWidth: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.width - content.cardInset * 2) : 0
blurHeight: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.height - content.cardInset * 2) : 0
blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
}
WlrLayershell.namespace: "dms:notification-popup"
@@ -25,6 +49,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 +65,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 +112,7 @@ PanelWindow {
signal exitStarted
signal exitFinished
signal popupHeightChanged
signal popupChromeGeometryChanged
function startExit() {
if (exiting || _isDestroying) {
@@ -68,6 +120,7 @@ PanelWindow {
}
exiting = true;
exitStarted();
popupChromeGeometryChanged();
exitAnim.restart();
exitWatchdog.restart();
if (NotificationService.removeFromVisibleNotifications)
@@ -132,22 +185,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.connectedFrameMode && !exiting && !_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 +272,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 +314,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 +360,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 +397,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 +417,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 +434,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 +457,7 @@ PanelWindow {
return Theme.snap(screen.width - alignedWidth - barRight, dpr);
}
function getContentY() {
function getAllocatedContentY() {
if (!screen)
return 0;
@@ -313,7 +467,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 +483,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 +594,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
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 +624,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 +653,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 +662,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.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
sourceRect.color: win.connectedFrameMode ? Theme.popupLayerColor(Theme.surfaceContainer) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
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,10 +705,10 @@ PanelWindow {
Rectangle {
anchors.fill: parent
anchors.margins: content.cardInset
radius: Theme.cornerRadius
radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
border.color: win.connectedFrameMode ? "transparent" : BlurService.borderColor
border.width: win.connectedFrameMode ? 0 : BlurService.borderWidth
z: 100
}
@@ -481,10 +739,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
@@ -654,7 +925,7 @@ PanelWindow {
win.descriptionExpanded = !win.descriptionExpanded;
}
propagateComposedEvents: true
propagateComposedEvents: false
onPressed: mouse => {
if (parent.hoveredLink)
mouse.accepted = false;
@@ -850,14 +1121,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 {
@@ -869,15 +1141,7 @@ PanelWindow {
if (win.exiting)
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;
}
}
@@ -908,20 +1172,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
@@ -929,9 +1201,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
}
]
}
@@ -943,16 +1223,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) {
@@ -977,35 +1257,35 @@ 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
}
}

View File

@@ -8,23 +8,46 @@ 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 +131,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 +162,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 +198,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 +236,319 @@ 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 +556,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 +603,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();

View File

@@ -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
@@ -693,6 +694,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 => {
@@ -807,11 +810,42 @@ Item {
}
}
Item {
visible: SettingsData.frameEnabled
width: parent.width
implicitHeight: frameNote.implicitHeight + Theme.spacingS * 2
Row {
id: frameNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Spacing and size are managed by Frame mode")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard {
iconName: "space_bar"
title: I18n.tr("Spacing")
settingKey: "barSpacing"
visible: selectedBarConfig?.enabled
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
SettingsSliderRow {
id: edgeSpacingSlider
@@ -1012,6 +1046,8 @@ Item {
SettingsSliderRow {
id: barTransparencySlider
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Bar Transparency")
value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0
@@ -1053,6 +1089,64 @@ Item {
restoreMode: Binding.RestoreBinding
}
}
Item {
visible: SettingsData.frameEnabled
width: parent.width
implicitHeight: transparencyFrameNote.implicitHeight + Theme.spacingS * 2
Row {
id: transparencyFrameNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Opacity is controlled by Frame Border Opacity in Frame settings")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
}
Item {
visible: dankBarTab.connectedFrameModeActive
width: parent.width
implicitHeight: connectedFrameStyleNote.implicitHeight + Theme.spacingS * 2
Row {
id: connectedFrameStyleNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Connected Frame mode keeps bar shadow override, border, and corner overrides off while active")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard {
@@ -1063,6 +1157,8 @@ Item {
collapsible: true
expanded: true
visible: selectedBarConfig?.enabled
enabled: !dankBarTab.connectedFrameModeActive
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom"
@@ -1296,6 +1392,8 @@ Item {
SettingsToggleRow {
text: I18n.tr("Square Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.squareCorners ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
squareCorners: checked
@@ -1343,6 +1441,8 @@ Item {
SettingsToggleRow {
text: I18n.tr("Goth Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
gothCornersEnabled: checked
@@ -1392,6 +1492,8 @@ Item {
iconName: "border_style"
title: I18n.tr("Border")
visible: selectedBarConfig?.enabled
enabled: !dankBarTab.connectedFrameModeActive
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
checked: selectedBarConfig?.borderEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
borderEnabled: checked

View File

@@ -7,6 +7,9 @@ import qs.Modules.Settings.Widgets
Item {
id: root
readonly property bool connectedFrameModeActive: SettingsData.frameEnabled
&& SettingsData.motionEffect === 1
&& SettingsData.directionalAnimationMode === 3
FileBrowserModal {
id: dockLogoFileBrowser
@@ -544,6 +547,8 @@ Item {
SettingsSliderRow {
text: I18n.tr("Exclusive Zone Offset")
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
value: SettingsData.dockBottomGap
minimum: -100
maximum: 100
@@ -553,6 +558,8 @@ Item {
SettingsSliderRow {
text: I18n.tr("Margin")
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
value: SettingsData.dockMargin
minimum: 0
maximum: 100
@@ -561,11 +568,42 @@ Item {
}
}
Item {
visible: root.connectedFrameModeActive
width: parent.width
implicitHeight: dockConnectedNote.implicitHeight + Theme.spacingS * 2
Row {
id: dockConnectedNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Connected Frame mode manages dock edge offset, transparency, blur, and border styling")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard {
width: parent.width
iconName: "opacity"
title: I18n.tr("Transparency")
settingKey: "dockTransparency"
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
SettingsSliderRow {
text: I18n.tr("Dock Transparency")
@@ -585,6 +623,8 @@ Item {
settingKey: "dockBorder"
collapsible: true
expanded: false
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
SettingsToggleRow {
text: I18n.tr("Border")

View File

@@ -0,0 +1,352 @@
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
// ── Enable Frame ──────────────────────────────────────────────────
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)
}
}
// ── Border ────────────────────────────────────────────────────────
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")
description: SettingsData.connectedFrameModeActive
? I18n.tr("Controls the radius of the frame and all connected popout, dock, and modal surfaces while Connected Mode is active")
: I18n.tr("Controls the frame border radius. This also becomes the connected surface radius whenever Connected Mode is active")
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("Height of horizontal bars / width of vertical bars in frame mode")
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")
description: I18n.tr("Frame border opacity. Controls all surface opacity globally when Connected Mode is active")
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 is controlled by Background Blur in Theme & Colors")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
// Color mode buttons
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;
if (fc === "primary") return 1;
if (fc === "surface") return 2;
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;
}
}
}
// Custom color swatch — only visible when a hex color is stored (Custom mode)
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 {
id: colorSwatch
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();
}
}
}
}
}
}
// ── Bar Integration ───────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "toolbar"
title: I18n.tr("Integrations")
settingKey: "frameBarIntegration"
collapsible: true
expanded: true
visible: SettingsData.frameEnabled
SettingsToggleRow {
visible: CompositorService.isNiri
settingKey: "frameShowOnOverview"
tags: ["frame", "overview", "show", "hide", "niri"]
text: I18n.tr("Show on Overview")
description: I18n.tr("Show the bar and frame during Niri overview mode")
checked: SettingsData.frameShowOnOverview
onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
}
SettingsToggleRow {
visible: SettingsData.frameEnabled
settingKey: "directionalAnimationMode"
tags: ["frame", "connected", "popout", "corner", "animation"]
text: I18n.tr("Connected Mode")
description: I18n.tr("Popouts emerge flush from the bar edge as one continuous piece")
checked: SettingsData.connectedFrameModeActive
onToggled: checked => {
if (checked) {
if (SettingsData.directionalAnimationMode !== 3)
SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode);
SettingsData.set("motionEffect", 1);
SettingsData.set("directionalAnimationMode", 3);
} else {
SettingsData.set("directionalAnimationMode", SettingsData.previousDirectionalMode);
}
}
Connections {
target: SettingsData
function onDirectionalAnimationModeChanged() {}
function onMotionEffectChanged() {}
}
}
SettingsToggleRow {
visible: SettingsData.frameEnabled
settingKey: "frameCloseGaps"
tags: ["frame", "connected", "gap", "edge", "flush", "popout", "notification"]
text: I18n.tr("Close the Gaps")
description: I18n.tr("Connected popouts and notification corners sit flush against the frame edge")
checked: SettingsData.frameCloseGaps
enabled: SettingsData.connectedFrameModeActive
opacity: enabled ? 1.0 : 0.5
onToggled: checked => SettingsData.set("frameCloseGaps", checked)
}
SettingsButtonGroupRow {
visible: SettingsData.frameEnabled
settingKey: "frameLauncherEmergeSide"
tags: ["frame", "connected", "launcher", "modal", "emerge", "direction", "bottom", "top"]
text: I18n.tr("Launcher Emerge Side")
description: I18n.tr("Which frame edge the Launcher slides in from. Other modals emerge from the opposite side.")
model: [I18n.tr("Bottom"), I18n.tr("Top")]
currentIndex: SettingsData.frameLauncherEmergeSide === "top" ? 1 : 0
enabled: SettingsData.connectedFrameModeActive
opacity: enabled ? 1.0 : 0.5
onSelectionChanged: (index, selected) => {
if (!selected) return;
SettingsData.set("frameLauncherEmergeSide", index === 1 ? "top" : "bottom");
}
}
}
// ── Display Assignment ────────────────────────────────────────────
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)
}
}
}
}
}

View File

@@ -11,6 +11,7 @@ import qs.Modules.Settings.Widgets
Item {
id: themeColorsTab
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)
@@ -1615,10 +1616,14 @@ Item {
SettingsSliderRow {
tab: "theme"
tags: ["popup", "transparency", "opacity", "modal"]
tags: ["surface", "popup", "transparency", "opacity", "modal"]
settingKey: "popupTransparency"
text: I18n.tr("Popup Transparency")
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
text: I18n.tr("Surface Opacity")
description: themeColorsTab.connectedFrameModeActive
? I18n.tr("Connected Frame mode follows Surface Opacity from the Frame tab for connected popouts, docks, and modal surfaces")
: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
enabled: !themeColorsTab.connectedFrameModeActive
opacity: themeColorsTab.connectedFrameModeActive ? 0.5 : 1.0
value: Math.round(SettingsData.popupTransparency * 100)
minimum: 0
maximum: 100
@@ -1632,7 +1637,9 @@ Item {
tags: ["corner", "radius", "rounded", "square"]
settingKey: "cornerRadius"
text: I18n.tr("Corner Radius")
description: I18n.tr("0 = square corners")
description: themeColorsTab.connectedFrameModeActive
? I18n.tr("Controls general UI rounding. Connected frame popouts, docks, and modal surfaces follow Border Radius in the Frame tab while Connected Frame mode is active")
: I18n.tr("0 = square corners")
value: SettingsData.cornerRadius
minimum: 0
maximum: 32
@@ -1837,7 +1844,11 @@ 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")
: (themeColorsTab.connectedFrameModeActive
? I18n.tr("Connected Frame mode follows Frame Blur for connected surfaces while this remains the master blur availability toggle")
: 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)

View File

@@ -55,6 +55,190 @@ 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.");
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
visible: SettingsData.motionEffect === 1
}
SettingsDropdownRow {
visible: SettingsData.motionEffect === 1
tab: "typography"
tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll", "connected"]
settingKey: "directionalAnimationMode"
text: I18n.tr("Directional Behavior")
description: {
if (SettingsData.connectedFrameModeActive)
return I18n.tr("Popouts emerge flush from the bar edge as a single continuous piece, with corner connectors bridging the junction");
return I18n.tr("How the popout emerges from the DankBar");
}
options: SettingsData.frameEnabled ? [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll"), I18n.tr("Connected")] : [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")]
currentValue: {
switch (SettingsData.directionalAnimationMode) {
case 1:
return I18n.tr("Slide");
case 2:
return I18n.tr("Roll");
case 3:
return SettingsData.frameEnabled ? I18n.tr("Connected") : I18n.tr("Slide");
default:
return I18n.tr("Overlap");
}
}
onValueChanged: value => {
if (value === I18n.tr("Slide"))
SettingsData.set("directionalAnimationMode", 1);
else if (value === I18n.tr("Roll"))
SettingsData.set("directionalAnimationMode", 2);
else if (value === I18n.tr("Connected") && SettingsData.frameEnabled) {
if (SettingsData.directionalAnimationMode !== 3)
SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode);
SettingsData.set("directionalAnimationMode", 3);
} else
SettingsData.set("directionalAnimationMode", 0);
}
}
}
SettingsCard {
tab: "typography"
tags: ["font", "family", "text", "typography"]
@@ -285,12 +469,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() {
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -121,9 +121,9 @@ Scope {
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
@@ -154,45 +154,69 @@ Scope {
id: scaleTransform
origin.x: contentContainer.width / 2
origin.y: contentContainer.height / 2
xScale: overviewScope.overviewOpen ? 1 : 0.96
yScale: overviewScope.overviewOpen ? 1 : 0.96
xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
Behavior on xScale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on yScale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
}
Translate {
id: motionTransform
x: 0
y: overviewScope.overviewOpen ? 0 : Theme.spacingL
x: {
if (overviewScope.overviewOpen)
return 0;
if (Theme.isDirectionalEffect)
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 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.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}

View File

@@ -5,6 +5,7 @@ import Quickshell.Wayland
import qs.Common
import qs.Modals.DankLauncherV2
import qs.Services
import qs.Widgets
Scope {
id: niriOverviewScope
@@ -124,6 +125,19 @@ Scope {
item: overlayVisible && spotlightContainer.visible ? spotlightContainer : null
}
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: spotlightContainer.visible && spotlightContainer.opacity > 0
blurX: spotlightContainer.x + spotlightContainer.width * (1 - s) * 0.5
blurY: spotlightContainer.y + spotlightContainer.height * (1 - s) * 0.5
blurWidth: active ? spotlightContainer.width * s : 0
blurHeight: active ? spotlightContainer.height * s : 0
blurRadius: Theme.cornerRadius
}
onShouldShowSpotlightChanged: {
if (shouldShowSpotlight) {
if (launcherContent?.controller) {
@@ -202,8 +216,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) {
@@ -234,8 +266,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
@@ -245,6 +277,7 @@ Scope {
Behavior on scale {
id: scaleAnimation
enabled: !Theme.isConnectedEffect
NumberAnimation {
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
@@ -258,6 +291,7 @@ Scope {
}
Behavior on opacity {
enabled: !Theme.isConnectedEffect
NumberAnimation {
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
@@ -265,6 +299,23 @@ Scope {
}
}
// 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.variantDuration(Theme.popoutAnimationDuration, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: overlayWindow.shouldShowSpotlight ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
onRunningChanged: {
if (running || !spotlightContainer.animatingOut)
return;
niriOverviewScope.resetState();
}
}
}
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)

View File

@@ -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
}
}
}

View 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
}
}
}
}

View File

@@ -0,0 +1,415 @@
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"
// ── Derived layout ──
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
}
}
}
}

View File

@@ -1,17 +1,12 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
Item {
id: root
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
property real popupWidth: 400
property real popupHeight: 300
property real triggerX: 0
@@ -31,9 +26,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
@@ -45,90 +37,60 @@ 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
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
}
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
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
// 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 open() {
if (impl.item)
impl.item.open();
}
function close() {
if (impl.item)
impl.item.close();
}
function toggle() {
shouldBeVisible ? 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,474 +109,90 @@ Item {
setBarContext(pos, bottomGap);
}
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
sourceComponent: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
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();
}
// shouldBeVisible is two-way — backend's open()/close() flips it internally.
it.shouldBeVisible = root.shouldBeVisible;
it.shouldBeVisibleChanged.connect(function () {
if (root.shouldBeVisible !== it.shouldBeVisible)
root.shouldBeVisible = it.shouldBeVisible;
});
it.opened.connect(root.opened);
it.popoutClosed.connect(root.popoutClosed);
it.backgroundClicked.connect(root.backgroundClicked);
}
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();
}
}
}
Timer {
id: closeTimer
interval: animationDuration
onTriggered: {
if (!shouldBeVisible) {
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
PopoutManager.hidePopout(root);
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: 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;
}
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
}
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":
console.warn("DankPopout: 'bottom' layer is not valid for popouts. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.warn("DankPopout: '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)
}
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
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;
}
}
target: root
function onShouldBeVisibleChanged() {
if (impl.item && impl.item.shouldBeVisible !== root.shouldBeVisible)
impl.item.shouldBeVisible = root.shouldBeVisible;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,624 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
Item {
id: root
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
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: 0.96
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property bool suspendShadowWhileResizing: false
property bool shouldBeVisible: false
property bool isClosing: false
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 _surfaceW: 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 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);
}
readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab
function updateSurfacePosition() {
if (useBackgroundWindow && shouldBeVisible) {
_surfaceMarginLeft = alignedX - shadowBuffer;
_surfaceW = alignedWidth + shadowBuffer * 2;
}
}
function open() {
if (!screen)
return;
closeTimer.stop();
isClosing = false;
_frozenMaskX = maskX;
_frozenMaskY = maskY;
_frozenMaskWidth = maskWidth;
_frozenMaskHeight = maskHeight;
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(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: animationDuration
onTriggered: {
if (!shouldBeVisible) {
isClosing = false;
contentWindow.visible = false;
if (useBackgroundWindow)
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: 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"
// When there's no overlay to render, skip buffer updates. Re-evaluates if
// overlayContent is assigned later (e.g., via a dispatcher forwarding it).
updatesEnabled: root.overlayContent !== null
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
}
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":
console.warn("DankPopout: 'bottom' layer is not valid for popouts. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.warn("DankPopout: '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 string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "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)
}
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
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;
}
}
}
}
}

View File

@@ -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() {

View File

@@ -3549,7 +3549,7 @@
},
{
"section": "popupTransparency",
"label": "Popup Transparency",
"label": "Surface Opacity",
"tabIndex": 10,
"category": "Theme & Colors",
"keywords": [
@@ -3567,6 +3567,7 @@
"popup",
"scheme",
"style",
"surface",
"their",
"theme",
"translucent",