diff --git a/meshroom/ui/qml/Controls/SearchBar.qml b/meshroom/ui/qml/Controls/SearchBar.qml
index 56b8bb71bd..863b6deda1 100644
--- a/meshroom/ui/qml/Controls/SearchBar.qml
+++ b/meshroom/ui/qml/Controls/SearchBar.qml
@@ -31,6 +31,7 @@ FocusScope {
Keys.forwardTo: [field]
function forceActiveFocus() {
+ root.isVisible = true
field.forceActiveFocus()
}
@@ -46,7 +47,7 @@ FocusScope {
text: MaterialIcons.search
onClicked: {
- isVisible = !root.isVisible
+ root.isVisible = !root.isVisible
// Set Focus on the Text Field
field.focus = field.visible
}
@@ -73,6 +74,9 @@ FocusScope {
if ((event.key == Qt.Key_Return || event.key == Qt.Key_Enter)) {
event.accepted = true
root.accepted()
+ } else if (event.key == Qt.Key_Escape) {
+ root.isVisible = false
+ field.focus = false
}
}
diff --git a/meshroom/ui/qml/Homepage.qml b/meshroom/ui/qml/Homepage.qml
index 17d8565a4e..d41c0f44e5 100644
--- a/meshroom/ui/qml/Homepage.qml
+++ b/meshroom/ui/qml/Homepage.qml
@@ -271,7 +271,7 @@ Page {
}
GridView {
- id: gridView
+ id: homepageGridView
visible: tabPanel.currentTab === 1
anchors.fill: parent
anchors.topMargin: cellHeight * 0.1
@@ -309,21 +309,21 @@ Page {
delegate: Column {
id: projectContent
- width: gridView.cellWidth
- height: gridView.cellHeight
+ width: homepageGridView.cellWidth
+ height: homepageGridView.cellHeight
property var source: modelData["thumbnail"] ? Filepath.stringToUrl(modelData["thumbnail"]) : ""
function updateThumbnail() {
- thumbnail.source = ThumbnailCache.thumbnail(source, gridView.currentIndex)
+ thumbnail.source = ThumbnailCache.thumbnail(source, homepageGridView.currentIndex)
}
onSourceChanged: updateThumbnail()
Button {
id: projectDelegate
- height: gridView.cellHeight * 0.95 - project.height
- width: gridView.cellWidth * 0.9
+ height: homepageGridView.cellHeight * 0.95 - project.height
+ width: homepageGridView.cellWidth * 0.9
// Handle case where the file is missing
property bool fileExists: modelData["status"] != 0
@@ -408,7 +408,7 @@ Page {
BusyIndicator {
anchors.centerIn: parent
- running: gridView.visible && modelData["thumbnail"] && thumbnail.status != Image.Ready
+ running: homepageGridView.visible && modelData["thumbnail"] && thumbnail.status != Image.Ready
visible: running
}
diff --git a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml
index 0ccfc9724b..e8dff06a31 100644
--- a/meshroom/ui/qml/ImageGallery/ImageDelegate.qml
+++ b/meshroom/ui/qml/ImageGallery/ImageDelegate.qml
@@ -13,11 +13,15 @@ Item {
property variant viewpoint
property int cellID: -1
- property bool isCurrentItem: false
property alias source: _viewpoint.source
property alias metadata: _viewpoint.metadata
property bool readOnly: false
property bool displayViewId: false
+ property int layoutMode: 0 // 0: grid, 1: list
+
+ property variant parentModel
+ property int selectedIndex: parentModel ? parentModel.selectedIndex : -1
+ property bool isCurrentItem: cellID >= 0 && cellID === selectedIndex
signal pressed(var mouse)
signal removeRequest()
@@ -25,6 +29,10 @@ Item {
default property alias children: imageMA.children
+ // Internal properties to hold thumbnail source & loading status
+ property url thumbnailSource: ""
+ property int thumbnailStatus: Image.Null
+
// Retrieve viewpoints inner data
QtObject {
id: _viewpoint
@@ -37,7 +45,7 @@ Item {
// Update thumbnail location
// Can be called from the GridView when a new thumbnail has been written on disk
function updateThumbnail() {
- thumbnail.source = ThumbnailCache.thumbnail(root.source, root.cellID)
+ root.thumbnailSource = ThumbnailCache.thumbnail(root.source, root.cellID)
}
onSourceChanged: {
updateThumbnail()
@@ -49,7 +57,7 @@ Item {
interval: 5000
running: true
onTriggered: {
- if (thumbnail.status == Image.Null) {
+ if (root.thumbnailStatus == Image.Null) {
updateThumbnail()
}
}
@@ -109,64 +117,151 @@ Item {
}
}
- ColumnLayout {
+ // Switch from the grid component (column layout) to the list component (row layout)
+ Loader {
+ id: itemDelegate
anchors.fill: parent
- spacing: 0
-
- // Image thumbnail and background
- Rectangle {
- id: imageBackground
- color: Qt.darker(imageLabel.palette.base, 1.15)
- Layout.fillHeight: true
- Layout.fillWidth: true
- border.color: isCurrentItem ? imageLabel.palette.highlight : Qt.darker(imageLabel.palette.highlight)
- border.width: imageMA.containsMouse || root.isCurrentItem ? 2 : 0
- Image {
- id: thumbnail
- anchors.fill: parent
- anchors.margins: 4
- asynchronous: true
- autoTransform: true
- fillMode: Image.PreserveAspectFit
- smooth: false
- cache: false
+ sourceComponent: root.layoutMode === 0 ? gridDelegate : listDelegate
+ }
+
+ Component {
+ id: gridDelegate
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ // Image thumbnail and background
+ Rectangle {
+ color: Qt.darker(grid_imageLabel.palette.base, 1.15)
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ border.color: isCurrentItem ? grid_imageLabel.palette.highlight : Qt.darker(grid_imageLabel.palette.highlight)
+ border.width: imageMA.containsMouse || root.isCurrentItem ? 2 : 0
+ Image {
+ id: grid_thumbnail
+ anchors.fill: parent
+ anchors.margins: 4
+ source: root.thumbnailSource
+ asynchronous: true
+ autoTransform: true
+ fillMode: Image.PreserveAspectFit
+ smooth: false
+ cache: false
+ onStatusChanged: root.thumbnailStatus = status
+ }
+ BusyIndicator {
+ anchors.centerIn: parent
+ running: grid_thumbnail.status != Image.Ready
+ }
}
- BusyIndicator {
- anchors.centerIn: parent
- running: thumbnail.status != Image.Ready
+
+ // Image basename
+ Label {
+ id: grid_imageLabel
+ Layout.fillWidth: true
+ padding: 2
+ font.pointSize: 8
+ elide: Text.ElideMiddle
+ horizontalAlignment: Text.AlignHCenter
+ text: Filepath.basename(root.source)
+ background: Rectangle {
+ color: root.isCurrentItem ? parent.palette.highlight : "transparent"
+ }
}
- }
- // Image basename
- Label {
- id: imageLabel
- Layout.fillWidth: true
- padding: 2
- font.pointSize: 8
- elide: Text.ElideMiddle
- horizontalAlignment: Text.AlignHCenter
- text: Filepath.basename(root.source)
- background: Rectangle {
- color: root.isCurrentItem ? parent.palette.highlight : "transparent"
+ // Image viewId
+ Loader {
+ active: displayViewId
+ Layout.fillWidth: true
+ visible: active
+ sourceComponent: Label {
+ padding: grid_imageLabel.padding
+ font.pointSize: grid_imageLabel.font.pointSize
+ elide: grid_imageLabel.elide
+ horizontalAlignment: grid_imageLabel.horizontalAlignment
+ text: _viewpoint.viewId
+ background: Rectangle {
+ color: grid_imageLabel.background.color
+ }
+ }
}
}
+ }
- // Image viewId
- Loader {
- active: displayViewId
- Layout.fillWidth: true
- visible: active
- sourceComponent: Label {
- padding: imageLabel.padding
- font.pointSize: imageLabel.font.pointSize
- elide: imageLabel.elide
- horizontalAlignment: imageLabel.horizontalAlignment
- text: _viewpoint.viewId
- background: Rectangle {
- color: imageLabel.background.color
+ Component {
+ id: listDelegate
+ RowLayout {
+ anchors.fill: parent
+ spacing: 4
+
+ // Image thumbnail and background
+ Rectangle {
+ color: Qt.darker(list_imageLabel.palette.base, 1.15)
+ Layout.fillHeight: true
+ Layout.preferredWidth: 100
+
+ border.color: isCurrentItem ? list_imageLabel.palette.highlight : Qt.darker(list_imageLabel.palette.highlight)
+ border.width: imageMA.containsMouse || root.isCurrentItem ? 2 : 0
+
+ Image {
+ id: list_thumbnail
+ anchors.fill: parent
+ anchors.margins: 4
+ source: root.thumbnailSource
+ asynchronous: true
+ autoTransform: true
+ fillMode: Image.PreserveAspectFit
+ smooth: false
+ cache: false
+ onStatusChanged: root.thumbnailStatus = status
+ }
+ BusyIndicator {
+ anchors.centerIn: parent
+ running: list_thumbnail.status != Image.Ready
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 0
+
+ // Image basename
+ Label {
+ id: list_imageLabel
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ padding: 4
+ font.pointSize: 8
+ elide: Text.ElideMiddle
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: Text.AlignVCenter
+ text: Filepath.basename(root.source)
+ background: Rectangle {
+ color: root.isCurrentItem ? parent.palette.highlight : "transparent"
+ }
+ }
+
+ // Image viewId
+ Loader {
+ active: root.displayViewId
+ Layout.fillWidth: true
+ Layout.fillHeight: active
+ visible: active
+ sourceComponent: Label {
+ padding: list_imageLabel.padding
+ font.pointSize: list_imageLabel.font.pointSize
+ elide: list_imageLabel.elide
+ horizontalAlignment: list_imageLabel.horizontalAlignment
+ verticalAlignment: list_imageLabel.verticalAlignment
+ text: _viewpoint.viewId
+ background: Rectangle {
+ color: list_imageLabel.background.color
+ }
+ }
}
}
}
}
}
-}
+}
\ No newline at end of file
diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml
index 55ea7dad10..6d0efac575 100644
--- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml
+++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml
@@ -20,15 +20,23 @@ Panel {
property variant cameraInit
property int cameraInitIndex
property variant tempCameraInit
- readonly property alias currentItem: grid.currentItem
- readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : ""
- readonly property var currentItemMetadata: grid.currentItem ? grid.currentItem.metadata : undefined
+
+ readonly property var currentItem: layoutLoader.item ? layoutLoader.item.currentItem : null
+ readonly property string currentItemSource: currentItem ? currentItem.source : ""
+ readonly property var currentItemMetadata: currentItem ? currentItem.metadata : undefined
readonly property int centerViewId: (_currentScene && _currentScene.sfmTransform) ? parseInt(_currentScene.sfmTransform.attribute("transformation").value) : 0
- readonly property alias galleryGrid: grid
+ readonly property var galleryGrid: layoutLoader.item // This now references the loaded view (grid or list)
property int defaultCellSize: 160
property bool readOnly: false
+ enum LayoutModes {
+ Grid=0,
+ List=1
+ }
+
+ property int displayMode: ImageGallery.LayoutModes.Grid
+
property var filesByType: ({})
property int nbMeshroomScenes: 0
property int nbDraggedFiles: 0
@@ -123,6 +131,11 @@ Panel {
populate_model()
}
+ function toggleDisplayMode() {
+ displayMode = displayMode === ImageGallery.LayoutModes.Grid ?
+ ImageGallery.LayoutModes.List : ImageGallery.LayoutModes.Grid
+ }
+
headerBar: RowLayout {
SearchBar {
id: searchBar
@@ -131,6 +144,15 @@ Panel {
maxWidth: 150
}
+ MaterialToolButton {
+ text: root.displayMode === ImageGallery.LayoutModes.Grid ? MaterialIcons.view_list : MaterialIcons.view_module
+ font.pointSize: 11
+ padding: 2
+ ToolTip.text: "Switch the layout to " + root.displayMode === ImageGallery.LayoutModes.Grid ? "List" : "Grid"
+ ToolTip.visible: hovered
+ onClicked: root.toggleDisplayMode()
+ }
+
MaterialToolButton {
text: MaterialIcons.more_vert
font.pointSize: 11
@@ -168,346 +190,220 @@ Panel {
onUpdateIntrinsicsRequest: _currentScene.rebuildIntrinsics(cameraInit)
}
- ColumnLayout {
- anchors.fill: parent
- spacing: 4
+ SortFilterDelegateModel {
+ id: sortedModel
+ model: m.viewpoints
+ sortRole: "path.basename"
+ filters: displayViewIdsAction.checked ? filtersWithViewIds : filtersBasic
+ property var filtersBasic: [
+ {role: "path", value: searchBar.text},
+ {role: "viewId.isReconstructed", value: reconstructionFilter}
+ ]
+ property var filtersWithViewIds: [
+ [
+ {role: "path", value: searchBar.text},
+ {role: "viewId.asString", value: searchBar.text}
+ ],
+ {role: "viewId.isReconstructed", value: reconstructionFilter}
+ ]
+ property var reconstructionFilter: undefined
+
+ // Override modelData to return basename of viewpoint's path for sorting
+ function modelData(item, roleName_) {
+ var roleNameAndCmd = roleName_.split(".")
+ var roleName = roleName_
+ var cmd = ""
+ if (roleNameAndCmd.length >= 2) {
+ roleName = roleNameAndCmd[0]
+ cmd = roleNameAndCmd[1]
+ }
+ if (cmd === "isReconstructed")
+ return _currentScene.isReconstructed(item.model.object);
+
+ var value = item.model.object.childAttribute(roleName).value;
+ if (cmd === "basename")
+ return Filepath.basename(value);
+ if (cmd === "asString")
+ return value.toString();
+
+ return value
+ }
- GridView {
- id: grid
+ property int selectedIndex: -1
- Layout.fillWidth: true
- Layout.fillHeight: true
+ delegate: ImageDelegate {
+ id: imageDelegate
- visible: !intrinsicsFilterButton.checked
+ layoutMode: root.displayMode
+ viewpoint: object.value
+ cellID: DelegateModel.filteredIndex
+ width: layoutLoader.item ? (displayMode === ImageGallery.LayoutModes.List ? layoutLoader.item.width : layoutLoader.item.cellWidth) : 0
+ height: layoutLoader.item ? layoutLoader.item.cellHeight : 0
- ScrollBar.vertical: MScrollBar {
- active : !intrinsicsFilterButton.checked
- }
+ readOnly: m.readOnly
+ displayViewId: displayViewIdsAction.checked
+ visible: !intrinsicsFilterButton.checked
+
+ parentModel: sortedModel
- focus: true
- clip: true
- cellWidth: thumbnailSizeSlider.value
- cellHeight: cellWidth
- highlightFollowsCurrentItem: true
- keyNavigationEnabled: true
- property bool updateSelectedViewFromGrid: true
-
- // Update grid current item when selected view changes
- Connections {
- target: _currentScene
- function onSelectedViewIdChanged() {
- if (_currentScene.selectedViewId > -1) {
- grid.updateCurrentIndexFromSelectionViewId()
- }
+ onPressed: {
+ if (layoutLoader.item) {
+ layoutLoader.item.currentIndex = DelegateModel.filteredIndex
+ sortedModel.selectedIndex = DelegateModel.filteredIndex
}
}
- function makeCurrentItemVisible() {
- grid.positionViewAtIndex(grid.currentIndex, GridView.Visible)
- }
- function updateCurrentIndexFromSelectionViewId() {
- var idx = grid.model.find(_currentScene.selectedViewId, "viewId")
- if (idx >= 0 && grid.currentIndex !== idx) {
- grid.currentIndex = idx
- }
- }
- onCurrentItemChanged: {
- if (grid.updateSelectedViewFromGrid && grid.currentItem) {
- // If tempCameraInit is set and the first image in the GridView is selected, there has been a change of the CameraInit group and the viewId might be the same
- // Forcing the index to -1 before re-setting it will always cause a refresh on the Viewer2D's side, even if the viewId has not changed
- if (tempCameraInit !== null && grid.currentIndex == 0)
- _currentScene.selectedViewId = -1
- _currentScene.selectedViewId = grid.currentItem.viewpoint.get("viewId").value
- }
+ function sendRemoveRequest() {
+ if (readOnly)
+ return
+
+ root.removeImageRequest(object)
+
+ // If the last image has been removed, make sure the viewpoints and intrinsics are reset
+ if (m.viewpoints.count === 0)
+ root.allViewpointsCleared()
}
- // Update grid item when corresponding thumbnail is computed
- Connections {
- target: ThumbnailCache
- function onThumbnailCreated(imgSource, callerID) {
- let item = grid.itemAtIndex(callerID); // "item" is an ImageDelegate
- if (item && item.source === imgSource) {
- item.updateThumbnail()
- return
- }
- // Fallback in case the ImageDelegate cellID changed
- for (let idx = 0; idx < grid.count; idx++) {
- item = grid.itemAtIndex(idx)
- if (item && item.source === imgSource) {
- item.updateThumbnail()
- }
- }
- }
+ function removeAllImages() {
+ _currentScene.removeAllImages()
+ _currentScene.selectedViewId = "-1"
}
- model: SortFilterDelegateModel {
- id: sortedModel
- model: m.viewpoints
- sortRole: "path.basename"
- filters: displayViewIdsAction.checked ? filtersWithViewIds : filtersBasic
- property var filtersBasic: [
- {role: "path", value: searchBar.text},
- {role: "viewId.isReconstructed", value: reconstructionFilter}
- ]
- property var filtersWithViewIds: [
- [
- {role: "path", value: searchBar.text},
- {role: "viewId.asString", value: searchBar.text}
- ],
- {role: "viewId.isReconstructed", value: reconstructionFilter}
- ]
- property var reconstructionFilter: undefined
-
- // Override modelData to return basename of viewpoint's path for sorting
- function modelData(item, roleName_) {
- var roleNameAndCmd = roleName_.split(".")
- var roleName = roleName_
- var cmd = ""
- if (roleNameAndCmd.length >= 2) {
- roleName = roleNameAndCmd[0]
- cmd = roleNameAndCmd[1]
+ onRemoveRequest: sendRemoveRequest()
+ Keys.onPressed: function(event) {
+ if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) {
+ removeAllImages()
+ } else if (event.key === Qt.Key_Delete) {
+ sendRemoveRequest()
+ }
+ }
+ onRemoveAllImagesRequest: {
+ removeAllImages()
+ }
+
+ RowLayout {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: 2
+ spacing: 2
+
+ property bool valid: Qt.isQtObject(object) // object can be evaluated to null at some point during creation/deletion
+ property bool inViews: valid && _currentScene && _currentScene.sfmReport && _currentScene.isInViews(object)
+
+ // Camera Initialization indicator
+ IntrinsicsIndicator {
+ intrinsic: parent.valid && _currentScene ? _currentScene.getIntrinsic(object) : null
+ metadata: imageDelegate.metadata
+ }
+
+ // Rig indicator
+ Loader {
+ id: rigIndicator
+ property int rigId: parent.valid ? object.childAttribute("rigId").value : -1
+ active: rigId >= 0
+ sourceComponent: ImageBadge {
+ property int rigSubPoseId: model.object.childAttribute("subPoseId").value
+ text: MaterialIcons.link
+ ToolTip.text: "Rig: Initialized
" +
+ "Rig ID: " + rigIndicator.rigId + "
" +
+ "SubPose: " + rigSubPoseId
}
- if (cmd == "isReconstructed")
- return _currentScene.isReconstructed(item.model.object);
-
- var value = item.model.object.childAttribute(roleName).value;
- if (cmd == "basename")
- return Filepath.basename(value);
- if (cmd == "asString")
- return value.toString();
-
- return value
}
- delegate: ImageDelegate {
- id: imageDelegate
-
- viewpoint: object.value
- cellID: DelegateModel.filteredIndex
- width: grid.cellWidth
- height: grid.cellHeight
- readOnly: m.readOnly
- displayViewId: displayViewIdsAction.checked
- visible: !intrinsicsFilterButton.checked
-
- isCurrentItem: GridView.isCurrentItem
-
- onPressed: {
- grid.currentIndex = DelegateModel.filteredIndex
- }
-
- function sendRemoveRequest() {
- if (readOnly)
- return
-
- removeImageRequest(object)
-
- // If the last image has been removed, make sure the viewpoints and intrinsics are reset
- if (m.viewpoints.count === 0)
- allViewpointsCleared()
- }
-
- function removeAllImages() {
- _currentScene.removeAllImages()
- _currentScene.selectedViewId = "-1"
- }
-
- onRemoveRequest: sendRemoveRequest()
- Keys.onPressed: function(event) {
- if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) {
- removeAllImages()
- } else if (event.key === Qt.Key_Delete) {
- sendRemoveRequest()
- }
+ // Center of SfMTransform
+ Loader {
+ id: sfmTransformIndicator
+ active: viewpoint && (viewpoint.get("viewId").value === centerViewId)
+ sourceComponent: ImageBadge {
+ text: MaterialIcons.gamepad
+ ToolTip.text: "Camera used to define the center of the scene."
}
- onRemoveAllImagesRequest: {
- removeAllImages()
- }
-
- RowLayout {
- anchors.top: parent.top
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.margins: 2
- spacing: 2
-
- property bool valid: Qt.isQtObject(object) // object can be evaluated to null at some point during creation/deletion
- property bool inViews: valid && _currentScene && _currentScene.sfmReport && _currentScene.isInViews(object)
-
- // Camera Initialization indicator
- IntrinsicsIndicator {
- intrinsic: parent.valid && _currentScene ? _currentScene.getIntrinsic(object) : null
- metadata: imageDelegate.metadata
- }
-
- // Rig indicator
- Loader {
- id: rigIndicator
- property int rigId: parent.valid ? object.childAttribute("rigId").value : -1
- active: rigId >= 0
- sourceComponent: ImageBadge {
- property int rigSubPoseId: model.object.childAttribute("subPoseId").value
- text: MaterialIcons.link
- ToolTip.text: "Rig: Initialized
" +
- "Rig ID: " + rigIndicator.rigId + "
" +
- "SubPose: " + rigSubPoseId
- }
- }
+ }
- // Center of SfMTransform
- Loader {
- id: sfmTransformIndicator
- active: viewpoint && (viewpoint.get("viewId").value === centerViewId)
- sourceComponent: ImageBadge {
- text: MaterialIcons.gamepad
- ToolTip.text: "Camera used to define the center of the scene."
- }
- }
+ Item { Layout.fillWidth: true }
- Item { Layout.fillWidth: true }
-
- // Reconstruction status indicator
- Loader {
- active: parent.inViews
- visible: active
- sourceComponent: ImageBadge {
- property bool reconstructed: _currentScene.sfmReport && _currentScene.isReconstructed(model.object)
- text: reconstructed ? MaterialIcons.videocam : MaterialIcons.videocam_off
- color: reconstructed ? Colors.green : Colors.red
- ToolTip.text: "Camera: " + (reconstructed ? "" : "Not ") + "Reconstructed"
- }
- }
+ // Reconstruction status indicator
+ Loader {
+ active: parent.inViews
+ visible: active
+ sourceComponent: ImageBadge {
+ property bool reconstructed: _currentScene.sfmReport && _currentScene.isReconstructed(model.object)
+ text: reconstructed ? MaterialIcons.videocam : MaterialIcons.videocam_off
+ color: reconstructed ? Colors.green : Colors.red
+ ToolTip.text: "Camera: " + (reconstructed ? "" : "Not ") + "Reconstructed"
}
}
}
+ }
+ }
- // Keyboard shortcut to change current image group
- Keys.priority: Keys.BeforeItem
- Keys.onPressed: function(event) {
- if (event.modifiers & Qt.AltModifier) {
- if (event.key === Qt.Key_Right) {
- _currentScene.cameraInitIndex = Math.min(root.cameraInits.count - 1, root.cameraInitIndex + 1)
- event.accepted = true
- } else if (event.key === Qt.Key_Left) {
- _currentScene.cameraInitIndex = Math.max(0, root.cameraInitIndex - 1)
- event.accepted = true
- }
- } else {
- if (event.key === Qt.Key_Right) {
- grid.moveCurrentIndexRight()
- event.accepted = true
- } else if (event.key === Qt.Key_Left) {
- grid.moveCurrentIndexLeft()
- event.accepted = true
- } else if (event.key === Qt.Key_Up) {
- grid.moveCurrentIndexUp()
- event.accepted = true
- } else if (event.key === Qt.Key_Down) {
- grid.moveCurrentIndexDown()
- event.accepted = true
- } else if (event.key === Qt.Key_Tab) {
- searchBar.forceActiveFocus()
- event.accepted = true
- }
- }
- }
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 4
- // Explanatory placeholder when no image has been added yet
- Column {
- id: dropImagePlaceholder
- anchors.centerIn: parent
- visible: (m.viewpoints ? m.viewpoints.count === 0 : true) && !intrinsicsFilterButton.checked
- spacing: 4
- Label {
- anchors.horizontalCenter: parent.horizontalCenter
- text: MaterialIcons.photo_library
- font.pointSize: 24
- font.family: MaterialIcons.fontFamily
- }
- Label {
- text: "Drop Image Files / Folders"
- }
- }
- // Placeholder when the filtered images list is empty
- Column {
- id: noImageImagePlaceholder
- anchors.centerIn: parent
- visible: (m.viewpoints ? m.viewpoints.count !== 0 : false) && !dropImagePlaceholder.visible && grid.model.count === 0 && !intrinsicsFilterButton.checked
- spacing: 4
- Label {
- anchors.horizontalCenter: parent.horizontalCenter
- text: MaterialIcons.filter_none
- font.pointSize: 24
- font.family: MaterialIcons.fontFamily
- }
- Label {
- text: "No images in this filtered view"
+ Loader {
+ id: layoutLoader
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: !intrinsicsFilterButton.checked
+
+ sourceComponent: root.displayMode === ImageGallery.LayoutModes.Grid ? gridViewComponent : listViewComponent
+
+ onLoaded: {
+ if (item) {
+ // Pass necessary properties to the loaded component
+ item.m = m
+ item.gallery = root
+ item.searchBar = searchBar
+ item.intrinsicsFilterButton = intrinsicsFilterButton
+ item.tempCameraInit = tempCameraInit
+ item.errorDialog = errorDialog
+ item.sortedModel = sortedModel
+ 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)
+ item.currentIndex = sortedModel.selectedIndex
+
+ // Don't scroll yet because we must make sure the layout is loaded first
+ scrollTimer.restart()
}
}
+ }
- DropArea {
- id: dropArea
- anchors.fill: parent
- enabled: !m.readOnly && !intrinsicsFilterButton.checked
- keys: ["text/uri-list"]
- onEntered: function(drag) {
- nbDraggedFiles = drag.urls.length
- filesByType = _currentScene.getFilesByTypeFromDrop(drag.urls)
- nbMeshroomScenes = filesByType["meshroomScenes"].length
- }
- onDropped: function(drop) {
- if (nbMeshroomScenes == nbDraggedFiles || nbMeshroomScenes == 0) {
- root.filesDropped(filesByType)
- } else {
- errorDialog.open()
- }
- }
-
- // Background opacifier
- Rectangle {
- visible: dropArea.containsDrag
- anchors.fill: parent
- color: root.palette.window
- opacity: 0.8
- }
-
- Label {
- id: addArea
- anchors.fill: parent
- visible: dropArea.containsDrag
- Layout.fillWidth: true
- Layout.fillHeight: true
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- text: {
- if (nbMeshroomScenes != nbDraggedFiles && nbMeshroomScenes != 0) {
- return "Cannot Add Projects And Images Together"
- }
-
- if (nbMeshroomScenes == 1 && nbMeshroomScenes == nbDraggedFiles) {
- return "Load Project"
- } else if (nbMeshroomScenes == nbDraggedFiles) {
- return "Only One Project"
- } else {
- return "Add Images"
+ // Add a timer with a small delay so that we scroll after loading the layout
+ Timer {
+ id: scrollTimer
+ interval: 25
+ repeat: false
+ onTriggered: {
+ if (layoutLoader.item && _currentScene.selectedViewId > -1) {
+ layoutLoader.item.updateCurrentIndexFromSelectionViewId()
+ // Use another short delay for the actual scroll
+ Qt.callLater(function() {
+ if (layoutLoader.item && layoutLoader.item.currentIndex >= 0) {
+ layoutLoader.item.makeCurrentItemVisible()
}
- }
- font.bold: true
- background: Rectangle {
- color: dropArea.containsDrag ? parent.palette.highlight : parent.palette.window
- opacity: 0.8
- border.color: parent.palette.highlight
- }
+ })
}
}
+ }
+
+ Component {
+ id: gridViewComponent
+ ImageGridView {
+ id: gridView
+ }
+ }
- MouseArea {
- anchors.fill: parent
- onPressed: function(mouse) {
- if (mouse.button == Qt.LeftButton)
- grid.forceActiveFocus()
- mouse.accepted = false
- }
+ Component {
+ id: listViewComponent
+ ImageListView {
+ id: listView
}
}
@@ -585,12 +481,11 @@ Panel {
//CODE FOR HEADERS
//UNCOMMENT WHEN COMPATIBLE WITH THE RIGHT QT VERSION
-
-// HorizontalHeaderView {
-// id: horizontalHeader
-// syncView: tableView
-// anchors.left: tableView.left
-// }
+ // HorizontalHeaderView {
+ // id: horizontalHeader
+ // syncView: tableView
+ // anchors.left: tableView.left
+ // }
}
RowLayout {
@@ -680,7 +575,7 @@ Panel {
MaterialToolLabelButton {
id : inputImagesFilterButton
Layout.minimumWidth: childrenRect.width
- ToolTip.text: grid.model.count + " Input Images"
+ ToolTip.text: (layoutLoader.item && layoutLoader.item.model ? layoutLoader.item.model.count : 0) + " Input Images"
iconText: MaterialIcons.image
label: (m.viewpoints ? m.viewpoints.count : 0)
padding: 3
diff --git a/meshroom/ui/qml/ImageGallery/ImageGridView.qml b/meshroom/ui/qml/ImageGallery/ImageGridView.qml
new file mode 100644
index 0000000000..756393d2a1
--- /dev/null
+++ b/meshroom/ui/qml/ImageGallery/ImageGridView.qml
@@ -0,0 +1,224 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQml.Models
+import Qt.labs.qmlmodels
+
+import Controls 1.0
+import MaterialIcons 2.2
+import Utils 1.0
+
+GridView {
+ id: root
+
+ // Exposed properties from ImageGallery
+ property var m: null
+ property var gallery: null
+ property var searchBar: null
+ property var thumbnailSizeSlider: null
+ property var intrinsicsFilterButton: null
+ property var tempCameraInit: null
+ property var errorDialog: null
+ property var sortedModel: null
+
+ // Signals
+ signal removeImageRequest(var attribute)
+ signal allViewpointsCleared()
+
+ ScrollBar.vertical: MScrollBar {
+ active: true
+ }
+
+ focus: true
+ clip: true
+ cellWidth: thumbnailSizeSlider ? thumbnailSizeSlider.value : 160
+ cellHeight: cellWidth
+ highlightFollowsCurrentItem: true
+ keyNavigationEnabled: true
+
+ // Update grid current item when selected view changes
+ Connections {
+ target: _currentScene
+ function onSelectedViewIdChanged() {
+ if (_currentScene.selectedViewId > -1) {
+ root.updateCurrentIndexFromSelectionViewId()
+ }
+ }
+ }
+
+ function makeCurrentItemVisible() {
+ root.positionViewAtIndex(root.currentIndex, GridView.Visible)
+ }
+
+ function updateCurrentIndexFromSelectionViewId() {
+ if (!sortedModel) return
+ var idx = sortedModel.find(_currentScene.selectedViewId, "viewId")
+ if (idx >= 0 && root.currentIndex !== idx) {
+ root.currentIndex = idx
+ }
+ }
+
+ onCurrentItemChanged: {
+ if (root.currentItem) {
+ if (tempCameraInit !== null && root.currentIndex == 0)
+ _currentScene.selectedViewId = -1
+ _currentScene.selectedViewId = root.currentItem.viewpoint.get("viewId").value
+ }
+ }
+
+ // Update grid item when corresponding thumbnail is computed
+ Connections {
+ target: ThumbnailCache
+ function onThumbnailCreated(imgSource, callerID) {
+ let item = root.itemAtIndex(callerID);
+ if (item && item.source === imgSource) {
+ item.updateThumbnail()
+ return
+ }
+ for (let idx = 0; idx < root.count; idx++) {
+ item = root.itemAtIndex(idx)
+ if (item && item.source === imgSource) {
+ item.updateThumbnail()
+ }
+ }
+ }
+ }
+
+ model: sortedModel
+
+ // Keyboard shortcut to change current image group
+ Keys.priority: Keys.BeforeItem
+ Keys.onPressed: function(event) {
+ if (event.modifiers & Qt.AltModifier) {
+ if (event.key === Qt.Key_Right && gallery && gallery.cameraInits) {
+ _currentScene.cameraInitIndex = Math.min(gallery.cameraInits.count - 1, gallery.cameraInitIndex + 1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Left) {
+ _currentScene.cameraInitIndex = Math.max(0, gallery.cameraInitIndex - 1)
+ event.accepted = true
+ }
+ } else {
+ if (event.key === Qt.Key_Right) {
+ root.moveCurrentIndexRight()
+ event.accepted = true
+ } else if (event.key === Qt.Key_Left) {
+ root.moveCurrentIndexLeft()
+ event.accepted = true
+ } else if (event.key === Qt.Key_Up) {
+ root.moveCurrentIndexUp()
+ event.accepted = true
+ } else if (event.key === Qt.Key_Down) {
+ root.moveCurrentIndexDown()
+ event.accepted = true
+ } else if (event.key === Qt.Key_Tab) {
+ if (searchBar)
+ searchBar.forceActiveFocus()
+ event.accepted = true
+ }
+ }
+ }
+
+ // Explanatory placeholder when no image has been added yet
+ Column {
+ id: dropImagePlaceholder
+ anchors.centerIn: parent
+ visible: (m && m.viewpoints ? m.viewpoints.count === 0 : true) && (!intrinsicsFilterButton || !intrinsicsFilterButton.checked)
+ spacing: 4
+ Label {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: MaterialIcons.photo_library
+ font.pointSize: 24
+ font.family: MaterialIcons.fontFamily
+ }
+ Label {
+ text: "Drop Image Files / Folders"
+ }
+ }
+
+ // Placeholder when the filtered images list is empty
+ Column {
+ id: noImageImagePlaceholder
+ anchors.centerIn: parent
+ visible: (m && m.viewpoints ? m.viewpoints.count !== 0 : false) && !dropImagePlaceholder.visible && root.count === 0 && (!intrinsicsFilterButton || !intrinsicsFilterButton.checked)
+ spacing: 4
+ Label {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: MaterialIcons.filter_none
+ font.pointSize: 24
+ font.family: MaterialIcons.fontFamily
+ }
+ Label {
+ text: "No images in this filtered view"
+ }
+ }
+
+ DropArea {
+ id: dropArea
+ anchors.fill: parent
+ enabled: m && !m.readOnly && (!intrinsicsFilterButton || !intrinsicsFilterButton.checked)
+ keys: ["text/uri-list"]
+
+ property int nbDraggedFiles: 0
+ property var filesByType: ({})
+ property int nbMeshroomScenes: 0
+
+ onEntered: function(drag) {
+ nbDraggedFiles = drag.urls.length
+ filesByType = _currentScene.getFilesByTypeFromDrop(drag.urls)
+ nbMeshroomScenes = filesByType["meshroomScenes"].length
+ }
+ onDropped: function(drop) {
+ if (nbMeshroomScenes === nbDraggedFiles || nbMeshroomScenes === 0) {
+ if (gallery)
+ gallery.filesDropped(filesByType)
+ } else {
+ if (errorDialog)
+ errorDialog.open()
+ }
+ }
+
+ // Background opacifier
+ Rectangle {
+ visible: dropArea.containsDrag
+ anchors.fill: parent
+ color: gallery ? gallery.palette.window : palette.window
+ opacity: 0.8
+ }
+
+ Label {
+ id: addArea
+ anchors.fill: parent
+ visible: dropArea.containsDrag
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ text: {
+ if (dropArea.nbMeshroomScenes != dropArea.nbDraggedFiles && dropArea.nbMeshroomScenes != 0) {
+ return "Cannot Add Projects And Images Together"
+ }
+
+ if (dropArea.nbMeshroomScenes == 1 && dropArea.nbMeshroomScenes == dropArea.nbDraggedFiles) {
+ return "Load Project"
+ } else if (dropArea.nbMeshroomScenes == dropArea.nbDraggedFiles) {
+ return "Only One Project"
+ } else {
+ return "Add Images"
+ }
+ }
+ font.bold: true
+ background: Rectangle {
+ color: dropArea.containsDrag ? parent.palette.highlight : parent.palette.window
+ opacity: 0.8
+ border.color: parent.palette.highlight
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: function(mouse) {
+ if (mouse.button == Qt.LeftButton)
+ root.forceActiveFocus()
+ mouse.accepted = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/meshroom/ui/qml/ImageGallery/ImageListView.qml b/meshroom/ui/qml/ImageGallery/ImageListView.qml
new file mode 100644
index 0000000000..fa3379df89
--- /dev/null
+++ b/meshroom/ui/qml/ImageGallery/ImageListView.qml
@@ -0,0 +1,219 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQml.Models
+import Qt.labs.qmlmodels
+
+import Controls 1.0
+import MaterialIcons 2.2
+import Utils 1.0
+
+ListView {
+ id: root
+
+ // Exposed properties from ImageGallery
+ property var m: null
+ property var gallery: null
+ property var searchBar: null
+ property var thumbnailSizeSlider: null
+ property var intrinsicsFilterButton: null
+ property var tempCameraInit: null
+ property var errorDialog: null
+ property var sortedModel: null
+
+ property real cellHeight: thumbnailSizeSlider.value / 2
+
+ // Signals
+ signal removeImageRequest(var attribute)
+ signal allViewpointsCleared()
+
+ ScrollBar.vertical: MScrollBar {
+ active: true
+ }
+
+ focus: true
+ clip: true
+ spacing: 2
+ highlightFollowsCurrentItem: true
+ keyNavigationEnabled: true
+
+ // Update list current item when selected view changes
+ Connections {
+ target: _currentScene
+ function onSelectedViewIdChanged() {
+ if (_currentScene.selectedViewId > -1) {
+ root.updateCurrentIndexFromSelectionViewId()
+ }
+ }
+ }
+
+ function makeCurrentItemVisible() {
+ root.positionViewAtIndex(root.currentIndex, ListView.Visible)
+ }
+
+ function updateCurrentIndexFromSelectionViewId() {
+ if (!sortedModel) return
+ var idx = sortedModel.find(_currentScene.selectedViewId, "viewId")
+ if (idx >= 0 && root.currentIndex !== idx) {
+ root.currentIndex = idx
+ }
+ }
+
+ onCurrentItemChanged: {
+ if (root.currentItem) {
+ if (tempCameraInit !== null && root.currentIndex == 0)
+ _currentScene.selectedViewId = -1
+ _currentScene.selectedViewId = root.currentItem.viewpoint.get("viewId").value
+ }
+ }
+
+ // Update list item when corresponding thumbnail is computed
+ Connections {
+ target: ThumbnailCache
+ function onThumbnailCreated(imgSource, callerID) {
+ let item = root.itemAtIndex(callerID);
+ if (item && item.source === imgSource) {
+ item.updateThumbnail()
+ return
+ }
+ for (let idx = 0; idx < root.count; idx++) {
+ item = root.itemAtIndex(idx)
+ if (item && item.source === imgSource) {
+ item.updateThumbnail()
+ }
+ }
+ }
+ }
+
+ model: sortedModel
+
+ // Keyboard shortcut to change current image group
+ Keys.priority: Keys.BeforeItem
+ Keys.onPressed: function(event) {
+ if (event.modifiers & Qt.AltModifier) {
+ if (event.key === Qt.Key_Right && gallery && gallery.cameraInits) {
+ _currentScene.cameraInitIndex = Math.min(gallery.cameraInits.count - 1, gallery.cameraInitIndex + 1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Left) {
+ _currentScene.cameraInitIndex = Math.max(0, gallery.cameraInitIndex - 1)
+ event.accepted = true
+ }
+ } else {
+ if (event.key === Qt.Key_Down) {
+ root.incrementCurrentIndex()
+ event.accepted = true
+ } else if (event.key === Qt.Key_Up) {
+ root.decrementCurrentIndex()
+ event.accepted = true
+ } else if (event.key === Qt.Key_Tab) {
+ if (searchBar)
+ searchBar.forceActiveFocus()
+ event.accepted = true
+ }
+ }
+ }
+
+ // Explanatory placeholder when no image has been added yet
+ Column {
+ id: dropImagePlaceholder
+ anchors.centerIn: parent
+ visible: (m && m.viewpoints ? m.viewpoints.count === 0 : true) && (!intrinsicsFilterButton || !intrinsicsFilterButton.checked)
+ spacing: 4
+ Label {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: MaterialIcons.photo_library
+ font.pointSize: 24
+ font.family: MaterialIcons.fontFamily
+ }
+ Label {
+ text: "Drop Image Files / Folders"
+ }
+ }
+
+ // Placeholder when the filtered images list is empty
+ Column {
+ id: noImageImagePlaceholder
+ anchors.centerIn: parent
+ visible: (m && m.viewpoints ? m.viewpoints.count !== 0 : false) && !dropImagePlaceholder.visible && root.count === 0 && (!intrinsicsFilterButton || !intrinsicsFilterButton.checked)
+ spacing: 4
+ Label {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: MaterialIcons.filter_none
+ font.pointSize: 24
+ font.family: MaterialIcons.fontFamily
+ }
+ Label {
+ text: "No images in this filtered view"
+ }
+ }
+
+ DropArea {
+ id: dropArea
+ anchors.fill: parent
+ enabled: m && !m.readOnly && (!intrinsicsFilterButton || !intrinsicsFilterButton.checked)
+ keys: ["text/uri-list"]
+
+ property int nbDraggedFiles: 0
+ property var filesByType: ({})
+ property int nbMeshroomScenes: 0
+
+ onEntered: function(drag) {
+ nbDraggedFiles = drag.urls.length
+ filesByType = _currentScene.getFilesByTypeFromDrop(drag.urls)
+ nbMeshroomScenes = filesByType["meshroomScenes"].length
+ }
+ onDropped: function(drop) {
+ if (nbMeshroomScenes == nbDraggedFiles || nbMeshroomScenes == 0) {
+ if (gallery)
+ gallery.filesDropped(filesByType)
+ } else {
+ if (errorDialog)
+ errorDialog.open()
+ }
+ }
+
+ // Background opacifier
+ Rectangle {
+ visible: dropArea.containsDrag
+ anchors.fill: parent
+ color: gallery ? gallery.palette.window : palette.window
+ opacity: 0.8
+ }
+
+ Label {
+ id: addArea
+ anchors.fill: parent
+ visible: dropArea.containsDrag
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ text: {
+ if (dropArea.nbMeshroomScenes != dropArea.nbDraggedFiles && dropArea.nbMeshroomScenes != 0) {
+ return "Cannot Add Projects And Images Together"
+ }
+
+ if (dropArea.nbMeshroomScenes == 1 && dropArea.nbMeshroomScenes == dropArea.nbDraggedFiles) {
+ return "Load Project"
+ } else if (dropArea.nbMeshroomScenes == dropArea.nbDraggedFiles) {
+ return "Only One Project"
+ } else {
+ return "Add Images"
+ }
+ }
+ font.bold: true
+ background: Rectangle {
+ color: dropArea.containsDrag ? parent.palette.highlight : parent.palette.window
+ opacity: 0.8
+ border.color: parent.palette.highlight
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: function(mouse) {
+ if (mouse.button == Qt.LeftButton)
+ root.forceActiveFocus()
+ mouse.accepted = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/meshroom/ui/qml/ImageGallery/qmldir b/meshroom/ui/qml/ImageGallery/qmldir
index 5132661071..4f846442a5 100644
--- a/meshroom/ui/qml/ImageGallery/qmldir
+++ b/meshroom/ui/qml/ImageGallery/qmldir
@@ -2,6 +2,9 @@ module ImageGallery
ImageGallery 1.0 ImageGallery.qml
ImageDelegate 1.0 ImageDelegate.qml
+ImageGridView 1.0 ImageGridView.qml
+ImageListView 1.0 ImageListView.qml
+
ImageIntrinsicDelegate 1.0 ImageIntrinsicDelegate.qml
ImageIntrinsicViewer 1.0 ImageIntrinsicViewer.qml
IntrinsicDisplayDelegate 1.0 IntrinsicDisplayDelegate.qml
diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml
index 12431d931e..e0d7fb2358 100644
--- a/meshroom/ui/qml/WorkspaceView.qml
+++ b/meshroom/ui/qml/WorkspaceView.qml
@@ -81,7 +81,6 @@ Item {
cameraInitIndex: currentScene ? currentScene.cameraInitIndex : -1
onRemoveImageRequest: function(attribute) { currentScene.removeImage(attribute) }
onAllViewpointsCleared: currentScene.selectedViewId = "-1"
- galleryGrid.currentIndex: 0
onFilesDropped: function(drop) {
if (drop["meshroomScenes"].length == 1) {
ensureSaved(function() {