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() {