/* SPDX-FileCopyrightText: 2012-2013 Eike Hein SPDX-FileCopyrightText: 2024 Nate Graham SPDX-License-Identifier: GPL-2.0-or-later */ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import org.kde.plasma.core as PlasmaCore import org.kde.ksvg as KSvg import org.kde.plasma.extras as PlasmaExtras import org.kde.plasma.components as PlasmaComponents3 import org.kde.kirigami as Kirigami import org.kde.plasma.private.taskmanager as TaskManagerApplet import org.kde.plasma.plasmoid import "code/layoutmetrics.js" as LayoutMetrics import "code/tools.js" as TaskTools PlasmaCore.ToolTipArea { id: task activeFocusOnTab: true // To achieve a bottom-to-top layout on vertical panels, the task manager // is rotated by 180 degrees(see main.qml). This makes the tasks rotated, // so un-rotate them here to fix that. rotation: Plasmoid.configuration.reverseMode && Plasmoid.formFactor === PlasmaCore.Types.Vertical ? 180 : 0 // Transform for movement animations and zoom effect - FIXED BINDING LOOP transform: { // Simple approach: always return appropriate transforms without modifying cache in binding if (frame.zoomEnabled && (frame.isZoomed || frame.isAnimating)) { return [translateTransform, zoomTransform]; } else { return [translateTransform]; } } // Performance: Cache expensive layout calculations - REMOVED CACHING TO FIX BINDING LOOPS implicitHeight: inPopup ? LayoutMetrics.preferredHeightInPopup() : Math.max(tasksRoot.height / tasksRoot.plasmoid.configuration.maxStripes, LayoutMetrics.preferredMinHeight()) implicitWidth: tasksRoot.vertical ? Math.max(LayoutMetrics.preferredMinWidth(), Math.min(LayoutMetrics.preferredMaxWidth(), tasksRoot.width / tasksRoot.plasmoid.configuration.maxStripes)) : 0 Layout.fillWidth: true Layout.fillHeight: !inPopup Layout.maximumWidth: tasksRoot.vertical ? -1 : ((model.IsLauncher && !tasks.iconsOnly) ? tasksRoot.height / taskList.rows : LayoutMetrics.preferredMaxWidth()) Layout.maximumHeight: tasksRoot.vertical ? LayoutMetrics.preferredMaxHeight() : -1 required property var model required property int index required property /*main.qml*/ Item tasksRoot readonly property int pid: model.AppPid readonly property string appName: model.AppName readonly property string appId: model.AppId.replace(/\.desktop/, '') readonly property bool isIcon: tasksRoot.iconsOnly || model.IsLauncher property bool toolTipOpen: false property bool inPopup: false property bool isWindow: model.IsWindow property int childCount: model.ChildCount property int previousChildCount: 0 property alias labelText: label.text property QtObject contextMenu: null readonly property bool smartLauncherEnabled: !inPopup && !model.IsStartup property QtObject smartLauncherItem: null property Item audioStreamIcon: null property var audioStreams: [] property bool delayAudioStreamIndicator: false property bool completed: false readonly property bool audioIndicatorsEnabled: Plasmoid.configuration.interactiveMute readonly property bool hasAudioStream: audioStreams.length > 0 readonly property bool playingAudio: hasAudioStream && audioStreams.some(item => !item.corked) readonly property bool muted: hasAudioStream && audioStreams.every(item => item.muted) readonly property bool highlighted: (inPopup && activeFocus) || (!inPopup && containsMouse) || (task.contextMenu && task.contextMenu.status === PlasmaExtras.Menu.Open) || (!!tasksRoot.groupDialog && tasksRoot.groupDialog.visualParent === task) active: !inPopup && !tasksRoot.groupDialog && task.contextMenu?.status !== PlasmaExtras.Menu.Open interactive: model.IsWindow || mainItem.playerData location: Plasmoid.location mainItem: !Plasmoid.configuration.showToolTips || !model.IsWindow ? pinnedAppToolTipDelegate : openWindowToolTipDelegate onXChanged: { if (!completed) { return; } if (oldX < 0) { oldX = x; return; } moveAnim.x = oldX - x + translateTransform.x; moveAnim.y = translateTransform.y; oldX = x; moveAnim.restart(); } onYChanged: { if (!completed) { return; } if (oldY < 0) { oldY = y; return; } moveAnim.y = oldY - y + translateTransform.y; moveAnim.x = translateTransform.x; oldY = y; moveAnim.restart(); } property real oldX: -1 property real oldY: -1 SequentialAnimation { id: moveAnim property real x property real y onRunningChanged: { if (running) { ++task.parent.animationsRunning; } else { --task.parent.animationsRunning; } } ParallelAnimation { NumberAnimation { target: translateTransform properties: "x" from: moveAnim.x to: 0 easing.type: Easing.OutCubic duration: Kirigami.Units.longDuration } NumberAnimation { target: translateTransform properties: "y" from: moveAnim.y to: 0 easing.type: Easing.OutCubic duration: Kirigami.Units.longDuration } } } Accessible.name: model.display Accessible.description: { if (!model.display) { return ""; } if (model.IsLauncher) { return i18nc("@info:usagetip %1 application name", "Launch %1", model.display) } let smartLauncherDescription = ""; if (iconBox.active) { smartLauncherDescription += i18ncp("@info:tooltip", "There is %1 new message.", "There are %1 new messages.", task.smartLauncherItem.count); } if (model.IsGroupParent) { switch (Plasmoid.configuration.groupedTaskVisualization) { case 0: break; // Use the default description case 1: { return `${i18nc("@info:usagetip %1 task name", "Show Task tooltip for %1", model.display)}; ${smartLauncherDescription}`; } case 2: { if (effectWatcher.registered) { return `${i18nc("@info:usagetip %1 task name", "Show windows side by side for %1", model.display)}; ${smartLauncherDescription}`; } // fallthrough } default: return `${i18nc("@info:usagetip %1 task name", "Open textual list of windows for %1", model.display)}; ${smartLauncherDescription}`; } } return `${i18n("Activate %1", model.display)}; ${smartLauncherDescription}`; } Accessible.role: Accessible.Button Accessible.onPressAction: leftTapHandler.leftClick() onToolTipVisibleChanged: toolTipVisible => { task.toolTipOpen = toolTipVisible; if (!toolTipVisible) { tasksRoot.toolTipOpenedByClick = null; } else { tasksRoot.toolTipAreaItem = task; } } onContainsMouseChanged: { if (containsMouse) { task.forceActiveFocus(Qt.MouseFocusReason); task.updateMainItemBindings(); } else { tasksRoot.toolTipOpenedByClick = null; } } onPidChanged: updateAudioStreams({delay: false}) onAppNameChanged: updateAudioStreams({delay: false}) onIsWindowChanged: { if (model.IsWindow) { taskInitComponent.createObject(task); updateAudioStreams({delay: false}); } } onChildCountChanged: { if (TaskTools.taskManagerInstanceCount < 2 && childCount > previousChildCount) { tasksModel.requestPublishDelegateGeometry(modelIndex(), backend.globalRect(task), task); } previousChildCount = childCount; } onIndexChanged: { hideToolTip(); if (!inPopup && !tasksRoot.vertical && !Plasmoid.configuration.separateLaunchers) { tasksRoot.requestLayout(); } } onSmartLauncherEnabledChanged: { if (smartLauncherEnabled && !smartLauncherItem) { const component = Qt.createComponent("org.kde.plasma.private.taskmanager", "SmartLauncherItem"); const smartLauncher = component.createObject(task); component.destroy(); smartLauncher.launcherUrl = Qt.binding(() => model.LauncherUrlWithoutIcon); smartLauncherItem = smartLauncher; } } onHasAudioStreamChanged: { const audioStreamIconActive = hasAudioStream; if (!audioStreamIconActive) { if (audioStreamIcon !== null) { audioStreamIcon.destroy(); audioStreamIcon = null; } return; } // Create item on demand instead of using Loader to reduce memory consumption, // because only a few applications have audio streams. const component = Qt.createComponent("AudioStream.qml"); audioStreamIcon = component.createObject(task); component.destroy(); } onAudioIndicatorsEnabledChanged: task.hasAudioStreamChanged() Keys.onMenuPressed: event => contextMenuTimer.start() Keys.onReturnPressed: event => TaskTools.activateTask(modelIndex(), model, event.modifiers, task, Plasmoid, tasksRoot, effectWatcher.registered) Keys.onEnterPressed: event => Keys.returnPressed(event); Keys.onSpacePressed: event => Keys.returnPressed(event); Keys.onUpPressed: event => Keys.leftPressed(event) Keys.onDownPressed: event => Keys.rightPressed(event) Keys.onLeftPressed: event => { if (!inPopup && (event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier)) { tasksModel.move(task.index, task.index - 1); } else { event.accepted = false; } } Keys.onRightPressed: event => { if (!inPopup && (event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier)) { tasksModel.move(task.index, task.index + 1); } else { event.accepted = false; } } function modelIndex(): /*QModelIndex*/ var { return inPopup ? tasksModel.makeModelIndex(groupDialog.visualParent.index, index) : tasksModel.makeModelIndex(index); } function showContextMenu(args: var): void { task.hideImmediately(); contextMenu = tasksRoot.createContextMenu(task, modelIndex(), args); contextMenu.show(); } function updateAudioStreams(args: var): void { if (args) { // When the task just appeared (e.g. virtual desktop switch), show the audio indicator // right away. Only when audio streams change during the lifetime of this task, delay // showing that to avoid distraction. delayAudioStreamIndicator = !!args.delay; } var pa = pulseAudio.item; if (!pa || !task.isWindow) { task.audioStreams = []; return; } // Check appid first for app using portal // https://docs.pipewire.org/page_portal.html var streams = pa.streamsForAppId(task.appId); if (!streams.length) { streams = pa.streamsForPid(model.AppPid); if (streams.length) { pa.registerPidMatch(model.AppName); } else { // We only want to fall back to appName matching if we never managed to map // a PID to an audio stream window. Otherwise if you have two instances of // an application, one playing and the other not, it will look up appName // for the non-playing instance and erroneously show an indicator on both. if (!pa.hasPidMatch(model.AppName)) { streams = pa.streamsForAppName(model.AppName); } } } task.audioStreams = streams; } function toggleMuted(): void { if (muted) { task.audioStreams.forEach(item => item.unmute()); } else { task.audioStreams.forEach(item => item.mute()); } } // Will also be called in activateTaskAtIndex(index) function updateMainItemBindings(): void { if ((mainItem.parentTask === this && mainItem.rootIndex.row === index) || (tasksRoot.toolTipOpenedByClick === null && !active) || (tasksRoot.toolTipOpenedByClick !== null && tasksRoot.toolTipOpenedByClick !== this)) { return; } mainItem.blockingUpdates = (mainItem.isGroup !== model.IsGroupParent); // BUG 464597 Force unload the previous component mainItem.parentTask = this; mainItem.rootIndex = tasksModel.makeModelIndex(index, -1); mainItem.appName = Qt.binding(() => model.AppName); mainItem.pidParent = Qt.binding(() => model.AppPid); mainItem.windows = Qt.binding(() => model.WinIdList); mainItem.isGroup = Qt.binding(() => model.IsGroupParent); mainItem.icon = Qt.binding(() => model.decoration); mainItem.launcherUrl = Qt.binding(() => model.LauncherUrlWithoutIcon); mainItem.isLauncher = Qt.binding(() => model.IsLauncher); mainItem.isMinimized = Qt.binding(() => model.IsMinimized); mainItem.display = Qt.binding(() => model.display); mainItem.genericName = Qt.binding(() => model.GenericName); mainItem.virtualDesktops = Qt.binding(() => model.VirtualDesktops); mainItem.isOnAllVirtualDesktops = Qt.binding(() => model.IsOnAllVirtualDesktops); mainItem.activities = Qt.binding(() => model.Activities); mainItem.smartLauncherCountVisible = Qt.binding(() => smartLauncherItem?.countVisible ?? false); mainItem.smartLauncherCount = Qt.binding(() => mainItem.smartLauncherCountVisible ? smartLauncherItem.count : 0); mainItem.blockingUpdates = false; tasksRoot.toolTipAreaItem = this; } Connections { target: pulseAudio.item ignoreUnknownSignals: true // Plasma-PA might not be available function onStreamsChanged(): void { task.updateAudioStreams({delay: true}) } } TapHandler { id: menuTapHandler acceptedButtons: Qt.LeftButton acceptedDevices: PointerDevice.TouchScreen | PointerDevice.Stylus gesturePolicy: TapHandler.ReleaseWithinBounds onLongPressed: { // When we're a launcher, there's no window controls, so we can show all // places without the menu getting super huge. if (model.IsLauncher) { showContextMenu({showAllPlaces: true}) } else { showContextMenu(); } } } TapHandler { acceptedButtons: Qt.RightButton acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus gesturePolicy: TapHandler.WithinBounds // Release grab when menu appears onPressedChanged: if (pressed) contextMenuTimer.start() } Timer { id: contextMenuTimer interval: 0 onTriggered: menuTapHandler.longPressed() } TapHandler { id: leftTapHandler acceptedButtons: Qt.LeftButton onTapped: (eventPoint, button) => leftClick() function leftClick(): void { if (task.active) { hideToolTip(); } TaskTools.activateTask(modelIndex(), model, point.modifiers, task, Plasmoid, tasksRoot, effectWatcher.registered); } } TapHandler { acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton onTapped: (eventPoint, button) => { if (button === Qt.MiddleButton) { if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.NewInstance) { tasksModel.requestNewInstance(modelIndex()); } else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.Close) { tasksRoot.taskClosedWithMouseMiddleButton = model.WinIdList.slice() tasksModel.requestClose(modelIndex()); } else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.ToggleMinimized) { tasksModel.requestToggleMinimized(modelIndex()); } else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.ToggleGrouping) { tasksModel.requestToggleGrouping(modelIndex()); } else if (Plasmoid.configuration.middleClickAction === TaskManagerApplet.Backend.BringToCurrentDesktop) { tasksModel.requestVirtualDesktops(modelIndex(), [virtualDesktopInfo.currentDesktop]); } } else if (button === Qt.BackButton || button === Qt.ForwardButton) { const playerData = mpris2Source.playerForLauncherUrl(model.LauncherUrlWithoutIcon, model.AppPid); if (playerData) { if (button === Qt.BackButton) { playerData.Previous(); } else { playerData.Next(); } } else { eventPoint.accepted = false; } } backend.cancelHighlightWindows(); } } KSvg.FrameSvgItem { id: frame anchors { fill: parent topMargin: (!tasksRoot.vertical && taskList.rows > 1) ? LayoutMetrics.iconMargin : 0 bottomMargin: (!tasksRoot.vertical && taskList.rows > 1) ? LayoutMetrics.iconMargin : 0 leftMargin: ((inPopup || tasksRoot.vertical) && taskList.columns > 1) ? LayoutMetrics.iconMargin : 0 rightMargin: ((inPopup || tasksRoot.vertical) && taskList.columns > 1) ? LayoutMetrics.iconMargin : 0 } imagePath: "widgets/tasks" property bool isHovered: task.highlighted && Plasmoid.configuration.taskHoverEffect property string basePrefix: "normal" prefix: isHovered ? TaskTools.taskPrefixHovered(basePrefix, Plasmoid.location) : TaskTools.taskPrefix(basePrefix, Plasmoid.location) // ALL zoom effect properties moved to frame for complete integration - PERFORMANCE OPTIMIZED property real zoomScale: 1.0 property bool zoomEnabled: tasksRoot.iconsOnly && Plasmoid.configuration.magnifyFactor > 0 property real magnifyFactor: Plasmoid.configuration.magnifyFactor property bool isZoomed: zoomScale > 1.0 property bool hoverDelayActive: false property int zoomDuration: Plasmoid.configuration.zoomDuration property int zoomEasing: Plasmoid.configuration.zoomEasing property int hoverDelay: Plasmoid.configuration.hoverDelay property int resetDelay: Plasmoid.configuration.resetDelay property int zoomAnchor: Plasmoid.configuration.zoomAnchor // Performance: Track animation state efficiently property bool isAnimating: zoomScale !== 1.0 || zoomScaleAnimation.running || zoomScaleYAnimation.running // Ensure zoomed frames appear above others z: isZoomed ? 100 : 0 // Performance: Throttle hover detection using existing highlight system property bool shouldZoom: (task.highlighted || model.IsActive || model.IsDemandingAttention) && zoomEnabled && !model.IsStartup // Performance: Debounce zoom state changes (but keep it very responsive) property bool _zoomPending: false onShouldZoomChanged: { // Much more aggressive response for rapid mouse movements if (_zoomPending) return; _zoomPending = true; zoomDebounceTimer.restart(); } // Highly responsive debounce timer for smooth rapid movements Timer { id: zoomDebounceTimer interval: 4 // Ultra-responsive ~250fps for rapid mouse movements onTriggered: { frame._zoomPending = false; if (frame.shouldZoom) { resetDelayTimer.stop(); // For rapid movements, reduce or eliminate hover delay if (frame.hoverDelay <= 50) { // Very fast or immediate zoom for low hover delays hoverDelayTimer.interval = Math.max(1, frame.hoverDelay); } else { hoverDelayTimer.interval = frame.hoverDelay; } hoverDelayTimer.restart(); // IMMEDIATE ZOOM: For ultra-low hover delays, apply zoom immediately if (frame.hoverDelay <= 10) { frame.hoverDelayActive = true; let zoomIntensity; if (model.IsDemandingAttention || (task.smartLauncherItem && task.smartLauncherItem.urgent)) { zoomIntensity = frame.magnifyFactor * 0.6; } else if (model.IsActive) { zoomIntensity = frame.magnifyFactor * 0.4; } else { zoomIntensity = frame.magnifyFactor * 0.5; } const baseZoom = 1.0 + zoomIntensity; frame.zoomScale = Math.max(1.0, baseZoom); } } else { hoverDelayTimer.stop(); // Faster reset for rapid movements resetDelayTimer.interval = Math.max(10, frame.resetDelay); resetDelayTimer.restart(); // IMMEDIATE RESET: For ultra-low reset delays, reset immediately if (frame.resetDelay <= 10) { frame.hoverDelayActive = false; frame.zoomScale = 1.0; } } } } // Hover delay timer for smoother experience - HIGHLY OPTIMIZED for rapid movements Timer { id: hoverDelayTimer interval: frame.hoverDelay // Dynamic interval set by debounce timer onTriggered: { if (frame.shouldZoom) { frame.hoverDelayActive = true; // Performance: Pre-calculate zoom intensity only when needed let zoomIntensity; if (model.IsDemandingAttention || (task.smartLauncherItem && task.smartLauncherItem.urgent)) { zoomIntensity = frame.magnifyFactor * 0.6; } else if (model.IsActive) { zoomIntensity = frame.magnifyFactor * 0.4; } else { zoomIntensity = frame.magnifyFactor * 0.5; } const baseZoom = 1.0 + zoomIntensity; frame.zoomScale = Math.max(1.0, baseZoom); } } } // Reset timer for smooth zoom out - OPTIMIZED for rapid movements Timer { id: resetDelayTimer interval: frame.resetDelay // Dynamic interval set by debounce timer onTriggered: { if (!frame.shouldZoom) { frame.hoverDelayActive = false; frame.zoomScale = 1.0; } } } // Performance: Static easing type lookup function getEasingType(easingIndex) { const easingTypes = [ Easing.Linear, Easing.OutQuad, Easing.OutCubic, Easing.OutQuart, Easing.OutBack, Easing.OutElastic, Easing.OutBounce ]; return easingTypes[easingIndex] || Easing.OutCubic; } // Avoid repositioning delegate item after dragFinished DragHandler { id: dragHandler grabPermissions: PointerHandler.CanTakeOverFromHandlersOfDifferentType function setRequestedInhibitDnd(value: bool): void { // This is modifying the value in the panel containment that // inhibits accepting drag and drop, so that we don't accidentally // drop the task on this panel. let item = this; while (item.parent) { item = item.parent; if (item.appletRequestsInhibitDnD !== undefined) { item.appletRequestsInhibitDnD = value } } } onActiveChanged: { if (active) { icon.grabToImage(result => { if (!dragHandler.active) { // BUG 466675 grabToImage is async, so avoid updating dragSource when active is false return; } setRequestedInhibitDnd(true); tasksRoot.dragSource = task; dragHelper.Drag.imageSource = result.url; dragHelper.Drag.mimeData = { "text/x-orgkdeplasmataskmanager_taskurl": backend.tryDecodeApplicationsUrl(model.LauncherUrlWithoutIcon).toString(), [model.MimeType]: model.MimeData, "application/x-orgkdeplasmataskmanager_taskbuttonitem": model.MimeData, }; dragHelper.Drag.active = dragHandler.active; }); } else { setRequestedInhibitDnd(false); dragHelper.Drag.active = false; dragHelper.Drag.imageSource = ""; } } } // Move iconBox and label to be children of frame so they scale with zoom Loader { id: iconBox anchors { left: parent.left leftMargin: adjustMargin(true, parent.width, taskFrame.margins.left) top: parent.top topMargin: adjustMargin(false, parent.height, taskFrame.margins.top) } width: task.inPopup ? Math.max(Kirigami.Units.iconSizes.sizeForLabels, Kirigami.Units.iconSizes.medium) : Math.min(task.parent?.minimumWidth ?? 0, task.height) height: task.inPopup ? width : (parent.height - adjustMargin(false, parent.height, taskFrame.margins.top) - adjustMargin(false, parent.height, taskFrame.margins.bottom)) asynchronous: true active: height >= Kirigami.Units.iconSizes.small && task.smartLauncherItem && task.smartLauncherItem.countVisible source: "TaskBadgeOverlay.qml" function adjustMargin(isVertical: bool, size: real, margin: real): real { if (!size) { return margin; } var margins = isVertical ? LayoutMetrics.horizontalMargins() : LayoutMetrics.verticalMargins(); if ((size - margins) < Kirigami.Units.iconSizes.small) { return Math.ceil((margin * (Kirigami.Units.iconSizes.small / size)) / 2); } return margin; } Kirigami.Icon { id: icon anchors.fill: parent active: task.highlighted enabled: true source: model.decoration // Use Transform objects for all animations (this is what works for bouncing) transform: [ Translate { y: model.IsStartup && launchAnimationLoader.item ? launchAnimationLoader.item.iconOffsetY : 0 }, Scale { xScale: model.IsStartup && launchAnimationLoader.item ? launchAnimationLoader.item.iconScale : 1.0 yScale: model.IsStartup && launchAnimationLoader.item ? launchAnimationLoader.item.iconScale : 1.0 origin.x: width / 2 origin.y: height / 2 }, Rotation { angle: model.IsStartup && launchAnimationLoader.item ? launchAnimationLoader.item.iconRotation : 0 origin.x: width / 2 origin.y: height / 2 } ] // Opacity still needs to be a direct property opacity: model.IsStartup && launchAnimationLoader.item ? launchAnimationLoader.item.iconOpacity : 1.0 } states: [ // Using a state transition avoids a binding loop between label.visible and // the text label margin, which derives from the icon width. State { name: "standalone" when: !label.visible && task.parent AnchorChanges { target: iconBox anchors.left: undefined anchors.horizontalCenter: parent.horizontalCenter } PropertyChanges { target: iconBox anchors.leftMargin: 0 width: Math.min(task.parent.minimumWidth, tasks.height) - adjustMargin(true, task.width, taskFrame.margins.left) - adjustMargin(true, task.width, taskFrame.margins.right) } } ] Loader { id: launchAnimationLoader anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width active: model.IsStartup sourceComponent: launchAnimationComponent onLoaded: { if (item) { item.active = Qt.binding(() => model.IsStartup); item.iconSource = Qt.binding(() => model.decoration); item.animationType = Qt.binding(() => plasmoid.configuration.launchAnimationType); item.animationDuration = Qt.binding(() => plasmoid.configuration.launchAnimationDuration); item.animationIntensity = Qt.binding(() => plasmoid.configuration.launchAnimationIntensity); } } } // Busy indicator overlay - shows on top of icon when animationType is 0 PlasmaComponents3.BusyIndicator { anchors.centerIn: parent width: Math.min(parent.width, parent.height) * 0.8 height: width visible: model.IsStartup && launchAnimationLoader.item && launchAnimationLoader.item.animationType === 0 running: visible z: 20 } } PlasmaComponents3.Label { id: label visible: (inPopup || !iconsOnly && !model.IsLauncher && (parent.width - iconBox.height - Kirigami.Units.smallSpacing) >= LayoutMetrics.spaceRequiredToShowText()) anchors { fill: parent leftMargin: taskFrame.margins.left + iconBox.width + LayoutMetrics.labelMargin topMargin: taskFrame.margins.top rightMargin: taskFrame.margins.right + (audioStreamIcon !== null && audioStreamIcon.visible ? (audioStreamIcon.width + LayoutMetrics.labelMargin) : 0) bottomMargin: taskFrame.margins.bottom } wrapMode: (maximumLineCount === 1) ? Text.NoWrap : Text.Wrap elide: Text.ElideRight textFormat: Text.PlainText verticalAlignment: Text.AlignVCenter maximumLineCount: Plasmoid.configuration.maxTextLines || undefined Accessible.ignored: true // use State to avoid unnecessary re-evaluation when the label is invisible states: State { name: "labelVisible" when: label.visible PropertyChanges { target: label text: model.display } } } } Loader { id: taskProgressOverlayLoader anchors.fill: frame asynchronous: true active: model.IsWindow && task.smartLauncherItem && task.smartLauncherItem.progressVisible source: "TaskProgressOverlay.qml" } states: [ State { name: "launcher" when: model.IsLauncher PropertyChanges { target: frame basePrefix: "" } }, State { name: "attention" when: model.IsDemandingAttention || (task.smartLauncherItem && task.smartLauncherItem.urgent) PropertyChanges { target: frame basePrefix: "attention" } }, State { name: "minimized" when: model.IsMinimized PropertyChanges { target: frame basePrefix: "minimized" } }, State { name: "active" when: model.IsActive PropertyChanges { target: frame basePrefix: "focus" } } ] Component.onCompleted: { if (!inPopup && model.IsWindow) { const component = Qt.createComponent("GroupExpanderOverlay.qml"); component.createObject(task); component.destroy(); updateAudioStreams({delay: false}); } if (!inPopup && !model.IsWindow) { taskInitComponent.createObject(task); } completed = true; } Component.onDestruction: { if (moveAnim.running) { (task.parent as TaskList).animationsRunning -= 1; } } onHighlightedChanged: { // ensure it doesn't get stuck with a window highlighted backend.cancelHighlightWindows(); } // Performance: Invalidate caches when relevant properties change Connections { target: tasksRoot function onHeightChanged() { } function onWidthChanged() { } } Connections { target: Plasmoid.configuration function onMaxStripesChanged() { } function onZoomAnchorChanged() { } // Add invalidation for filter changes that affect task visibility function onShowOnlyCurrentDesktopChanged() { } function onShowOnlyCurrentActivityChanged() { } function onShowOnlyCurrentScreenChanged() { } function onShowOnlyMinimizedChanged() { } } // Enhanced: Monitor task model changes that affect individual task layout Connections { target: tasksModel function onCountChanged() { } } // Movement transform - always present Translate { id: translateTransform } // Zoom transform - only applied when actively zooming - REMOVED CACHING TO FIX BINDING LOOPS Scale { id: zoomTransform // Simple, direct origin calculation without caching to avoid binding loops origin.x: { const effectiveWidth = frame.width; const effectiveHeight = frame.height; switch(frame.zoomAnchor) { case 0: return effectiveWidth / 2; // Center case 1: return effectiveWidth / 2; // Bottom case 2: return effectiveWidth / 2; // Top case 3: return 0; // Left case 4: return effectiveWidth; // Right case 5: return 0; // BottomLeft case 6: return effectiveWidth; // BottomRight case 7: return 0; // TopLeft case 8: return effectiveWidth; // TopRight default: return effectiveWidth / 2; // Default to Bottom } } origin.y: { const effectiveWidth = frame.width; const effectiveHeight = frame.height; switch(frame.zoomAnchor) { case 0: return effectiveHeight / 2; // Center case 1: return effectiveHeight; // Bottom case 2: return 0; // Top case 3: return effectiveHeight / 2; // Left case 4: return effectiveHeight / 2; // Right case 5: return effectiveHeight; // BottomLeft case 6: return effectiveHeight; // BottomRight case 7: return 0; // TopLeft case 8: return 0; // TopRight default: return effectiveHeight; // Default to Bottom } } xScale: frame.zoomScale yScale: frame.zoomScale // Performance: Optimize animation behavior Behavior on xScale { enabled: frame.zoomEnabled NumberAnimation { id: zoomScaleAnimation duration: frame.zoomDuration easing.type: frame.getEasingType(frame.zoomEasing) easing.overshoot: frame.zoomEasing === 4 ? 1.2 : 1.0 // Performance: Prevent unnecessary intermediate updates easing.period: frame.zoomEasing === 5 ? 0.3 : 1.0 } } Behavior on yScale { enabled: frame.zoomEnabled NumberAnimation { id: zoomScaleYAnimation duration: frame.zoomDuration easing.type: frame.getEasingType(frame.zoomEasing) easing.overshoot: frame.zoomEasing === 4 ? 1.2 : 1.0 // Performance: Prevent unnecessary intermediate updates easing.period: frame.zoomEasing === 5 ? 0.3 : 1.0 } } } }