Skip to content
Open
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
113 changes: 93 additions & 20 deletions meshroom/ui/qml/Homepage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,28 @@ import Controls 1.0
Page {
id: root

property bool isLoading: false

// Schedule an action to run after the current frame has been rendered and
// displayed, so that visual feedback (highlight, spinner) is visible before
// the UI thread is blocked by a heavy synchronous operation.
// Uses _window (the ApplicationWindow id from main.qml) since root.window
// is not reliably available for Page items inside a StackView.
function executeAfterFrameRendered(action) {
function onFrame() {
_window.frameSwapped.disconnect(onFrame)
action()
}
_window.frameSwapped.connect(onFrame)
}

onVisibleChanged: {
logo.playing = false
if (visible) {
logo.playing = true
isLoading = false
homepageGridView.loadingIndex = -1
pipelinesListView.loadingIndex = -1
}
}

Expand Down Expand Up @@ -242,6 +260,8 @@ Page {
id: pipelinesListView
visible: tabPanel.currentTab === 0

property int loadingIndex: -1

anchors.fill: parent
anchors.margins: 10

Expand All @@ -251,20 +271,44 @@ Page {
id: pipelineDelegate
padding: 10
width: pipelinesListView.width
enabled: !root.isLoading || index === pipelinesListView.loadingIndex
opacity: (!root.isLoading || index === pipelinesListView.loadingIndex) ? 1.0 : 0.4
Comment on lines +274 to +275
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled: !root.isLoading || index === pipelinesListView.loadingIndex leaves the selected pipeline button enabled while root.isLoading is true. This allows repeated clicks on the same item during the short window before the heavy operation starts, potentially scheduling multiple executeAfterFrameRendered callbacks and pushing multiple Application pages / calling _currentScene.new multiple times. Consider disabling all pipeline delegates while loading (or at least adding a re-entrancy guard so subsequent clicks are ignored).

Copilot uses AI. Check for mistakes.

contentItem: RowLayout {
Label {
id: pipeline
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
text: modelData["name"]
}
BusyIndicator {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
running: index === pipelinesListView.loadingIndex && root.isLoading
visible: running
}
}

contentItem: Label {
id: pipeline
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
text: modelData["name"]
// Highlight overlay shown when this pipeline is being loaded
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: palette.highlight
border.width: 2
visible: root.isLoading && index === pipelinesListView.loadingIndex
}

Connections {
target: pipelineDelegate
function onClicked() {
// Open pipeline
mainStack.push("Application.qml")
_currentScene.new(modelData["path"])
root.isLoading = true
pipelinesListView.loadingIndex = index
let path = modelData["path"]
root.executeAfterFrameRendered(function() {
Comment on lines 304 to +308
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeAfterFrameRendered connects to _window.frameSwapped every time it’s called. Since onClicked for pipelines doesn’t guard against root.isLoading, repeated clicks can create multiple pending connections, and all of them will fire on the next frame swap. Add an early return when root.isLoading is already true (or ensure only one pending callback can be registered at a time).

Copilot uses AI. Check for mistakes.
mainStack.push("Application.qml")
_currentScene.new(path)
})
}
}
}
Expand All @@ -276,6 +320,8 @@ Page {
anchors.fill: parent
anchors.topMargin: cellHeight * 0.1

property int loadingIndex: -1

cellWidth: 195
cellHeight: cellWidth
anchors.margins: 10
Expand Down Expand Up @@ -311,6 +357,7 @@ Page {

width: homepageGridView.cellWidth
height: homepageGridView.cellHeight
opacity: (!root.isLoading || index === homepageGridView.loadingIndex) ? 1.0 : 0.4

property var source: modelData["thumbnail"] ? Filepath.stringToUrl(modelData["thumbnail"]) : ""
property int retryCount: 0
Expand Down Expand Up @@ -387,6 +434,8 @@ Page {

onClicked: function(mouse) {

if (root.isLoading) return

if (mouse.button === Qt.RightButton) {

if (!modelData["path"]) { return }
Expand All @@ -403,11 +452,18 @@ Page {
}

else {
// Open project
mainStack.push("Application.qml")
if (_currentScene.load(modelData["path"])) {
MeshroomApp.addRecentProjectFile(modelData["path"])
}
root.isLoading = true
homepageGridView.loadingIndex = index
let path = modelData["path"]
root.executeAfterFrameRendered(function() {
mainStack.push("Application.qml")
if (_currentScene.load(path)) {
MeshroomApp.addRecentProjectFile(path)
} else {
root.isLoading = false
homepageGridView.loadingIndex = -1
}
})
Comment on lines +455 to +466
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project open logic (set isLoading, set loadingIndex, defer to executeAfterFrameRendered, push Application, attempt _currentScene.load, then reset on failure) is duplicated in both the left-click handler and the context menu "Open" action. Consider extracting this into a single helper function (e.g., openProjectAtIndex(path, index)) to avoid the two paths drifting and to keep future changes (like resetting state, error handling, or navigation) consistent.

Copilot uses AI. Check for mistakes.
}
}
}
Expand All @@ -416,13 +472,21 @@ Page {
id: projectContextMenu

MenuItem {
enabled: projectDelegate.fileExists
enabled: projectDelegate.fileExists && !root.isLoading
text: "Open"
onTriggered: {
if (_currentScene.load(modelData["path"])) {
onTriggered: {
root.isLoading = true
homepageGridView.loadingIndex = index
let path = modelData["path"]
root.executeAfterFrameRendered(function() {
mainStack.push("Application.qml")
MeshroomApp.addRecentProjectFile(modelData["path"])
}
if (_currentScene.load(path)) {
MeshroomApp.addRecentProjectFile(path)
} else {
root.isLoading = false
homepageGridView.loadingIndex = -1
}
})
}
}

Expand Down Expand Up @@ -462,8 +526,17 @@ Page {

BusyIndicator {
anchors.centerIn: parent
running: homepageGridView.visible && projectContent.thumbnailBusy
visible: homepageGridView.visible && projectContent.thumbnailBusy
running: (homepageGridView.visible && projectContent.thumbnailBusy) || (root.isLoading && index === homepageGridView.loadingIndex)
visible: running
}

// Highlight overlay shown when this project is being loaded
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: palette.highlight
border.width: 2
visible: root.isLoading && index === homepageGridView.loadingIndex
}
}
Label {
Expand Down
Loading