// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 import QtQuick import QtQuick.Controls.Basic as Basic import QtQuick.Window import QtQuick.Shapes import StudioTheme as StudioTheme import StudioWindowManager QtObject { id: root property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle property alias titleBar: titleBarContent.children default property alias content: mainContent.children property alias contentWidth: mainContent.width property int width: 320 property int height: column.implicitHeight property int maximumWidth: -1 property int maximumHeight: -1 property alias flags: window.flags property alias visible: window.visible property int anchorGap: 10 property int edge: Qt.LeftEdge property int actualEdge: root.edge //property alias chevronVisible: chevron.visible property rect __itemGlobal: Qt.rect(0, 0, 100, 100) property bool __movedManually: false property bool keepOpen: false signal closing(close: var) function showGlobal() { root.__movedManually = false var pos = WindowManager.globalCursorPosition() root.__itemGlobal = Qt.rect(pos.x, pos.y, 300, 20) //root.chevronVisible = false root.layout() window.show() window.raise() } function show(target: Item): void { root.__movedManually = false var originGlobal = target.mapToGlobal(0, 0) root.__itemGlobal = Qt.rect(originGlobal.x, originGlobal.y, target.width, target.height) //root.chevronVisible = true root.layout() window.show() window.raise() } function close() { window.close() } function layout() { if (root.__movedManually) return let position = Qt.point(root.__itemGlobal.x, root.__itemGlobal.y) var screen = WindowManager.getScreenGeometry(position) // Collect region information let edges = window.getRegions(screen, root.__itemGlobal) if (Object.keys(edges).length === 0) { console.log("Warning: Couldn't find regions.") return } // Determine edge let edge = window.findPlacement(edges) root.actualEdge = edge let anchor = edges[edge].anchor let popoverRect = window.popoverGeometry(edge, anchor, edges[edge].region) //if (chevron.visible) // chevron.layout(edge, popoverRect, anchor) window.x = popoverRect.x window.y = popoverRect.y } property Window window: Window { id: window property int margin: 0 //20 width: root.width + (2 * window.margin) height: root.height + (2 * window.margin) maximumWidth: { if (root.maximumWidth < 0) return root.width + (2 * window.margin) else return root.maximumWidth + (2 * window.margin) } maximumHeight:{ if (root.maximumHeight < 0) return root.height + (2 * window.margin) else return root.maximumHeight + (2 * window.margin) } visible: false flags: Qt.FramelessWindowHint | Qt.Tool color: "transparent" onClosing: function (close) { root.closing(close) } function findPlacement(edges: var): int { if (edges[root.edge].fit) // Original edge does fit return root.edge return window.findAlternativePlacement(edges) } function findAlternativePlacement(edges: var): int { let horizontal = (Qt.LeftEdge | Qt.RightEdge) if (root.edge & horizontal) if (edges[root.edge ^ horizontal].fit) return root.edge ^ horizontal let vertical = (Qt.TopEdge | Qt.BottomEdge) if (root.edge & vertical) if (edges[root.edge ^ vertical].fit) return root.edge ^ vertical // Take the first that fits for (var key in edges) { if (edges[key].fit) return Number(key) } return Qt.LeftEdge // Default } function contains(a: rect, b: rect): bool { let halfSizeA = Qt.size(a.width * 0.5, a.height * 0.5) let halfSizeB = Qt.size(b.width * 0.5, b.height * 0.5) let centerA = Qt.point(a.x + halfSizeA.width, a.y + halfSizeA.height) let centerB = Qt.point(b.x + halfSizeB.width, b.y + halfSizeB.height) if (Math.abs(centerA.x - centerB.x) > (halfSizeA.width + halfSizeB.width)) return false if (Math.abs(centerA.y - centerB.y) > (halfSizeA.height + halfSizeB.height)) return false return true } function getRegions(source: rect, target: rect): var { var edges = {} // Overlaps or Inside if (!window.contains(source, target)) return edges var targetCenter = Qt.point(target.x + (target.width * 0.5), target.y + (target.height * 0.5)) // Just as a reminder why calculating custom right and bottom: // > Note that for historical reasons this function returns top() + height() - 1; // > use y() + height() to retrieve the true y-coordinate. let sourceRight = source.x + source.width let sourceBottom = source.y + source.height // TOP let topAnchor = Qt.point(targetCenter.x, target.y - root.anchorGap) let topRegion = Qt.rect(source.x, source.y, source.width, (topAnchor.y < source.top) ? 0 : Math.abs(topAnchor.y - source.top)) edges[Qt.TopEdge] = { anchor: topAnchor, region: topRegion, fit: topRegion.width >= window.maximumWidth && topRegion.height >= window.maximumHeight } // RIGHT let rightAnchor = Qt.point(target.x + target.width + root.anchorGap, targetCenter.y) let rightRegion = Qt.rect(rightAnchor.x, source.y, (rightAnchor.x > sourceRight) ? 0 : Math.abs(sourceRight - rightAnchor.x), source.height) edges[Qt.RightEdge] = { anchor: rightAnchor, region: rightRegion, fit: rightRegion.width >= window.maximumWidth && rightRegion.height >= window.maximumHeight } // BOTTOM let bottomAnchor = Qt.point(targetCenter.x, target.y + target.height + root.anchorGap) let bottomRegion = Qt.rect(source.x, bottomAnchor.y, source.width, (bottomAnchor.y > sourceBottom) ? 0 : Math.abs(sourceBottom - bottomAnchor.y)) edges[Qt.BottomEdge] = { anchor: bottomAnchor, region: bottomRegion, fit: bottomRegion.width >= window.maximumWidth && bottomRegion.height >= window.maximumHeight } // LEFT let leftAnchor = Qt.point(target.x - root.anchorGap, targetCenter.y) let leftRegion = Qt.rect(source.x, source.y, (leftAnchor.x < source.left) ? 0 : Math.abs(leftAnchor.x - source.left), source.height) edges[Qt.LeftEdge] = { anchor: leftAnchor, region: leftRegion, fit: leftRegion.width >= window.maximumWidth && leftRegion.height >= window.maximumHeight } return edges } function popoverGeometry(edge: int, anchor: point, region: rect): rect { if (edge === Qt.TopEdge) { let height = Math.min(window.height, region.height) return Qt.rect(Math.max(region.x, Math.min(anchor.x - (window.width * 0.5), region.x + region.width - window.width)), anchor.y - height, Math.min(window.width, region.width), height) } if (edge === Qt.RightEdge) { let width = Math.min(window.width, region.width) return Qt.rect(anchor.x, Math.max(region.y, Math.min(anchor.y - (window.height * 0.5), region.y + region.height - window.height)), width, Math.min(window.height, region.height)) } if (edge === Qt.BottomEdge) { let height = Math.min(window.height, region.height) return Qt.rect(Math.max(region.x, Math.min(anchor.x - (window.width * 0.5), region.x + region.width - window.width)), anchor.y, Math.min(window.width, region.width), height) } if (edge === Qt.LeftEdge) { let width = Math.min(window.width, region.width) return Qt.rect(anchor.x - width, Math.max(region.y, Math.min(anchor.y - (window.height * 0.5), region.y + region.height - window.height)), width, Math.min(window.height, region.height)) } } onHeightChanged: { if (window.visible) root.layout() } Component.onCompleted: { if (window.visible) root.layout() } Connections { target: WindowManager enabled: root.visible function onFocusWindowChanged(focusWindow) { if (!focusWindow) return if (root.keepOpen) return if (focusWindow !== window && focusWindow.transientParent !== window) root.close() } function onAboutToQuit() { root.close() } function onMainWindowVisibleChanged(value) { if (!value) root.close() } } Rectangle { id: background anchors.fill: parent anchors.margins: window.margin color: StudioTheme.Values.themePopoutBackground border.color: "#636363" border.width: StudioTheme.Values.border TapHandler { id: tapHandler onTapped: root.close() } containmentMask: QtObject { function contains(point: point): bool { return point.x < 0 || point.x > background.width || point.y < 0 || point.y > background.height } } } /* // The chevron will be reactivated when we fixed all the issues that where found during testing. // * Potential Qt bug: black background instead of transparent border due to GPU selection on Windows // * Ghost chevron on macOS after dragging the window Shape { id: chevron property int chevronWidth: window.margin anchors.fill: parent function layout(edge: int, rect: rect, anchor: point) { let center = Qt.point(rect.x + (rect.width * 0.5), rect.y + (rect.height * 0.5)) // Horizontal if (edge === Qt.LeftEdge || edge === Qt.RightEdge) { let topLimit = window.margin let bottomLimit = window.height - window.margin - background.border.width let point = Math.round((window.height * 0.5) + (anchor.y - center.y)) peak.y = Math.max(topLimit, Math.min(bottomLimit, point)) let topLimitChevron = topLimit + chevron.chevronWidth let bottomLimitChevron = bottomLimit - chevron.chevronWidth path.startY = Math.max(topLimit, Math.min(bottomLimitChevron, point - chevron.chevronWidth)) end.y = Math.max(topLimitChevron, Math.min(bottomLimit, point + chevron.chevronWidth)) } if (edge === Qt.LeftEdge) { peak.x = window.width - background.border.width path.startX = end.x = window.width - window.margin - background.border.width } if (edge === Qt.RightEdge) { peak.x = background.border.width path.startX = end.x = window.margin + background.border.width } // Vertical if (edge === Qt.TopEdge || edge === Qt.BottomEdge) { let leftLimit = window.margin + background.border.width let rightLimit = window.width - window.margin let point = Math.round((window.width * 0.5) + (anchor.x - center.x)) peak.x = Math.max(leftLimit, Math.min(rightLimit, point)) let leftLimitChevron = leftLimit + chevron.chevronWidth let rightLimitChevron = rightLimit - chevron.chevronWidth path.startX = Math.max(leftLimit, Math.min(rightLimitChevron, point - chevron.chevronWidth)) end.x = Math.max(leftLimitChevron, Math.min(rightLimit, point + chevron.chevronWidth)) } if (edge === Qt.TopEdge) { peak.y = window.height - background.border.width path.startY = end.y = window.height - window.margin - background.border.width } if (edge === Qt.BottomEdge) { peak.y = background.border.width path.startY = end.y = window.margin + background.border.width } } ShapePath { id: path strokeStyle: ShapePath.SolidLine strokeWidth: background.border.width strokeColor: background.border.color fillColor: background.color startX: 0 startY: 0 PathLine { id: peak; x: 0; y: 0 } PathLine { id: end; x: 0; y: 0 } } } */ Column { id: column anchors.fill: parent anchors.margins: window.margin + StudioTheme.Values.border Item { id: titleBarItem width: parent.width height: StudioTheme.Values.titleBarHeight DragHandler { id: dragHandler target: null grabPermissions: PointerHandler.CanTakeOverFromAnything onActiveChanged: { if (dragHandler.active) { root.__movedManually = true window.startSystemMove() // QTBUG-102488 } } } Row { id: row anchors.fill: parent anchors.leftMargin: StudioTheme.Values.popupMargin anchors.rightMargin: StudioTheme.Values.popupMargin spacing: 0 Item { id: titleBarContent width: row.width - closeIndicator.width height: row.height } IconIndicator { id: closeIndicator anchors.verticalCenter: parent.verticalCenter icon: StudioTheme.Constants.colorPopupClose pixelSize: StudioTheme.Values.myIconFontSize onClicked: root.close() } } } Rectangle { width: parent.width - 8 height: StudioTheme.Values.border anchors.horizontalCenter: parent.horizontalCenter color: "#636363" } Basic.ScrollView { id: scrollView width: parent.width height: { let actualHeight = mainContent.childrenRect.height + 2 * StudioTheme.Values.popupMargin if (root.maximumHeight < 0) return actualHeight return Math.min(actualHeight, root.maximumHeight - titleBarItem.height - 3 * StudioTheme.Values.border) } padding: StudioTheme.Values.popupMargin clip: true ScrollBar.vertical: TransientScrollBar { id: verticalBar style: StudioTheme.Values.controlStyle parent: scrollView x: scrollView.width - verticalBar.width y: scrollView.topPadding height: scrollView.availableHeight orientation: Qt.Vertical show: (scrollView.hovered || scrollView.focus) && verticalBar.isNeeded //otherInUse: horizontalBar.inUse } Flickable { id: frame boundsMovement: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds contentWidth: mainContent.width contentHeight: mainContent.height Item { id: mainContent width: scrollView.width - 2 * scrollView.padding height: mainContent.childrenRect.height anchors.horizontalCenter: parent.horizontalCenter } } } } } }