aerothemeplasma/plasma/plasmoids/io.gitgud.wackyideas.seventasks/contents/ui/WindowThumbnail.qml
2025-07-04 04:08:55 +02:00

494 lines
16 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
import org.kde.ksvg as KSvg
import org.kde.pipewire as PipeWire
import org.kde.taskmanager as TaskManager
import org.kde.kwindowsystem
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.plasmoid
import org.kde.plasma.private.mpris as Mpris
MouseArea {
id: thumbnailRoot
property QtObject root
property bool isGroupDelegate: false
readonly property var captionAlignment: {
if(Plasmoid.configuration.thmbnlCaptionAlignment == 0) return Text.AlignLeft
if(Plasmoid.configuration.thmbnlCaptionAlignment == 1) return Text.AlignHCenter
if(Plasmoid.configuration.thmbnlCaptionAlignment == 2) return Text.AlignRight
}
readonly property var display: isGroupDelegate ? model.display : root.display
readonly property var icon: isGroupDelegate ? model.decoration : root.icon
readonly property var active: isGroupDelegate ? model.IsActive : root.active
readonly property var modelIndex: isGroupDelegate ? (tasksModel.makeModelIndex(root.taskIndex, index)) : root.modelIndex
readonly property var windows: isGroupDelegate ? model.WinIdList : root.windows
readonly property var minimized: isGroupDelegate ? model.IsMinimized : root.minimized
readonly property var demandsAttention: isGroupDelegate ? model.IsDemandingAttention : root.demandsAttention
property int maxPreviewWidth: 164
property int maxPreviewHeight: 94
property real baselineAspectRatio: maxPreviewWidth / maxPreviewHeight
property real thumbnailHeight: maxPreviewHeight
readonly property int margins: Kirigami.Units.smallSpacing*8
implicitWidth: maxPreviewWidth + margins
implicitHeight: thumbnailHeight + margins +
(tasks.iconsOnly ? header.height : 0) +
(mprisControls.active ? (mprisControls.height - (Kirigami.Units.smallSpacing*2)) : 0)
onImplicitHeightChanged: if(isGroupDelegate) {
ListView.view.updateMaxSize()
}
width: implicitWidth
height: {
if(isGroupDelegate && ListView.view.maxThumbnailItem !== thumbnailRoot)
return ListView.view.maxThumbnailHeight;
else
return implicitHeight;
}
hoverEnabled: true
propagateComposedEvents: true
Item {
id: frames
anchors.fill: content
anchors.margins: -Kirigami.Units.smallSpacing*2
KSvg.FrameSvgItem {
id: attentionTexture
anchors.fill: parent
imagePath: Qt.resolvedUrl("svgs/menuitem.svg")
prefix: "attention"
visible: demandsAttention
opacity: root.parentTask.attentionAnimOpacity
}
KSvg.FrameSvgItem {
id: activeTexture
anchors.fill: parent
imagePath: Qt.resolvedUrl("svgs/menuitem.svg")
prefix: "active"
visible: active
}
KSvg.FrameSvgItem {
id: hoverTexture
anchors.fill: parent
imagePath: Qt.resolvedUrl("svgs/menuitem.svg")
prefix: {
if(contentMa.containsPress) return "pressed";
else return "hover";
}
opacity: thumbnailCloseMa.containsMouse || contentMa.containsMouse || closeMa.containsMouse || (!tasks.iconsOnly && root.taskHovered && !isGroupDelegate)
Behavior on opacity {
NumberAnimation { duration: 250 }
}
}
}
function closeTask() {
tasksModel.requestClose(modelIndex);
if(!isGroupDelegate) root.parentTask.hideImmediately();
}
DropArea {
signal urlsDropped(var urls)
anchors.fill: parent
onPositionChanged: activationTimer.restart();
onEntered: root.containsDrag = true;
onExited: {
activationTimer.stop();
root.containsDrag = false;
}
onDropped: event => {
if (event.hasUrls) {
urlsDropped(event.urls);
return;
}
}
onUrlsDropped: (urls) => {
tasksModel.requestOpenUrls(modelIndex, urls);
root.containsDrag = false;
}
Timer {
id: activationTimer
interval: 250
repeat: false
onTriggered: tasksModel.requestActivate(modelIndex);
}
visible: isGroupDelegate
}
MouseArea {
id: contentMa
anchors.fill: content
anchors.margins: -Kirigami.Units.smallSpacing*2
hoverEnabled: true
propagateComposedEvents: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onContainsMouseChanged: {
if(containsMouse) windowPeek.start();
else {
windowPeek.stop();
root.isPeeking = false;
tasks.windowsHovered(thumbnailRoot.windows, false)
}
}
onClicked: (mouse) => {
if(mouse.button == Qt.LeftButton) {
tasksModel.requestActivate(modelIndex);
tasks.windowsHovered(thumbnailRoot.windows, false)
root.parentTask.hideImmediately();
}
if(mouse.button == Qt.MiddleButton) {
thumbnailRoot.closeTask();
}
}
}
Timer {
id: windowPeek
interval: root.isPeeking ? 1 : 800
repeat: false
onTriggered: {
if(!minimized) {
tasks.windowsHovered(thumbnailRoot.windows, true);
root.isPeeking = true;
}
}
}
ColumnLayout {
id: content
anchors.centerIn: parent
anchors.verticalCenterOffset: mprisControls.active ? -(mprisControls.height / 4) - 2 : 0
width: parent.width - margins
height: parent.height - margins - (mprisControls.active ? (mprisControls.height - (Kirigami.Units.smallSpacing*2)) : 0)
spacing: Kirigami.Units.smallSpacing/2
RowLayout {
id: header
Layout.fillWidth: true
Layout.minimumHeight: 16
Layout.maximumHeight: 16
spacing: Kirigami.Units.smallSpacing
visible: tasks.iconsOnly
Kirigami.Icon {
Layout.preferredHeight: 16
Layout.preferredWidth: 16
source: icon
}
Text {
id: txt
Layout.fillWidth: true
Layout.fillHeight: true
verticalAlignment: Text.AlignVCenter
text: display
color: "white"
elide: Text.ElideRight
wrapMode: Text.NoWrap
style: Text.Outline
styleColor: "#02ffffff"
horizontalAlignment: captionAlignment
rightPadding: captionAlignment == Text.AlignHCenter ? (close.visible ? 0 : close.width + header.spacing) : 0
}
KSvg.FrameSvgItem {
id: close
Layout.preferredWidth: 14
Layout.preferredHeight: 14
imagePath: Qt.resolvedUrl("svgs/button-close.svg")
prefix: closeMa.containsMouse ? (closeMa.containsPress ? "pressed" : "hover") : "normal"
visible: opacity
opacity: contentMa.containsMouse || closeMa.containsMouse
Behavior on opacity {
NumberAnimation { duration: 250 }
}
MouseArea {
id: closeMa
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onClicked: {
thumbnailRoot.closeTask();
}
}
}
}
Item {
id: thumbnail
Layout.minimumWidth: thumbnailRoot.width - margins
Layout.minimumHeight: thumbnailHeight
Layout.fillHeight: true
Loader {
id: thumbnailLoader
anchors.centerIn: parent
width: parent.Layout.minimumWidth
height: 94
active: true
asynchronous: true
sourceComponent: minimized ? appIcon : (KWindowSystem.isPlatformWayland ? (tasks.toolTipOpen ? waylandThumbnail : undefined) : x11Thumbnail)
onLoaded: {
if(sourceComponent !== x11Thumbnail) thumbnailRoot.thumbnailHeight = thumbnailLoader.height;
if(isGroupDelegate && ListView.view !== null) ListView.view.updateMaxSize()
}
Component {
id: x11Thumbnail
PlasmaCore.WindowThumbnail {
winId: windows !== undefined ? windows[0] : undefined
onPaintedSizeChanged: thumbnailRoot.thumbnailHeight = paintedHeight;
Rectangle {
anchors.centerIn: parent
width: parent.paintedWidth+2
height: parent.paintedHeight+2
color: "transparent"
border.width: 1
border.color: "black"
opacity: 0.5
}
}
}
Component {
id: waylandThumbnail
PipeWire.PipeWireSourceItem {
id: wl_pw_src
nodeId: waylandItem.nodeId
TaskManager.ScreencastingRequest {
id: waylandItem
uuid: windows[0]
}
// Calculates aspect ratio of the PipeWire stream size, which is effectively the window's dimensions
property real aspectRatio: (wl_pw_src.streamSize.height == 0) ? 0.0 : wl_pw_src.streamSize.width / wl_pw_src.streamSize.height
// If the stream's width is larger than the height, and also the aspectRatio is greater or equal to the
// aspect ratio of the maximum thumbnail's dimensions, then it follows that the thumbnail preview's width
// will be at the maximum, therefore it's a fixed known value.
// In this case, the height is calculated through simple proportions
// Otherwise the calculations can be derived in a similar manner
property bool widthTakesPrecedence: (wl_pw_src.streamSize.width > wl_pw_src.streamSize.height) &&
(aspectRatio >= thumbnailRoot.baselineAspectRatio)
onStreamSizeChanged: {
outlineRect.updateSize();
}
onReadyChanged: {
if(ready) outlineRect.updateSize();
}
Rectangle {
id: outlineRect
anchors.centerIn: parent
function updateSize() {
if(wl_pw_src.aspectRatio === 0.0 || !wl_pw_src.ready) {
width = 0;
height = 0;
} else if(wl_pw_src.widthTakesPrecedence) {
width = Math.floor(thumbnailRoot.maxPreviewWidth + 2);
height = Math.floor((thumbnailRoot.maxPreviewWidth / wl_pw_src.aspectRatio) + 2);
} else {
width = Math.floor((wl_pw_src.aspectRatio * thumbnailRoot.maxPreviewHeight) + 2);
height = Math.floor(thumbnailRoot.maxPreviewHeight + 2);
}
}
onHeightChanged: {
thumbnailRoot.thumbnailHeight = ((outlineRect.height-2) > 0) ? outlineRect.height-2 : thumbnailRoot.maxPreviewHeight
}
color: "black"
border.width: 1
border.color: "black"
opacity: 0.5
z: -1
}
}
}
// Used when there's no thumbnail available.
Component {
id: appIcon
Item {
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0; color: "#ffffff" }
GradientStop { position: 1; color: "#cccccc" }
}
Rectangle {
anchors.fill: parent
anchors.margins: -1
color: "transparent"
border.width: 1
border.color: "black"
radius: 1
}
}
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
source: icon
}
}
}
Connections { // Reload the component when thumbnailRoot's windows property changes. This fixes a bug in which the thumbnail shows the wrong window.
target: thumbnailRoot
function onWindowsChanged() {
thumbnailLoader.active = false;
thumbnailLoader.active = true;
}
}
}
Loader {
id: shadowLoader
anchors.fill: thumbnailLoader
active: true
asynchronous: true
sourceComponent: DropShadow {
id: realShadow
horizontalOffset: 1
// Fix for shadow not appearing properly at the bottom
// when appIcon is the sourceComponent.
verticalOffset: thumbnailLoader.sourceComponent == appIcon ? 2 : 1
radius: 1
samples: 1
color: "#70000000"
source: thumbnailLoader.item
}
}
KSvg.FrameSvgItem { // Shown when labels are turned on
id: thumbnailClose
anchors {
top: parent.top
right: parent.right
}
width: 14
height: width
imagePath: Qt.resolvedUrl("svgs/button-close.svg")
prefix: thumbnailCloseMa.containsMouse ? (thumbnailCloseMa.containsPress ? "pressed" : "hover") : "normal"
visible: opacity
opacity: (contentMa.containsMouse || thumbnailCloseMa.containsMouse) && !tasks.iconsOnly
Behavior on opacity {
NumberAnimation { duration: 250 }
}
MouseArea {
id: thumbnailCloseMa
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onClicked: {
thumbnailRoot.closeTask();
}
}
}
}
}
Loader {
id: mprisControls
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
readonly property QtObject root: thumbnailRoot.root
active: root.playerData !== null
asynchronous: true
source: "PlayerController.qml"
}
Component.onDestruction: if(isGroupDelegate) ListView.view.updateMaxSize()
}