diff --git a/meshroom/ui/components/__init__.py b/meshroom/ui/components/__init__.py index 59c77618f8..aef07a61f2 100755 --- a/meshroom/ui/components/__init__.py +++ b/meshroom/ui/components/__init__.py @@ -1,11 +1,12 @@ def registerTypes(): - from PySide6.QtQml import qmlRegisterType + from PySide6.QtQml import qmlRegisterType, qmlRegisterSingletonType from meshroom.ui.components.clipboard import ClipboardHelper from meshroom.ui.components.edge import EdgeMouseArea from meshroom.ui.components.filepath import FilepathHelper from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController, Transformations3DHelper from meshroom.ui.components.csvData import CsvData + from meshroom.ui.components.geom2D import Geom2D qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea") qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable @@ -14,3 +15,5 @@ def registerTypes(): qmlRegisterType(Transformations3DHelper, "Meshroom.Helpers", 1, 0, "Transformations3DHelper") # TODO: uncreatable qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController") qmlRegisterType(CsvData, "DataObjects", 1, 0, "CsvData") + + qmlRegisterSingletonType(Geom2D, "Meshroom.Helpers", 1, 0, "Geom2D") diff --git a/meshroom/ui/components/geom2D.py b/meshroom/ui/components/geom2D.py new file mode 100644 index 0000000000..5cc972f1c2 --- /dev/null +++ b/meshroom/ui/components/geom2D.py @@ -0,0 +1,8 @@ +from PySide6.QtCore import QObject, Slot, QRectF + + +class Geom2D(QObject): + @Slot(QRectF, QRectF, result=bool) + def rectRectIntersect(self, rect1: QRectF, rect2: QRectF) -> bool: + """Check if two rectangles intersect.""" + return rect1.intersects(rect2) diff --git a/meshroom/ui/qml/Utils/DelegateSelectionBox.qml b/meshroom/ui/qml/Utils/DelegateSelectionBox.qml new file mode 100644 index 0000000000..a6034c8ac3 --- /dev/null +++ b/meshroom/ui/qml/Utils/DelegateSelectionBox.qml @@ -0,0 +1,32 @@ +import QtQuick +import Meshroom.Helpers + +/* +A SelectionBox that can be used to select delegates in a model instantiator (Repeater, ListView...). +Interesection test is done in the coordinate system of the container Item, using delegate's bounding boxes. +The list of selected indices is emitted when the selection ends. +*/ + +SelectionBox { + id: root + + // The Item instantiating the delegates. + property Item modelInstantiator + // The Item containing the delegates (used for coordinate mapping). + property Item container + // Emitted when the selection has ended, with the list of selected indices and modifiers. + signal delegateSelectionEnded(list indices, int modifiers) + + onSelectionEnded: function(selectionRect, modifiers) { + let selectedIndices = []; + const mappedSelectionRect = mapToItem(container, selectionRect); + for (var i = 0; i < modelInstantiator.count; ++i) { + const delegate = modelInstantiator.itemAt(i); + const delegateRect = Qt.rect(delegate.x, delegate.y, delegate.width, delegate.height); + if (Geom2D.rectRectIntersect(mappedSelectionRect, delegateRect)) { + selectedIndices.push(i); + } + } + delegateSelectionEnded(selectedIndices, modifiers); + } +} diff --git a/meshroom/ui/qml/Utils/SelectionBox.qml b/meshroom/ui/qml/Utils/SelectionBox.qml new file mode 100644 index 0000000000..e7763e8437 --- /dev/null +++ b/meshroom/ui/qml/Utils/SelectionBox.qml @@ -0,0 +1,60 @@ +import QtQuick + +/* +Simple selection box that can be used by a MouseArea. + +Usage: +1. Create a MouseArea and a SelectionBox. +2. Bind the SelectionBox to the MouseArea by setting the `mouseArea` property. +3. Call startSelection() with coordinates when the selection starts. +4. Call endSelection() when the selection ends. +5. Listen to the selectionEnded signal to get the selection rectangle. +*/ + +Item { + id: root + + property MouseArea mouseArea + property alias color: selectionBox.color + property alias border: selectionBox.border + + readonly property bool active: mouseArea.drag.target == dragTarget + + signal selectionEnded(rect selectionRect, int modifiers) + + function startSelection(mouse) { + dragTarget.startPos.x = dragTarget.x = mouse.x; + dragTarget.startPos.y = dragTarget.y = mouse.y; + dragTarget.modifiers = mouse.modifiers; + mouseArea.drag.target = dragTarget; + } + + function endSelection() { + if (!active) { + return; + } + mouseArea.drag.target = null; + const rect = Qt.rect(selectionBox.x, selectionBox.y, selectionBox.width, selectionBox.height) + selectionEnded(rect, dragTarget.modifiers); + } + + visible: active + + Rectangle { + id: selectionBox + color: "#109b9b9b" + border.width: 1 + border.color: "#b4b4b4" + + x: Math.min(dragTarget.startPos.x, dragTarget.x) + y: Math.min(dragTarget.startPos.y, dragTarget.y) + width: Math.abs(dragTarget.x - dragTarget.startPos.x) + height: Math.abs(dragTarget.y - dragTarget.startPos.y) + } + + Item { + id: dragTarget + property point startPos + property var modifiers + } +} diff --git a/meshroom/ui/qml/Utils/qmldir b/meshroom/ui/qml/Utils/qmldir index 1fb827b937..7761357fd3 100644 --- a/meshroom/ui/qml/Utils/qmldir +++ b/meshroom/ui/qml/Utils/qmldir @@ -6,6 +6,8 @@ Request 1.0 request.js Format 1.0 format.js ErrorHandler 1.0 errorHandler.js singleton ExifOrientation 1.0 ExifOrientation.qml +SelectionBox 1.0 SelectionBox.qml +DelegateSelectionBox 1.0 DelegateSelectionBox.qml # using singleton here causes random crash at application exit # singleton Clipboard 1.0 Clipboard.qml # singleton Filepath 1.0 Filepath.qml