aerothemeplasma/plasma/plasmoids/io.gitgud.wackyideas.seventasks/contents/ui/WindowThumbnail.qml
2026-10-12 22:38:54 -03:00

472 lines
15 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.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 : (tasks.toolTipOpen ? waylandThumbnail : undefined)
onLoaded: {
thumbnailRoot.thumbnailHeight = thumbnailLoader.height;
if (isGroupDelegate && ListView.view !== null) {
ListView.view.updateMaxSize()
}
}
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()
}