205 lines
6.6 KiB
QML
205 lines
6.6 KiB
QML
import QtQuick
|
|
import qs.Common
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: win
|
|
|
|
property var panel: null
|
|
property string widgetId: ""
|
|
property string title: ""
|
|
property string icon: ""
|
|
property real widgetWidth: 360
|
|
property real widgetHeight: 320
|
|
property real minimumY: 0
|
|
property bool shown: false
|
|
property Component contentComponent: null
|
|
|
|
readonly property real margin: panel ? panel.edgeMargin : 12
|
|
readonly property real panelWidth: panel ? panel.width : 0
|
|
readonly property real panelHeight: panel ? panel.height : 0
|
|
readonly property string screenKey: panel ? panel.screenKey : ""
|
|
// Which monitor the current x/y were resolved for; guards against saving a
|
|
// position under a screen it was never arranged on.
|
|
property string appliedKey: ""
|
|
readonly property real maxX: Math.max(margin, (panel ? panel.width : width) - width - margin)
|
|
readonly property real maxY: Math.max(minimumY, (panel ? panel.height : height) - height - margin)
|
|
|
|
function clampX(value) {
|
|
return Math.max(margin, Math.min(maxX, value));
|
|
}
|
|
|
|
function clampY(value) {
|
|
return Math.max(minimumY, Math.min(maxY, value));
|
|
}
|
|
|
|
// Each monitor keeps its own absolute arrangement, so moving between a 4K
|
|
// and a 1440p screen restores what was arranged there rather than a scaled
|
|
// copy of the other one.
|
|
function applyStoredPosition() {
|
|
if (!panel || panel.width <= 0 || panel.height <= 0)
|
|
return;
|
|
|
|
const stored = panel.widgetPosition(win.widgetId);
|
|
win.x = win.clampX(stored.x);
|
|
win.y = win.clampY(stored.y);
|
|
win.appliedKey = win.screenKey;
|
|
}
|
|
|
|
function raise() {
|
|
if (panel)
|
|
win.z = panel.nextWindowZ();
|
|
}
|
|
|
|
function storePosition() {
|
|
if (!panel || panel.width <= 0 || panel.height <= 0)
|
|
return;
|
|
|
|
// Mid-transition to another monitor: the position on screen does not
|
|
// belong to the new screen yet.
|
|
if (win.screenKey === "" || win.screenKey !== win.appliedKey)
|
|
return;
|
|
|
|
panel.saveWidgetPosition(win.widgetId, win.x, win.y);
|
|
}
|
|
|
|
width: widgetWidth
|
|
height: widgetHeight
|
|
visible: shown && opacity > 0
|
|
opacity: shown ? 1 : 0
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: win.applyStoredPosition()
|
|
onShownChanged: {
|
|
if (shown) {
|
|
win.applyStoredPosition();
|
|
win.raise();
|
|
}
|
|
}
|
|
// The panel has neither size nor a resolved screen while the delegate is
|
|
// created, and the two settle independently - the screen can change without
|
|
// a resize (same resolution) and vice versa, so react to both.
|
|
onPanelWidthChanged: win.applyStoredPosition()
|
|
onPanelHeightChanged: win.applyStoredPosition()
|
|
onScreenKeyChanged: win.applyStoredPosition()
|
|
|
|
Rectangle {
|
|
id: frame
|
|
|
|
anchors.fill: parent
|
|
radius: Theme.cornerRadius * 1.25
|
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
border.color: dragArea.pressed ? Theme.primary : Theme.outlineMedium
|
|
border.width: dragArea.pressed ? 2 : Theme.layerOutlineWidth
|
|
|
|
Rectangle {
|
|
id: titleBar
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 38
|
|
radius: frame.radius
|
|
color: dragArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
|
|
|
|
// Square off the bottom corners so the title bar blends into the body.
|
|
Rectangle {
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: parent.radius
|
|
color: parent.color
|
|
}
|
|
|
|
DankIcon {
|
|
id: titleIcon
|
|
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
name: win.icon
|
|
size: Theme.iconSize - 6
|
|
color: Theme.primary
|
|
}
|
|
|
|
StyledText {
|
|
anchors.left: titleIcon.right
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.right: closeButton.left
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: win.title
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
DankActionButton {
|
|
id: closeButton
|
|
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
iconName: "close"
|
|
buttonSize: 26
|
|
iconSize: 15
|
|
iconColor: Theme.surfaceVariantText
|
|
onClicked: {
|
|
if (win.panel)
|
|
win.panel.toggleWidget(win.widgetId);
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: dragArea
|
|
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
anchors.right: closeButton.left
|
|
hoverEnabled: true
|
|
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
|
drag.target: win
|
|
drag.axis: Drag.XAndYAxis
|
|
drag.minimumX: win.margin
|
|
drag.maximumX: win.maxX
|
|
drag.minimumY: win.minimumY
|
|
drag.maximumY: win.maxY
|
|
drag.threshold: 0
|
|
onPressed: win.raise()
|
|
onReleased: win.storePosition()
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.AllButtons
|
|
propagateComposedEvents: true
|
|
z: -1
|
|
onPressed: mouse => {
|
|
win.raise();
|
|
mouse.accepted = false;
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
anchors.top: titleBar.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.rightMargin: Theme.spacingM
|
|
anchors.bottomMargin: Theme.spacingM
|
|
active: win.shown
|
|
sourceComponent: win.contentComponent
|
|
}
|
|
}
|
|
}
|