Files
Huỳnh Thiện Lộc 8ff7ce2cea feat(island): add system tray slot and overflow menu to compact home (#3780)
* feat(island): add system tray slot and overflow menu to compact home

- Add system tray slot and overflow expander menu in Dynamic Island
- Share tray icon path resolution and DBus context menu fallback in TrayMenuManager
- Support edge-anchored context menu positioning across top, bottom, left, and right
- Lock bottom and edge anchoring in DContextMenu to prevent animation jitter

* fix(island): only instantiate tray menus when tray slot is enabled

* feat(island): expand tray item and caret click area to slot height

* fix(island): use Theme.surfaceTextHover and restore DContextMenu height behavior

* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island

* Revert "refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island"

This reverts commit 29888f89cd.

* refactor(tray): use shared TrayMenuManager helpers and TrayItemIcon, support tray order and IPC in island

* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid

* feat(island): support tray item drag and drop reordering

* fix(island): prevent re-entrant tray drag reordering on model destruction

* fix(tray): remove native submenu anchor hydration to prevent popup stutter

* fix(tray): decouple submenu hydrator from anchor window to avoid stutter

* fix(tray): restore anchor window on submenu hydrator and disable on island

* tray: fix bar overflow surface & reuse max visible items on the island
2026-10-14 16:55:26 -04:00

1065 lines
41 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modals.Clipboard
import qs.Modals.DLauncherV2.Components
import qs.Modules.ControlCenter
import qs.Modules.DBar
import qs.Modules.DDash
QtObject {
id: root
property string interactionMode: "click"
property bool hoverExpanded: false
property bool inputSuspended: false
property bool expanded: false
property bool pointerInside: false
// A right-click face swap resizes the island under the pointer; hover stays off until it leaves.
property bool hoverHeld: false
property int slotHoverCount: 0
readonly property bool slotHovered: slotHoverCount > 0
property bool keyboardDismissRequested: false
property string activeActivity: "home"
property bool mediaAvailable: false
property bool mediaPreferred: false
property bool mediaAutoSwitch: false
property bool launcherCycleEnabled: false
property bool launcherSessionActive: false
property bool launcherInputFocused: false
onLauncherInputFocusedChanged: {
if (launcherInputFocused)
hoverExpanded = false;
}
property var transientSurfaces: null
readonly property bool transientSurfacesActive: transientSurfaces?.active ?? false
onTransientSurfacesActiveChanged: {
if (!transientSurfacesActive)
return;
hoverExpanded = false;
hoverCloseTimer.stop();
}
property string launcherPendingQuery: ""
property string launcherPendingMode: ""
property string controlCenterPendingSection: ""
property bool notificationExpandAllowed: false
property bool keyboardYielded: false
property real alongOffset: 0
property real outerGap: 8
property var barConfig: null
property var screen: null
property string edge: "top"
// Screen position of the host window origin; item coordinates inside the island are window-relative.
property real sectionOriginX: 0
property real sectionOriginY: 0
readonly property bool isVertical: edge === "left" || edge === "right"
readonly property bool farEdge: edge === "bottom" || edge === "right"
property real cornerRadius: 34
property real pillRadius: cornerRadius
readonly property real edgeCornerRadius: Math.round(cornerRadius * 0.75)
property bool freeMode: false
property bool dotMode: false
// Hosted in a bar: the sheet is flush with the band, so its near corners stay square.
property bool embedded: false
property bool notch: false
property real dotSize: 48
property real compactThickness: 38
property string batteryStyle: "duo"
property bool mediaClockVisible: true
property int transientTimeout: 2200
property int notificationTimeout: 5000
property string transientReturnActivity: ""
property string suspendedActivity: ""
property bool suspendedExpanded: false
property bool suspendedKeyboardDismissRequested: false
property int hoverOpenDelay: 150
property int hoverCloseDelay: 150
property int mediaReturnDelay: 1800
property real controlCenterMaxHeight: 640
property string editingActivity: ""
property real editRoom: 0
// The bar host grants the room once its surface has presented a frame at the grown size.
property bool editRoomGranted: false
readonly property real controlCenterSheetInset: 30
readonly property int controlCenterColumnCap: CcMetrics.columnCapFor(dashboardAvailableWidth - controlCenterSheetInset - PopoutMetrics.editOverflow * 2)
readonly property int controlCenterColumns: Math.min(CcMetrics.gridColumns, controlCenterColumnCap)
readonly property real controlCenterSheetWidth: CcMetrics.sheetWidthFor(controlCenterColumns)
readonly property real controlCenterMaxWidth: CcMetrics.sheetWidthFor(editingActivity === "controlcenter" ? controlCenterColumnCap : controlCenterColumns) + controlCenterSheetInset
readonly property real controlCenterHeight: controlCenterHeightFor(editRoomGranted && editingActivity === "controlcenter")
readonly property bool compactDense: compactThickness < 40
readonly property real compactFaceThickness: BarMetrics.compactFaceThickness(compactThickness)
readonly property real compactIconSize: Math.max(14, Math.min(32, compactThickness - 8))
property bool homeCompactTight: false
readonly property real homeCompactFaceThickness: homeCompactTight ? Math.max(16, Math.min(32, compactThickness - 8)) : compactFaceThickness
property int unreadNotificationCount: 0
readonly property bool homeNotificationBadge: (root.dotMode || SettingsData.islandHomeGroupEnabled(root.barConfig, "notifications")) && unreadNotificationCount > 0
readonly property bool homeWeatherEnabled: SettingsData.weatherEnabled && SettingsData.islandHomeGroupEnabled(root.barConfig, "weather")
readonly property real homeSlotMargin: homeCompactTight ? Theme.spacingS : Theme.spacingM
property real homeContentLength: 200
property real mediaContentLength: 360
property real dashboardAvailableWidth: 1920
property real dashboardAvailableHeight: 1080
readonly property int dashboardColumnCap: DashMetrics.columnCapFor(dashboardAvailableWidth - PopoutMetrics.editOverflow * 2, SettingsData.showWeekNumber)
readonly property real dashboardMaxWidth: Math.min(dashboardAvailableWidth, DashMetrics.widthFor(SettingsData.showWeekNumber, undefined, editingActivity !== "" ? dashboardColumnCap : DashRegistry.widestPanelColumns))
property var dashboardContentHeights: ({})
readonly property real dashChromeHeight: DashMetrics.islandHandleChromeHeight
readonly property real editSurfaceHeight: editRoom <= 0 ? 0 : editingActivity === "controlcenter" ? controlCenterHeightFor(true) : dashboardHeightFor(editingActivity, true)
readonly property real dashboardHeight: Math.min(dashboardAvailableHeight, Math.max(DashMetrics.tabDefaultHeight + root.dashChromeHeight, ...Object.values(dashboardContentHeights)))
readonly property int dashboardRowBudget: Math.max(DashMetrics.minimumTabRows, Math.floor((dashboardAvailableHeight - DashMetrics.islandEditChromeHeight + DashMetrics.gridGap) / (DashMetrics.gridRowUnit + DashMetrics.gridGap)))
readonly property real mediaCompactMaxLength: 360
property real notificationContentLength: 0
readonly property real notificationCompactMinLength: isVertical ? compactFaceThickness : (compactDense ? 200 : 240)
readonly property real notificationCompactMaxLength: mediaCompactMaxLength
signal sessionStarted(string activityId)
function setHomeContentLength(length) {
const next = Math.ceil(length);
if (!isFinite(next) || next <= 0 || Math.abs(next - homeContentLength) < 2)
return;
homeContentLength = next;
}
function setNotificationContentLength(length) {
const next = Math.ceil(length);
if (!isFinite(next) || next <= 0 || Math.abs(next - notificationContentLength) < 2)
return;
notificationContentLength = next;
}
function setDashboardContentHeight(activityId, height) {
const next = Math.ceil(height);
if (!isFinite(next) || next <= 0 || dashboardContentHeights[activityId] === next)
return;
dashboardContentHeights = Object.assign({}, dashboardContentHeights, {
[activityId]: next
});
}
function dashEntryIdFor(activityId) {
switch (activityId) {
case "home":
return DashRegistry.overviewId;
case "notificationcenter":
return "notifications";
}
return activityId;
}
function dashboardWidthFor(activityId) {
return Math.min(dashboardAvailableWidth, DashMetrics.widthFor(SettingsData.showWeekNumber, undefined, DashMetrics.panelColumnsFor(dashEntryIdFor(activityId))));
}
function dashboardHeightFor(activityId, withEditRoom) {
const minimum = DashMetrics.panelHeightFor(dashEntryIdFor(activityId));
const height = Math.max(minimum + root.dashChromeHeight, dashboardContentHeights[activityId] ?? 0);
return Math.min(dashboardAvailableHeight, height + (withEditRoom ? root.editRoom : 0));
}
function controlCenterHeightFor(withEditRoom) {
return Math.max(320, Math.min(controlCenterMaxHeight, destinationContentHeight("controlcenter") + (withEditRoom ? root.editRoom : 0)));
}
function dashboardTargetFor(activityId) {
return sheetTarget(dashboardWidthFor(activityId), dashboardHeightFor(activityId, root.editRoomGranted && root.editingActivity === activityId));
}
function setMediaContentLength(length) {
const next = Math.ceil(Math.min(root.mediaCompactMaxLength, length));
if (!isFinite(next) || next <= 0 || Math.abs(next - mediaContentLength) < 2)
return;
mediaContentLength = next;
}
property real powerMenuContentWidth: 0
function setPowerMenuContentWidth(width) {
const next = Math.ceil(width);
if (!isFinite(next) || next <= 0 || Math.abs(next - powerMenuContentWidth) < 2)
return;
powerMenuContentWidth = next;
}
readonly property var destinations: ["launcher", "controlcenter", "wallpaper", "weather", "notificationcenter", "clipboard", "powermenu"]
readonly property var blankClickOwners: ["launcher", "controlcenter", "wallpaper", "notificationcenter", "clipboard", "powermenu"]
readonly property var destinationDefaults: ({
"launcher": {
"contentLength": 160,
"contentHeight": 0
},
"controlcenter": {
"contentLength": 180,
"contentHeight": 420
},
"wallpaper": {
"contentLength": 150,
"contentHeight": 0
},
"weather": {
"contentLength": 130,
"contentHeight": 0
},
"notificationcenter": {
"contentLength": 170,
"contentHeight": 320
},
"clipboard": {
"contentLength": 150,
"contentHeight": 0
},
"powermenu": {
"contentLength": 150,
"contentHeight": 0
}
})
property var destinationState: root.freshDestinationState()
property int destinationRevision: 0
property int requestedRevision: 0
function freshDestinationState() {
const state = {};
for (const id of destinations) {
state[id] = {
"requested": false,
"ready": false,
"pending": false,
"pendingKeyboardFocus": false,
"contentLength": destinationDefaults[id].contentLength,
"contentHeight": destinationDefaults[id].contentHeight
};
}
return state;
}
function destinationEntry(activityId) {
destinationRevision;
return destinationState[activityId] ?? null;
}
function isDestination(activityId) {
return destinations.indexOf(activityId) !== -1;
}
function visualsRequested(activityId) {
requestedRevision;
return destinationState[activityId]?.requested ?? false;
}
function visualsReady(activityId) {
return destinationEntry(activityId)?.ready ?? false;
}
function destinationContentLength(activityId) {
return destinationEntry(activityId)?.contentLength ?? 0;
}
function destinationContentHeight(activityId) {
return destinationEntry(activityId)?.contentHeight ?? 0;
}
function setDestinationContentLength(activityId, length) {
if (isSystemActivity(activityId)) {
setSystemStateContentLength(length);
return;
}
const entry = destinationState[activityId];
const next = Math.ceil(length);
if (!entry || !isFinite(next) || next <= 0 || Math.abs(next - entry.contentLength) < 2)
return;
entry.contentLength = next;
destinationRevision++;
}
function setEditing(activityId, editing, room = 0) {
if (editing) {
editRoomGranted = false;
editingActivity = activityId;
editRoom = room;
// Option sheets open child popups; a hover peek would collapse under them
hoverExpanded = false;
hoverCloseTimer.stop();
} else if (editingActivity === activityId) {
editRoomGranted = false;
editRoom = 0;
editingActivity = "";
}
}
function setDestinationContentHeight(activityId, height) {
const entry = destinationState[activityId];
const next = Math.round(height);
if (!entry || !isFinite(next) || Math.abs(next - entry.contentHeight) < 1)
return;
entry.contentHeight = next;
destinationRevision++;
}
function setVisualsReady(activityId, ready) {
const entry = destinationState[activityId];
if (!entry || entry.ready === ready)
return;
entry.ready = ready;
destinationRevision++;
}
function markVisualsReady(activityId) {
const entry = destinationState[activityId];
if (!entry)
return;
const pending = entry.pending;
entry.ready = true;
entry.pending = false;
destinationRevision++;
if (!pending)
return;
requestActivity(activityId, true, entry.pendingKeyboardFocus);
}
function clearPendingRequests(exceptId) {
let changed = false;
for (const id of destinations) {
const entry = destinationState[id];
if (id === exceptId || !entry.pending)
continue;
entry.pending = false;
changed = true;
}
if (changed)
destinationRevision++;
}
// The launcher stays loaded once built, so reopening never waits on a rebuild.
function keepsVisuals(activityId) {
return activityId === "launcher" && !SettingsData.dankLauncherV2UnloadOnClose && destinationState[activityId].ready;
}
function releaseIdleVisuals() {
for (const id of destinations) {
if (expanded && activeActivity === id)
continue;
const entry = destinationState[id];
if (root.keepsVisuals(id) || (!entry.requested && !entry.ready))
continue;
visualsReleaseTimer.restart();
return;
}
visualsReleaseTimer.stop();
}
function dropIdleVisuals() {
for (const id of destinations) {
if (expanded && activeActivity === id)
continue;
if (destinationState[id].pending || root.keepsVisuals(id))
continue;
dropVisuals(id);
}
}
function dropVisuals(activityId) {
const entry = destinationState[activityId];
entry.requested = false;
entry.ready = false;
entry.pending = false;
entry.contentHeight = destinationDefaults[activityId].contentHeight;
destinationRevision++;
requestedRevision++;
switch (activityId) {
case "launcher":
launcherSessionActive = false;
launcherInputFocused = false;
launcherPendingQuery = "";
launcherPendingMode = "";
return;
case "controlcenter":
controlCenterPendingSection = "";
return;
case "wallpaper":
case "clipboard":
keyboardYielded = false;
return;
}
}
readonly property real destinationCompactEndPad: root.isVertical ? Theme.spacingS : Math.max(root.compactFaceThickness / 2, Theme.spacingM) + Theme.spacingS
function destinationCompactLength(activityId) {
return Math.ceil(Math.max(root.compactFaceThickness, destinationContentLength(activityId) + root.destinationCompactEndPad * 2));
}
function homeGroups(side) {
const layout = SettingsData.getIslandHomeLayout(root.barConfig);
const clock = layout.findIndex(g => g.id === "clock");
const groups = side === "left" ? layout.slice(0, clock) : layout.slice(clock + 1);
return groups.filter(g => g.enabled).map(g => g.id);
}
readonly property var homeLeftGroups: homeGroups("left")
readonly property var homeRightGroups: homeGroups("right")
property string homeStatusContent: "battery"
property string homeClockDisplay: "both"
property string homeVolumeDisplay: "both"
property string homeBrightnessDisplay: "both"
readonly property real homeCompactLength: Math.ceil(homeSlotMargin * 2 + Math.max(homeContentLength, 1))
readonly property real mediaCompactLength: Math.ceil(Math.max(1, mediaContentLength))
// Corners touching the attached screen edge stay tighter than the ones facing the desktop.
function sheetRadii() {
if (root.freeMode)
return [root.cornerRadius, root.cornerRadius, root.cornerRadius, root.cornerRadius];
return root.edgeRadii(root.embedded || root.notch ? 0 : root.edgeCornerRadius, root.cornerRadius);
}
// [topLeft, topRight, bottomLeft, bottomRight] with `near` on the corners touching the edge.
function edgeRadii(near, far) {
switch (root.edge) {
case "bottom":
return [far, far, near, near];
case "left":
return [near, far, near, far];
case "right":
return [far, near, far, near];
}
return [near, near, far, far];
}
function pillTarget(alongSize, crossSize) {
const radius = Math.min(crossSize / 2, root.pillRadius);
const radii = root.edgeRadii(root.notch ? 0 : radius, radius);
return {
"width": root.isVertical ? crossSize : alongSize,
"height": root.isVertical ? alongSize : crossSize,
"offsetAlong": root.alongOffset,
"offsetCross": root.outerGap,
"topLeftRadius": radii[0],
"topRightRadius": radii[1],
"bottomLeftRadius": radii[2],
"bottomRightRadius": radii[3]
};
}
function sheetTarget(width, height) {
const radii = root.sheetRadii();
return {
"sheet": true,
"width": width,
"height": height,
"offsetAlong": root.alongOffset,
"offsetCross": root.outerGap,
"topLeftRadius": radii[0],
"topRightRadius": radii[1],
"bottomLeftRadius": radii[2],
"bottomRightRadius": radii[3]
};
}
readonly property var dotCompactTarget: ({
"width": root.dotSize,
"height": root.dotSize,
"offsetAlong": root.alongOffset,
"offsetCross": root.outerGap,
"topLeftRadius": root.dotSize / 2,
"topRightRadius": root.dotSize / 2,
"bottomLeftRadius": root.dotSize / 2,
"bottomRightRadius": root.dotSize / 2
})
readonly property var systemLevelActivities: ["volume", "brightness", "mic"]
readonly property var systemStateActivities: ["capslock", "powerprofile", "idleinhibitor", "charging"]
readonly property var systemActivities: systemLevelActivities.concat(systemStateActivities)
readonly property var dotTransientActivities: ["notification"].concat(systemActivities)
function isSystemActivity(activityId) {
return root.systemActivities.indexOf(activityId) !== -1;
}
// Transients keep their island pill so the dot can still show a notification or a level.
function usesDotFace(activityId) {
return root.dotMode && root.dotTransientActivities.indexOf(activityId) === -1;
}
readonly property var homeCompactTarget: pillTarget(homeCompactLength, homeCompactFaceThickness)
readonly property var mediaCompactTarget: pillTarget(mediaCompactLength, compactFaceThickness)
readonly property var homeExpandedTarget: dashboardTargetFor("home")
readonly property var mediaExpandedTarget: dashboardTargetFor("media")
readonly property var launcherExpandedTarget: sheetTarget(Math.min(dashboardAvailableWidth, LauncherMetrics.sizeWidth(SettingsData.dankLauncherV2Size)), Math.min(dashboardAvailableHeight, LauncherMetrics.sizeHeight(SettingsData.dankLauncherV2Size)))
readonly property var controlCenterExpandedTarget: sheetTarget(controlCenterSheetWidth + controlCenterSheetInset, controlCenterHeight)
readonly property var systemCompactTarget: pillTarget(root.isVertical ? 240 : (SettingsData.osdAlwaysShowValue ? 330 : 282), compactFaceThickness)
property real systemStateContentLength: 0
readonly property var systemStateCompactTarget: pillTarget(Math.ceil(Math.max(compactFaceThickness, systemStateContentLength + destinationCompactEndPad * 2)), compactFaceThickness)
readonly property var systemExpandedTarget: sheetTarget(460, 176)
readonly property var notificationCompactTarget: pillTarget(Math.ceil(Math.max(notificationCompactMinLength, Math.min(notificationCompactMaxLength, notificationContentLength))), compactFaceThickness)
readonly property var notificationExpandedTarget: sheetTarget(520, 220)
readonly property var notificationCenterExpandedTarget: dashboardTargetFor("notificationcenter")
readonly property var clipboardExpandedTarget: sheetTarget(Math.min(dashboardAvailableWidth, ClipboardConstants.sizeWidth(SettingsData.clipboardSize)), Math.min(dashboardAvailableHeight, ClipboardConstants.sizeHeight(SettingsData.clipboardSize)))
readonly property var powerMenuExpandedTarget: sheetTarget(Math.max(1, Math.min(dashboardAvailableWidth, powerMenuContentWidth)), Math.max(1, Math.min(dashboardAvailableHeight, destinationContentHeight("powermenu"))))
readonly property bool systemActivityActive: isSystemActivity(activeActivity)
readonly property bool notificationActive: activeActivity === "notification"
readonly property bool notificationHeldForSystem: root.systemActivityActive && root.transientReturnActivity === "notification"
readonly property bool transientActive: systemActivityActive || notificationActive
readonly property bool timeoutSuspended: pointerInside || (expanded && !notificationActive)
readonly property bool activityOwnsBlankClicks: blankClickOwners.indexOf(activeActivity) !== -1
readonly property bool hoverExpandEnabled: root.interactionMode === "hybrid" && !root.systemActivityActive
function compactTargetFor(activityId) {
if (root.usesDotFace(activityId))
return dotCompactTarget;
switch (activityId) {
case "notification":
return notificationCompactTarget;
case "media":
return mediaCompactTarget;
case "home":
return homeCompactTarget;
}
if (systemLevelActivities.indexOf(activityId) !== -1)
return systemCompactTarget;
if (systemStateActivities.indexOf(activityId) !== -1)
return systemStateCompactTarget;
return isDestination(activityId) ? pillTarget(destinationCompactLength(activityId), compactFaceThickness) : homeCompactTarget;
}
readonly property var compactTarget: compactTargetFor(activeActivity)
function expandedTargetFor(activityId) {
switch (activityId) {
case "home":
return homeExpandedTarget;
case "notification":
return notificationExpandedTarget;
case "launcher":
return launcherExpandedTarget;
case "controlcenter":
return controlCenterExpandedTarget;
case "notificationcenter":
return notificationCenterExpandedTarget;
case "clipboard":
return clipboardExpandedTarget;
case "powermenu":
return powerMenuExpandedTarget;
case "media":
return mediaExpandedTarget;
}
return isSystemActivity(activityId) ? systemExpandedTarget : dashboardTargetFor(activityId);
}
readonly property var expandedTarget: expandedTargetFor(activeActivity)
readonly property var targetDescriptor: expanded ? expandedTarget : compactTarget
function syncNotificationTimeout(restart) {
if (!notificationActive || timeoutSuspended || notificationTimeout <= 0) {
notificationTimer.stop();
return;
}
if (restart === true || !notificationTimer.running)
notificationTimer.restart();
}
onNotificationTimeoutChanged: syncNotificationTimeout(true)
function syncTransientTimeout(restart) {
if (!systemActivityActive || timeoutSuspended) {
transientTimer.stop();
return;
}
if (restart === true || !transientTimer.running)
transientTimer.restart();
}
function updateMediaAvailability(available) {
if (available) {
// A return inside the delay is a blip (track change, player switch), not a new start.
const blip = !mediaAvailable && mediaReturnTimer.running;
const started = !mediaAvailable && !blip;
mediaReturnTimer.stop();
mediaAvailable = true;
if (started && mediaAutoSwitch)
autoSwitchToMedia();
else if (blip && mediaPreferred && !expanded && activeActivity === "home")
activeActivity = "media";
return;
}
if (!mediaAvailable)
return;
mediaAvailable = false;
mediaReturnTimer.restart();
}
// Set directly: requestActivity would drop a destination open still waiting for its visuals.
function autoSwitchToMedia() {
if (activeActivity === "home" && !expanded && !(pointerInside && hoverExpandEnabled))
activeActivity = "media";
else if (systemActivityActive && transientReturnActivity === "home")
transientReturnActivity = "media";
else if ((notificationActive || notificationHeldForSystem) && suspendedActivity === "home" && !suspendedExpanded)
suspendedActivity = "media";
else
return;
mediaPreferred = true;
}
onInputSuspendedChanged: {
if (!inputSuspended)
return;
hoverExpanded = false;
hoverOpenTimer.stop();
hoverCloseTimer.stop();
keyboardDismissRequested = false;
expanded = false;
keyboardYielded = false;
launcherSessionActive = false;
clearPendingRequests("");
if (!transientActive)
activeActivity = idleActivity();
}
// The face a closed sheet lands on: media only when the user picked the media face.
function idleActivity() {
return mediaAvailable && mediaPreferred ? "media" : "home";
}
readonly property bool mediaFaceToggleAvailable: !expanded && mediaAvailable && (activeActivity === "home" || activeActivity === "media")
function toggleMediaFace() {
if (!mediaFaceToggleAvailable)
return false;
hoverHeld = true;
hoverOpenTimer.stop();
return requestActivity(activeActivity === "media" ? "home" : "media", false, false);
}
function requestHoverExpand() {
if (inputSuspended || hoverHeld || !hoverExpandEnabled || activeActivity === "powermenu")
return false;
if (notificationActive) {
hoverExpanded = true;
keyboardDismissRequested = false;
expanded = true;
hoverCloseTimer.stop();
notificationTimer.stop();
return true;
}
if (!requestExpand(false))
return false;
hoverExpanded = true;
return true;
}
function requestHoverCollapse() {
if (!hoverExpanded)
return false;
if (notificationActive) {
hoverExpanded = false;
expanded = false;
keyboardDismissRequested = false;
syncNotificationTimeout(true);
return true;
}
return requestCollapse();
}
function requestExpand(requestKeyboardFocus) {
if (inputSuspended)
return false;
if (systemActivityActive) {
syncTransientTimeout(true);
return false;
}
if (isDestination(activeActivity))
return requestActivity(activeActivity, true, requestKeyboardFocus);
if (notificationActive && !notificationExpandAllowed)
return requestNotificationCenter(false);
hoverExpanded = false;
clearPendingRequests("");
hoverCloseTimer.stop();
transientTimer.stop();
notificationTimer.stop();
keyboardDismissRequested = requestKeyboardFocus === true;
expanded = true;
return true;
}
function requestCollapse() {
hoverExpanded = false;
hoverOpenTimer.stop();
transientTimer.stop();
notificationTimer.stop();
keyboardDismissRequested = false;
keyboardYielded = false;
expanded = false;
launcherSessionActive = false;
clearPendingRequests("");
if (notificationActive) {
resumeAfterNotification(false);
return true;
}
if (systemActivityActive) {
finishTransient();
return true;
}
activeActivity = idleActivity();
return true;
}
function resolvedReturnActivity(activityId) {
switch (activityId) {
case "notification":
case "home":
return activityId;
case "media":
return mediaAvailable ? "media" : "home";
}
if (isDestination(activityId))
return activityId;
return idleActivity();
}
function requestActivity(activityId, shouldExpand, requestKeyboardFocus) {
const destination = isDestination(activityId);
if (activityId !== "home" && activityId !== "media" && !destination)
return false;
if (inputSuspended && shouldExpand === true)
return false;
if (activityId === "media" && !mediaAvailable)
return false;
if (activityId === "weather" && !SettingsData.weatherEnabled)
return false;
if (activityId !== "launcher")
launcherSessionActive = false;
clearPendingRequests(activityId);
if (activeActivity === activityId && expanded && shouldExpand === true) {
if (requestKeyboardFocus === true) {
keyboardDismissRequested = true;
hoverExpanded = false;
}
return true;
}
if (destination && shouldExpand === true && !visualsReady(activityId)) {
const entry = destinationState[activityId];
entry.pending = true;
entry.pendingKeyboardFocus = requestKeyboardFocus === true;
entry.requested = true;
destinationRevision++;
requestedRevision++;
return true;
}
if (requestKeyboardFocus === true)
hoverExpanded = false;
hoverOpenTimer.stop();
hoverCloseTimer.stop();
transientTimer.stop();
consumeTransientNotification();
keyboardDismissRequested = shouldExpand === true && requestKeyboardFocus === true;
expanded = shouldExpand === true;
activeActivity = activityId;
if ((activityId === "media" || activityId === "home") && shouldExpand !== true)
mediaPreferred = activityId === "media";
launcherSessionActive = activityId === "launcher" && expanded;
if (destination && expanded)
sessionStarted(activityId);
return true;
}
function requestLauncher(query, mode, shouldToggle) {
const open = activeActivity === "launcher" && expanded;
if (shouldToggle === true && open)
return requestCollapse();
launcherPendingQuery = query || "";
launcherPendingMode = mode || "";
const handled = requestActivity("launcher", true, true);
// An open launcher applies a new query or mode in place.
if (handled && open && (launcherPendingQuery || launcherPendingMode))
sessionStarted("launcher");
// Only a launcher that is still loading reads these later.
if (!destinationState.launcher.pending) {
launcherPendingQuery = "";
launcherPendingMode = "";
}
return handled;
}
function requestControlCenter(section, shouldToggle) {
if (shouldToggle === true && activeActivity === "controlcenter" && expanded)
return requestCollapse();
controlCenterPendingSection = section || "";
return requestActivity("controlcenter", true, true);
}
function requestWallpaper(shouldToggle) {
if (shouldToggle === true && activeActivity === "wallpaper" && expanded)
return requestCollapse();
return requestActivity("wallpaper", true, true);
}
function requestWeather(shouldToggle) {
if (shouldToggle === true && activeActivity === "weather" && expanded)
return requestCollapse();
return requestActivity("weather", true, true);
}
function requestNotificationCenter(shouldToggle) {
if (shouldToggle === true && activeActivity === "notificationcenter" && expanded)
return requestCollapse();
root.consumeTransientNotification();
return requestActivity("notificationcenter", true, true);
}
function requestClipboard(shouldToggle) {
if (shouldToggle === true && activeActivity === "clipboard" && expanded)
return requestCollapse();
return requestActivity("clipboard", true, true);
}
function requestPowerMenu(shouldToggle) {
if (shouldToggle === true && activeActivity === "powermenu" && expanded)
return requestCollapse();
return requestActivity("powermenu", true, true);
}
function cycleActivity(direction, shouldExpand) {
const activities = ["home"];
if (mediaAvailable)
activities.push("media");
if (launcherCycleEnabled)
activities.push("launcher");
activities.push("controlcenter");
activities.push("wallpaper");
if (SettingsData.weatherEnabled)
activities.push("weather");
activities.push("notificationcenter");
const currentIndex = Math.max(0, activities.indexOf(activeActivity));
const step = direction < 0 ? -1 : 1;
const nextIndex = (currentIndex + step + activities.length) % activities.length;
const next = activities[nextIndex];
if (!requestActivity(next, shouldExpand, shouldExpand))
return false;
if (next === "home" || next === "media")
mediaPreferred = next === "media";
return true;
}
function finishTransient() {
if (!systemActivityActive)
return;
transientTimer.stop();
const returnActivity = resolvedReturnActivity(transientReturnActivity);
transientReturnActivity = "";
if (returnActivity === "notification") {
hoverExpanded = false;
keyboardDismissRequested = false;
expanded = notificationExpandAllowed;
activeActivity = "notification";
syncNotificationTimeout(true);
return;
}
activeActivity = returnActivity;
}
function setSystemStateContentLength(length) {
const next = Math.ceil(length);
if (!isFinite(next) || next <= 0 || Math.abs(next - systemStateContentLength) < 2)
return;
systemStateContentLength = next;
}
function requestSystemActivity(activityId) {
if (!isSystemActivity(activityId))
return false;
launcherSessionActive = false;
clearPendingRequests("");
if (expanded && !notificationActive) {
if (activeActivity !== activityId)
return false;
transientTimer.stop();
return true;
}
if (notificationActive) {
notificationTimer.stop();
hoverExpanded = false;
hoverOpenTimer.stop();
hoverCloseTimer.stop();
keyboardDismissRequested = false;
expanded = false;
}
if (!systemActivityActive)
transientReturnActivity = activeActivity;
activeActivity = activityId;
syncTransientTimeout(true);
return true;
}
function canRequestNotification(critical) {
return notificationActive || !expanded || critical === true;
}
function consumeTransientNotification() {
if (transientReturnActivity === "notification")
transientReturnActivity = "";
if (!notificationActive)
return;
notificationTimer.stop();
suspendedActivity = "";
suspendedExpanded = false;
suspendedKeyboardDismissRequested = false;
}
function requestNotification(critical) {
if (!canRequestNotification(critical))
return false;
if (notificationActive)
return refreshNotification();
clearPendingRequests("");
transientTimer.stop();
suspendedActivity = activeActivity;
suspendedExpanded = expanded;
suspendedKeyboardDismissRequested = keyboardDismissRequested;
launcherSessionActive = false;
keyboardDismissRequested = false;
expanded = notificationExpandAllowed;
activeActivity = "notification";
syncNotificationTimeout(true);
return true;
}
function refreshNotification() {
if (!notificationActive)
return false;
syncNotificationTimeout(true);
return true;
}
function resumeAfterNotification(restoreExpanded) {
if (!notificationActive)
return;
notificationTimer.stop();
const nextExpanded = restoreExpanded === true && suspendedExpanded;
const suspendedFace = suspendedActivity === "home" || suspendedActivity === "media";
const nextActivity = !suspendedActivity || (suspendedFace && !nextExpanded) ? idleActivity() : suspendedActivity === "media" && !mediaAvailable ? "home" : suspendedActivity;
const nextKeyboardDismissRequested = nextExpanded && suspendedKeyboardDismissRequested;
suspendedActivity = "";
suspendedExpanded = false;
suspendedKeyboardDismissRequested = false;
expanded = nextExpanded;
keyboardDismissRequested = nextKeyboardDismissRequested;
activeActivity = nextActivity;
launcherSessionActive = nextActivity === "launcher" && nextExpanded;
if (nextExpanded && isDestination(nextActivity))
sessionStarted(nextActivity);
if (systemActivityActive && !expanded)
transientTimer.restart();
}
function completeNotification() {
if (transientReturnActivity === "notification")
transientReturnActivity = "";
if (!notificationActive)
return false;
resumeAfterNotification(true);
return true;
}
function requestToggle(requestKeyboardFocus) {
if (notificationActive && hoverExpanded && expanded)
return requestNotificationCenter(false);
if (expanded)
return requestCollapse();
return requestExpand(requestKeyboardFocus);
}
function slotHoverEntered() {
slotHoverCount++;
}
function slotHoverExited() {
slotHoverCount = Math.max(0, slotHoverCount - 1);
}
onSlotHoveredChanged: {
if (slotHovered) {
hoverOpenTimer.stop();
return;
}
if (pointerInside && hoverExpandEnabled && !expanded)
hoverOpenTimer.restart();
}
function updatePointerInside(inside) {
pointerInside = inside === true;
syncNotificationTimeout(!pointerInside);
syncTransientTimeout(!pointerInside);
if (!pointerInside) {
hoverHeld = false;
slotHoverCount = 0;
hoverOpenTimer.stop();
if (hoverExpanded && expanded)
hoverCloseTimer.restart();
return;
}
hoverCloseTimer.stop();
if (hoverExpandEnabled && !expanded && !slotHovered)
hoverOpenTimer.restart();
}
property Timer hoverOpenTimer: Timer {
interval: root.hoverOpenDelay
onTriggered: {
if (!root.pointerInside || !root.hoverExpandEnabled || root.inputSuspended || root.slotHovered)
return;
root.requestHoverExpand();
}
}
property Timer hoverCloseTimer: Timer {
interval: root.hoverCloseDelay
onTriggered: {
if (!root.pointerInside && root.hoverExpanded)
root.requestHoverCollapse();
}
}
property Timer mediaReturnTimer: Timer {
interval: root.mediaReturnDelay
onTriggered: {
if (root.mediaAvailable)
return;
root.mediaPreferred = false;
if (root.activeActivity !== "media")
return;
root.keyboardDismissRequested = false;
root.expanded = false;
root.activeActivity = "home";
}
}
property Timer transientTimer: Timer {
interval: root.transientTimeout
onTriggered: {
if (!root.transientActive || root.expanded)
return;
root.finishTransient();
}
}
property Timer notificationTimer: Timer {
interval: root.notificationTimeout
onTriggered: root.completeNotification()
}
property Timer visualsReleaseTimer: Timer {
interval: 260
onTriggered: root.dropIdleVisuals()
}
}