overlay: make widgets resizable

This commit is contained in:
end-4
2025-11-08 23:50:21 +01:00
parent 241f33fb2f
commit 788c01c242
9 changed files with 134 additions and 32 deletions
@@ -84,20 +84,26 @@ Singleton {
property JsonObject crosshair: JsonObject { property JsonObject crosshair: JsonObject {
property bool pinned: false property bool pinned: false
property bool clickthrough: true property bool clickthrough: true
property real x: 835 property real x: 827
property real y: 483 property real y: 441
property real width: 250
property real height: 100
} }
property JsonObject recorder: JsonObject { property JsonObject recorder: JsonObject {
property bool pinned: false property bool pinned: false
property bool clickthrough: false property bool clickthrough: false
property real x: 80 property real x: 80
property real y: 80 property real y: 80
property real width: 350
property real height: 130
} }
property JsonObject resources: JsonObject { property JsonObject resources: JsonObject {
property bool pinned: false property bool pinned: false
property bool clickthrough: true property bool clickthrough: true
property real x: 1500 property real x: 1500
property real y: 770 property real y: 770
property real width: 350
property real height: 200
property int tabIndex: 0 property int tabIndex: 0
} }
property JsonObject volumeMixer: JsonObject { property JsonObject volumeMixer: JsonObject {
@@ -105,6 +111,8 @@ Singleton {
property bool clickthrough: false property bool clickthrough: false
property real x: 80 property real x: 80
property real y: 280 property real y: 280
property real width: 350
property real height: 600
property int tabIndex: 0 property int tabIndex: 0
} }
property JsonObject fpsLimiter: JsonObject { property JsonObject fpsLimiter: JsonObject {
@@ -112,6 +120,8 @@ Singleton {
property bool clickthrough: false property bool clickthrough: false
property real x: 1576 property real x: 1576
property real y: 630 property real y: 630
property real width: 280
property real height: 80
} }
} }
@@ -8,6 +8,8 @@ import qs.modules.common
MouseArea { MouseArea {
id: root id: root
property alias animateXPos: xBehavior.enabled
property alias animateYPos: yBehavior.enabled
property bool draggable: true property bool draggable: true
drag.target: draggable ? root : undefined drag.target: draggable ? root : undefined
cursorShape: (draggable && containsPress) ? Qt.ClosedHandCursor : draggable ? Qt.OpenHandCursor : Qt.ArrowCursor cursorShape: (draggable && containsPress) ? Qt.ClosedHandCursor : draggable ? Qt.OpenHandCursor : Qt.ArrowCursor
@@ -18,9 +20,11 @@ MouseArea {
} }
Behavior on x { Behavior on x {
id: xBehavior
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
} }
Behavior on y { Behavior on y {
id: yBehavior
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
} }
} }
@@ -13,29 +13,67 @@ import qs.modules.common.widgets.widgetCanvas
* To make an overlay widget: * To make an overlay widget:
* 1. Create a modules/overlay/<yourWidget>/<YourWidget>.qml, using this as the base class and declare your widget content as contentItem * 1. Create a modules/overlay/<yourWidget>/<YourWidget>.qml, using this as the base class and declare your widget content as contentItem
* 2. Add an entry to OverlayContext.availableWidgets with identifier=<yourWidgetIdentifier> * 2. Add an entry to OverlayContext.availableWidgets with identifier=<yourWidgetIdentifier>
* 3. Add an entry in Persistent.states.overlay.<yourWidgetIdentifier> with x, y, pinned, clickthrough properties set to reasonable defaults * 3. Add an entry in Persistent.states.overlay.<yourWidgetIdentifier> with x, y, width, height, pinned, clickthrough properties set to reasonable defaults
* 4. Add an entry in OverlayWidgetDelegateChooser with roleValue=<yourWidgetIdentifier> and Declare your widget in there * 4. Add an entry in OverlayWidgetDelegateChooser with roleValue=<yourWidgetIdentifier> and Declare your widget in there
* Use existing entries as reference. * Use existing entries as reference.
*/ */
AbstractOverlayWidget { AbstractOverlayWidget {
id: root id: root
// To be defined by subclasses
required property Item contentItem required property Item contentItem
property bool fancyBorders: true property bool fancyBorders: true
property bool showCenterButton: false property bool showCenterButton: false
property bool showClickabilityButton: true property bool showClickabilityButton: true
// Defaults n stuff
required property var modelData required property var modelData
readonly property string identifier: modelData.identifier readonly property string identifier: modelData.identifier
readonly property string materialSymbol: modelData.materialSymbol ?? "widgets" readonly property string materialSymbol: modelData.materialSymbol ?? "widgets"
property string title: identifier.replace(/([A-Z])/g, " $1").replace(/^./, function(str){ return str.toUpperCase(); }) property string title: identifier.replace(/([A-Z])/g, " $1").replace(/^./, function(str){ return str.toUpperCase(); })
property var persistentStateEntry: Persistent.states.overlay[identifier] property var persistentStateEntry: Persistent.states.overlay[identifier]
property real radius: Appearance.rounding.windowRounding property real radius: Appearance.rounding.windowRounding
property real minWidth: 250 property real minimumWidth: 250
property real minimumHeight: 100
property real resizeMargin: 8
property real padding: 6 property real padding: 6
property real contentRadius: radius - padding property real contentRadius: radius - padding
// Resizing
function getXResizeDirection(x) {
return (x < root.resizeMargin) ? -1 : (x > root.width - root.resizeMargin) ? 1 : 0
}
function getYResizeDirection(y) {
return (y < root.resizeMargin) ? -1 : (y > root.height - root.resizeMargin) ? 1 : 0
}
hoverEnabled: true
property bool resizable: true
property bool resizing: false
property int resizeXDirection: getXResizeDirection(mouseX)
property int resizeYDirection: getYResizeDirection(mouseY)
draggable: GlobalStates.overlayOpen draggable: GlobalStates.overlayOpen
drag.target: undefined
animateXPos: !dragHandler.active
animateYPos: !dragHandler.active
z: dragHandler.active ? 2 : 1
cursorShape: {
if (dragHandler.active) return root.resizing ? cursorShape : Qt.ArrowCursor;
if (resizeMargin < mouseX && mouseX < width - resizeMargin &&
resizeMargin < mouseY && mouseY < height - resizeMargin) {
return Qt.ArrowCursor;
} else {
if (!root.resizable) return Qt.ArrowCursor;
const dragIsLeft = mouseX < width / 2
const dragIsTop = mouseY < height / 2
if ((dragIsLeft && dragIsTop) || (!dragIsLeft && !dragIsTop)) {
return Qt.SizeFDiagCursor
} else {
return Qt.SizeBDiagCursor
}
}
}
// Positioning & sizing
x: Math.round(persistentStateEntry.x) // Round or it'll be blurry x: Math.round(persistentStateEntry.x) // Round or it'll be blurry
y: Math.round(persistentStateEntry.y) // Round or it'll be blurry y: Math.round(persistentStateEntry.y) // Round or it'll be blurry
pinned: persistentStateEntry.pinned pinned: persistentStateEntry.pinned
@@ -68,7 +106,52 @@ AbstractOverlayWidget {
} }
// Hooks // Hooks
onReleased: savePosition(); onPressed: (event) => {
// We're only interested in handling resize here
// Early returns
if (!root.resizable) return;
if (root.resizeMargin < event.x && event.x < root.width - root.resizeMargin &&
root.resizeMargin < event.y && event.y < root.height - root.resizeMargin) {
return;
}
// Resizing setup
root.resizing = true;
root.resizeXDirection = getXResizeDirection(event.x);
root.resizeYDirection = getYResizeDirection(event.y);
if (root.resizeYDirection !== 0 && root.resizeXDirection === 0) {
root.resizeXDirection = event.x < root.width / 2 ? -1 : 1;
} else if (root.resizeXDirection !== 0 && root.resizeYDirection === 0) {
root.resizeYDirection = event.y < root.height / 2 ? -1 : 1;
}
}
onPositionChanged: (event) => {
if (!resizing) return;
contentContainer.implicitWidth = Math.max(root.persistentStateEntry.width + dragHandler.xAxis.activeValue * root.resizeXDirection, root.minimumWidth);
contentContainer.implicitHeight = Math.max(root.persistentStateEntry.height + dragHandler.yAxis.activeValue * root.resizeYDirection, root.minimumHeight);
const negativeXDrag = root.resizeXDirection === -1;
const negativeYDrag = root.resizeYDirection === -1;
const wantedX = root.persistentStateEntry.x + (negativeXDrag ? dragHandler.xAxis.activeValue : 0)
const wantedY = root.persistentStateEntry.y + (negativeYDrag ? dragHandler.yAxis.activeValue : 0)
const negativeXDragLimit = root.persistentStateEntry.x + root.persistentStateEntry.width - contentContainer.implicitWidth;
const negativeYDragLimit = root.persistentStateEntry.y + root.persistentStateEntry.height - contentContainer.implicitHeight;
root.x = negativeXDrag ? Math.min(wantedX, negativeXDragLimit) : wantedX;
root.y = negativeYDrag ? Math.min(wantedY, negativeYDragLimit) : wantedY;
}
DragHandler {
id: dragHandler
acceptedButtons: Qt.LeftButton | Qt.RightButton
target: (root.draggable && !root.resizing) ? root : null
onActiveChanged: { // Handle drag release
if (!active) {
root.resizing = false;
root.savePosition();
}
}
xAxis.minimum: 0
xAxis.maximum: root.parent?.width - root.width
yAxis.minimum: 0
yAxis.maximum: root.parent?.height - root.height
}
function close() { function close() {
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== root.identifier); Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== root.identifier);
@@ -82,26 +165,31 @@ AbstractOverlayWidget {
persistentStateEntry.clickthrough = !persistentStateEntry.clickthrough; persistentStateEntry.clickthrough = !persistentStateEntry.clickthrough;
} }
function savePosition(xPos = root.x, yPos = root.y) { function savePosition(xPos = root.x, yPos = root.y, width = contentContainer.implicitWidth, height = contentContainer.implicitHeight) {
persistentStateEntry.x = xPos; persistentStateEntry.x = Math.round(xPos);
persistentStateEntry.y = yPos; persistentStateEntry.y = Math.round(yPos);
persistentStateEntry.width = Math.round(width);
persistentStateEntry.height = Math.round(height);
} }
function center() { function center() {
const targetX = (root.parent.width - contentColumn.width) / 2 const targetX = (root.parent.width - contentColumn.width) / 2 - root.resizeMargin
const targetY = (root.parent.height - contentItem.height) / 2 - titleBar.implicitHeight + border.border.width const targetY = (root.parent.height - contentContainer.height) / 2 - titleBar.implicitHeight + border.border.width - root.resizeMargin
root.x = targetX root.x = targetX
root.y = targetY root.y = targetY
root.savePosition(targetX, targetY) root.savePosition(targetX, targetY)
} }
visible: GlobalStates.overlayOpen || actuallyPinned visible: GlobalStates.overlayOpen || actuallyPinned
implicitWidth: Math.max(contentColumn.implicitWidth, minWidth) implicitWidth: contentColumn.implicitWidth + resizeMargin * 2
implicitHeight: contentColumn.implicitHeight implicitHeight: contentColumn.implicitHeight + resizeMargin * 2
Rectangle { Rectangle {
id: border id: border
anchors.fill: parent anchors {
fill: parent
margins: root.resizeMargin
}
color: (root.fancyBorders && GlobalStates.overlayOpen) ? Appearance.colors.colLayer1 : "transparent" color: (root.fancyBorders && GlobalStates.overlayOpen) ? Appearance.colors.colLayer1 : "transparent"
radius: root.radius radius: root.radius
border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1) border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1)
@@ -205,8 +293,8 @@ AbstractOverlayWidget {
Layout.margins: root.fancyBorders ? root.padding : 0 Layout.margins: root.fancyBorders ? root.padding : 0
Layout.topMargin: -border.border.width // Border of a rectangle is drawn inside its bounds, so we do this to make the gap not too big Layout.topMargin: -border.border.width // Border of a rectangle is drawn inside its bounds, so we do this to make the gap not too big
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
implicitWidth: root.contentItem.implicitWidth implicitWidth: Math.max(root.persistentStateEntry.width, root.minimumWidth)
implicitHeight: root.contentItem.implicitHeight implicitHeight: Math.max(root.persistentStateEntry.height, root.minimumHeight)
children: [root.contentItem] children: [root.contentItem]
} }
} }
@@ -11,6 +11,7 @@ StyledOverlayWidget {
opacity: 1 // The crosshair itself already has transparency if configured opacity: 1 // The crosshair itself already has transparency if configured
showClickabilityButton: false showClickabilityButton: false
clickthrough: true clickthrough: true
resizable: false
contentItem: CrosshairContent { contentItem: CrosshairContent {
anchors.centerIn: parent anchors.centerIn: parent
@@ -6,6 +6,8 @@ import qs.modules.overlay
StyledOverlayWidget { StyledOverlayWidget {
id: root id: root
title: "MangoHud FPS" title: "MangoHud FPS"
minimumWidth: 275
minimumHeight: 100
contentItem: FpsLimiterContent { contentItem: FpsLimiterContent {
radius: root.contentRadius radius: root.contentRadius
} }
@@ -9,25 +9,22 @@ import qs.modules.overlay
StyledOverlayWidget { StyledOverlayWidget {
id: root id: root
minimumWidth: 310
minimumHeight: 130
contentItem: Rectangle { contentItem: Rectangle {
id: contentItem id: contentItem
anchors.centerIn: parent anchors.fill: parent
radius: root.contentRadius radius: root.contentRadius
color: Appearance.m3colors.m3surfaceContainer color: Appearance.m3colors.m3surfaceContainer
property real padding: 8 property real padding: 8
implicitHeight: contentColumn.implicitHeight + padding * 2
implicitWidth: 350
ColumnLayout { ColumnLayout {
id: contentColumn id: contentColumn
anchors { anchors.centerIn: parent
fill: parent
margins: parent.padding
}
spacing: 10 spacing: 10
Row { Row {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: 10 spacing: 10
BigRecorderButton { BigRecorderButton {
@@ -68,7 +65,7 @@ StyledOverlayWidget {
} }
RippleButton { RippleButton {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.fillWidth: false Layout.fillWidth: false
buttonRadius: height / 2 buttonRadius: height / 2
colBackground: Appearance.colors.colLayer3 colBackground: Appearance.colors.colLayer3
@@ -14,6 +14,8 @@ import qs.modules.overlay
StyledOverlayWidget { StyledOverlayWidget {
id: root id: root
minimumWidth: 300
minimumHeight: 200
property list<var> resources: [ property list<var> resources: [
{ {
"icon": "planner_review", "icon": "planner_review",
@@ -37,13 +39,10 @@ StyledOverlayWidget {
contentItem: Rectangle { contentItem: Rectangle {
id: contentItem id: contentItem
anchors.centerIn: parent anchors.fill: parent
color: Appearance.m3colors.m3surfaceContainer color: Appearance.m3colors.m3surfaceContainer
radius: root.contentRadius radius: root.contentRadius
property real padding: 4 property real padding: 4
implicitWidth: 350
implicitHeight: 200
// implicitHeight: contentColumn.implicitHeight + padding * 2
ColumnLayout { ColumnLayout {
id: contentColumn id: contentColumn
anchors { anchors {
@@ -10,13 +10,14 @@ import qs.modules.sidebarRight.volumeMixer
StyledOverlayWidget { StyledOverlayWidget {
id: root id: root
minimumWidth: 300
minimumHeight: 380
contentItem: Rectangle { contentItem: Rectangle {
anchors.centerIn: parent anchors.fill: parent
color: Appearance.m3colors.m3surfaceContainer color: Appearance.m3colors.m3surfaceContainer
radius: root.contentRadius radius: root.contentRadius
property real padding: 6 property real padding: 6
implicitHeight: 600
implicitWidth: 350
ColumnLayout { ColumnLayout {
id: contentColumn id: contentColumn