Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions meshroom/ui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,10 @@ def undoImpl(self):


class RemoveImagesCommand(GraphCommand):
"""
Remove all the images from one or several CameraInit nodes as a single operation.
Both the viewpoints and intrinsics lists are reset to their default values.
"""
def __init__(self, graph, cameraInitNodes, parent=None):
super().__init__(graph, parent)
self.cameraInits = cameraInitNodes
Expand Down Expand Up @@ -560,6 +564,43 @@ def undoImpl(self):
self.graph.node(cameraInit).intrinsics.value = self.intrinsics[cameraInit]


class RemoveSelectedImagesCommand(GraphCommand):
"""
Remove a specific subset of images (viewpoints and their orphaned intrinsics) from a single
CameraInit node as a single operation.
"""
def __init__(self, graph, cameraInitNode, imagesToRemove, parent=None):
super().__init__(graph, parent)
self.cameraInitNode = cameraInitNode

# Save current state of viewpoints and intrinsics
self.oldViewpoints = cameraInitNode.attribute("viewpoints").getSerializedValue()
self.oldIntrinsics = cameraInitNode.attribute("intrinsics").getSerializedValue()

# Build a set of viewIds to remove based on the provided images list and then the new viewpoints list
removeViewIds = {image.viewId.value for image in imagesToRemove}
self.newViewpoints = [vp for vp in self.oldViewpoints if vp.get("viewId") not in removeViewIds]

# Compute set of intrinsicIds that are still referenced by the remaining viewpoints and then
# the new intrinsics list
keptIntrinsicIds = {vp.get("intrinsicId") for vp in self.newViewpoints}
self.newIntrinsics = [intr for intr in self.oldIntrinsics if intr.get("intrinsicId") in keptIntrinsicIds]

self.title = f"Remove {len(removeViewIds)} Image{'(s)' if len(removeViewIds) > 1 else ''}"
self.setText(self.title)

def redoImpl(self):
with GraphModification(self.graph):
self.cameraInitNode.viewpoints.value = self.newViewpoints
self.cameraInitNode.intrinsics.value = self.newIntrinsics
return True

def undoImpl(self):
with GraphModification(self.graph):
self.cameraInitNode.viewpoints.value = self.oldViewpoints
self.cameraInitNode.intrinsics.value = self.oldIntrinsics


class MoveNodeCommand(GraphCommand):
""" Move a node to a given position. """
def __init__(self, graph, node, position, parent=None):
Expand Down
7 changes: 7 additions & 0 deletions meshroom/ui/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,13 @@ def removeImage(self, image):
# After every check we finally remove the attribute
self.removeAttribute(image)

@Slot(list)
def removeImages(self, images: list):
""" Remove a list of images as a single operation. """
if not images:
return
self.push(commands.RemoveSelectedImagesCommand(self._graph, self.cameraInit, images))

@Slot()
def removeAllImages(self):
with self.groupedGraphModification("Remove All Images"):
Expand Down
18 changes: 10 additions & 8 deletions meshroom/ui/qml/ImageGallery/ImageDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ Item {
property variant parentModel
property int selectedIndex: parentModel ? parentModel.selectedIndex : -1
property bool isCurrentItem: cellID >= 0 && cellID === selectedIndex
property var selectedIndices: parentModel ? parentModel.selectedIndices : []
property bool isInMultiSelection: cellID >= 0 && selectedIndices.indexOf(cellID) >= 0

signal pressed(var mouse)
signal removeRequest()
signal removeSelectedRequest()
signal removeAllImagesRequest()

default property alias children: imageMA.children
Expand Down Expand Up @@ -102,9 +104,9 @@ Item {
}
}
MenuItem {
text: "Remove"
enabled: !root.readOnly
onClicked: removeRequest()
text: "Remove Selected Image" + (root.selectedIndices.length > 1 ? "s " : " ") + "(" + root.selectedIndices.length + ")"
enabled: !root.readOnly && root.selectedIndices.length > 0
onClicked: removeSelectedRequest()
}
MenuItem {
text: "Remove All Images"
Expand Down Expand Up @@ -155,7 +157,7 @@ Item {
Layout.fillWidth: true
visible: root.displayThumbnail
border.color: isCurrentItem ? grid_imageLabel.palette.highlight : Qt.darker(grid_imageLabel.palette.highlight)
border.width: imageMA.containsMouse || root.isCurrentItem ? 2 : 0
border.width: imageMA.containsMouse || root.isCurrentItem || root.isInMultiSelection ? 2 : 0
Image {
id: grid_thumbnail
anchors.fill: parent
Expand Down Expand Up @@ -206,7 +208,7 @@ Item {
horizontalAlignment: Text.AlignHCenter
text: Filepath.basename(root.source)
background: Rectangle {
color: root.isCurrentItem ? parent.palette.highlight : "transparent"
color: root.isCurrentItem ? parent.palette.highlight : (root.isInMultiSelection ? Qt.alpha(parent.palette.highlight, 0.5) : "transparent")
}
}

Expand Down Expand Up @@ -243,7 +245,7 @@ Item {
visible: root.displayThumbnail

border.color: isCurrentItem ? list_imageLabel.palette.highlight : Qt.darker(list_imageLabel.palette.highlight)
border.width: imageMA.containsMouse || root.isCurrentItem ? 2 : 0
border.width: imageMA.containsMouse || root.isCurrentItem || root.isInMultiSelection ? 2 : 0

Image {
id: list_thumbnail
Expand Down Expand Up @@ -301,7 +303,7 @@ Item {
verticalAlignment: Text.AlignVCenter
text: Filepath.basename(root.source)
background: Rectangle {
color: root.isCurrentItem ? parent.palette.highlight : "transparent"
color: root.isCurrentItem ? parent.palette.highlight : (root.isInMultiSelection ? Qt.alpha(parent.palette.highlight, 0.5) : "transparent")
}
}

Expand Down
131 changes: 120 additions & 11 deletions meshroom/ui/qml/ImageGallery/ImageGallery.qml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Panel {
property int nbMeshroomScenes: 0
property int nbDraggedFiles: 0

signal removeImageRequest(var attribute)
signal removeSelectedImagesRequest(var objects)
signal allViewpointsCleared()
signal filesDropped(var drop)

Expand All @@ -53,6 +53,7 @@ Panel {

function onCameraInitChanged() {
nodesCB.currentIndex = root.cameraInitIndex
sortedModel.clearMultiSelection(false)
}
}

Expand Down Expand Up @@ -230,6 +231,43 @@ Panel {
}

property int selectedIndex: -1
property var selectedIndices: []

function toggleIndex(idx) {
var newArr = selectedIndices.slice()
var pos = newArr.indexOf(idx)
if (pos >= 0) {
newArr.splice(pos, 1)
} else {
newArr.push(idx)
}
selectedIndices = newArr
}

function selectRange(from, to) {
var newArr = []
var start = Math.min(from, to)
var end = Math.max(from, to)
for (var i = start; i <= end; i++) {
newArr.push(i)
}
selectedIndices = newArr
}

function clearMultiSelection(keepPosition) {
if (keepPosition) {
// Pick the lowest selected index as the landing position; after removal
// the next surviving item slides up to that slot.
// Clamp to the last remaining item in case the selection was at the tail.
var sortedSel = selectedIndices.slice().sort(function(a, b){ return a - b })
var remainingCount = count - selectedIndices.length
selectedIndex = Math.min(sortedSel[0], remainingCount - 1)
selectedIndices = [selectedIndex]
} else {
selectedIndex = -1
selectedIndices = []
}
}

delegate: ImageDelegate {
id: imageDelegate
Expand All @@ -247,21 +285,93 @@ Panel {

parentModel: sortedModel

onPressed: {
onPressed: function(mouse) {
if (mouse.button !== Qt.LeftButton)
return
if (layoutLoader.item) {
layoutLoader.item.currentIndex = DelegateModel.filteredIndex
sortedModel.selectedIndex = DelegateModel.filteredIndex
var idx = DelegateModel.filteredIndex
if (mouse.modifiers & Qt.ShiftModifier && sortedModel.selectedIndex >= 0) {
// Range select from last selectedIndex to clicked item
sortedModel.selectRange(sortedModel.selectedIndex, idx)
} else if (mouse.modifiers & Qt.ControlModifier) {
// Toggle this item's selection
sortedModel.toggleIndex(idx)
// If the item is being removed from the selection, then we should return
// before setting the current index: this prevents highlighting the item which is being
// removed, as it could be confusing for the user
if (sortedModel.selectedIndices.indexOf(idx) < 0) {
if (sortedModel.selectedIndices.length === 0) {
// Last item deselected: clear the viewer entirely
sortedModel.selectedIndex = -1
layoutLoader.item.currentIndex = -1
_currentScene.selectedViewId = "-1"
} else if (idx === sortedModel.selectedIndex) {
// The currently viewed item was deselected: move to the
// closest remaining selected item.
var remaining = sortedModel.selectedIndices
var next = remaining[0]
var minDist = Math.abs(remaining[0] - idx)
for (var r = 1; r < remaining.length; r++) {
var dist = Math.abs(remaining[r] - idx)
if (dist < minDist) {
minDist = dist
next = remaining[r]
}
}
sortedModel.selectedIndex = next
layoutLoader.item.currentIndex = next
}
return
}
} else {
// Normal click: clear multi-selection, select only this item
sortedModel.selectedIndices = [idx]
}
// Update selectedIndex before currentIndex to prevent onCurrentItemChanged
// from incorrectly resetting the multi-selection
sortedModel.selectedIndex = idx
layoutLoader.item.currentIndex = idx
}
}

function sendRemoveRequest() {
function sendRemoveSelectedRequest() {
if (readOnly)
return

root.removeImageRequest(object)

// Capture delegate-scope references immediately: this prevents falling into
// cases where "sortedModel" is unresolvable because the delegate has been destroyed before
// the line accessing "sortedModel" is reached
var model = sortedModel
var view = root.galleryGrid

// If all the images are selected, we can just remove all of them at once
if (model.selectedIndices.length === m.viewpoints.count) {
removeAllImages()
return
}

var objects = []
for (var i = 0; i < model.selectedIndices.length; i++) {
var obj = model.getObjectAt(model.selectedIndices[i])
if (obj)
objects.push(obj)
}
if (objects.length > 0) {
root.removeSelectedImagesRequest(objects)
model.clearMultiSelection(true)

// Restore a sensible position once the model has finished updating
var targetIndex = model.selectedIndex
Qt.callLater(function() {
if (targetIndex >= 0 && view) {
view.currentIndex = targetIndex
view.makeCurrentItemVisible()
}
})
}

// If the last image has been removed, make sure the viewpoints and intrinsics are reset
if (m.viewpoints.count === 0)
if (m.viewpoints !== undefined && m.viewpoints.count === 0)
root.allViewpointsCleared()
}

Expand All @@ -270,12 +380,12 @@ Panel {
_currentScene.selectedViewId = "-1"
}

onRemoveRequest: sendRemoveRequest()
onRemoveSelectedRequest: sendRemoveSelectedRequest()
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) {
removeAllImages()
} else if (event.key === Qt.Key_Delete) {
sendRemoveRequest()
sendRemoveSelectedRequest()
}
}
onRemoveAllImagesRequest: {
Expand Down Expand Up @@ -364,7 +474,6 @@ Panel {
item.thumbnailSizeSlider = thumbnailSizeSlider

// Connect signals
item.removeImageRequest.connect(root.removeImageRequest)
item.allViewpointsCleared.connect(root.allViewpointsCleared)

// Restore currentIndex (before connecting signals to avoid unwanted selection change)
Expand Down
11 changes: 10 additions & 1 deletion meshroom/ui/qml/ImageGallery/ImageGridView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ GridView {
property var sortedModel: null

// Signals
signal removeImageRequest(var attribute)
signal allViewpointsCleared()

ScrollBar.vertical: MScrollBar {
Expand Down Expand Up @@ -67,6 +66,12 @@ GridView {
if (tempCameraInit !== null && root.currentIndex == 0)
_currentScene.selectedViewId = -1
_currentScene.selectedViewId = root.currentItem.viewpoint.get("viewId").value
if (sortedModel && sortedModel.selectedIndex !== root.currentIndex) {
sortedModel.selectedIndex = root.currentIndex
sortedModel.selectedIndices = [root.currentIndex]
}
} else {
_currentScene.selectedViewId = "-1"
}
}

Expand Down Expand Up @@ -118,6 +123,10 @@ GridView {
if (searchBar)
searchBar.forceActiveFocus()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
if (sortedModel)
sortedModel.selectedIndices = [sortedModel.selectedIndex]
event.accepted = true
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion meshroom/ui/qml/ImageGallery/ImageListView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ ListView {
property real cellHeight: thumbnailSizeSlider ? thumbnailSizeSlider.value / 2 : 80

// Signals
signal removeImageRequest(var attribute)
signal allViewpointsCleared()

ScrollBar.vertical: MScrollBar {
Expand Down Expand Up @@ -68,6 +67,12 @@ ListView {
if (tempCameraInit !== null && root.currentIndex == 0)
_currentScene.selectedViewId = -1
_currentScene.selectedViewId = root.currentItem.viewpoint.get("viewId").value
if (sortedModel && sortedModel.selectedIndex !== root.currentIndex) {
sortedModel.selectedIndex = root.currentIndex
sortedModel.selectedIndices = [root.currentIndex]
}
} else {
_currentScene.selectedViewId = "-1"
}
}

Expand Down Expand Up @@ -113,6 +118,10 @@ ListView {
if (searchBar)
searchBar.forceActiveFocus()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
if (sortedModel)
sortedModel.selectedIndices = [sortedModel.selectedIndex]
event.accepted = true
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions meshroom/ui/qml/Utils/SortFilterDelegateModel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ DelegateModel {
return -1
}

/// Get the model.object for the item at 'filteredIndex'
function getObjectAt(filteredIndex) {
if (filteredIndex >= 0 && filteredIndex < filteredItems.count) {
return filteredItems.get(filteredIndex).model.object
}
return null
}

/**
* Return whether 'value' respects 'filter' condition
*
Expand Down
Loading
Loading