Skip to content

Commit cf189fd

Browse files
authored
Merge pull request #2637 from alicevision/dev/optimRecentProjects
[ui] Refactor the access to the list of recent project files
2 parents e51bb93 + 59afac3 commit cf189fd

File tree

2 files changed

+125
-47
lines changed

2 files changed

+125
-47
lines changed

meshroom/ui/app.py

Lines changed: 119 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from PySide6 import __version__ as PySideVersion
88
from PySide6 import QtCore
9-
from PySide6.QtCore import Qt, QUrl, QJsonValue, qInstallMessageHandler, QtMsgType, QSettings
9+
from PySide6.QtCore import QUrl, QJsonValue, qInstallMessageHandler, QtMsgType, QSettings
1010
from PySide6.QtGui import QIcon
1111
from PySide6.QtQml import QQmlDebuggingEnabler
1212
from PySide6.QtQuickControls2 import QQuickStyle
@@ -243,6 +243,13 @@ def __init__(self, inputArgs):
243243
meshroom.core.initNodes()
244244
meshroom.core.initSubmitters()
245245

246+
# Initialize the list of recent project files
247+
self._recentProjectFiles = self._getRecentProjectFilesFromSettings()
248+
# Flag set to True if, for all the project files in the list, thumbnails have been retrieved when they
249+
# are available. If set to False, then all the paths in the list are accurate, but some thumbnails might
250+
# be retrievable
251+
self._updatedRecentProjectFilesThumbnails = True
252+
246253
# QML engine setup
247254
qmlDir = os.path.join(pwd, "qml")
248255
url = os.path.join(qmlDir, "main.qml")
@@ -347,38 +354,92 @@ def reloadTemplateList(self):
347354
meshroom.core.initPipelines()
348355
self.pipelineTemplateFilesChanged.emit()
349356

350-
def _recentProjectFiles(self):
357+
def _retrieveThumbnailPath(self, filepath: str) -> str:
358+
"""
359+
Given the path of a project file, load this file and try to retrieve the path to its thumbnail, i.e. its
360+
first viewpoint image.
361+
362+
Args:
363+
filepath: the path of the project file to retrieve the thumbnail from
364+
365+
Returns:
366+
The path to the thumbnail if it could be found, an empty string otherwise
367+
"""
368+
try:
369+
with open(filepath) as file:
370+
fileData = json.load(file)
371+
372+
graphData = fileData.get("graph", {})
373+
for node in graphData.values():
374+
if node.get("nodeType") != "CameraInit":
375+
continue
376+
if viewpoints := node.get("inputs", {}).get("viewpoints"):
377+
return viewpoints[0].get("path", "")
378+
379+
except FileNotFoundError:
380+
logging.info("File {} not found on disk.".format(filepath))
381+
except (json.JSONDecodeError, UnicodeDecodeError):
382+
logging.info("Error while loading file {}.".format(filepath))
383+
except KeyError as err:
384+
logging.info("The following key does not exist: {}".format(str(err)))
385+
except Exception as err:
386+
logging.info("Exception: {}".format(str(err)))
387+
388+
return ""
389+
390+
def _getRecentProjectFilesFromSettings(self) -> list[dict[str, str]]:
391+
"""
392+
Read the list of recent project files from the QSettings, retrieve their filepath, and if it exists, their
393+
thumbnail.
394+
395+
Returns:
396+
The list containing dictionaries of the form {"path": "/path/to/project/file", "thumbnail":
397+
"/path/to/thumbnail"} based on the recent projects stored in the QSettings.
398+
"""
351399
projects = []
352400
settings = QSettings()
353401
settings.beginGroup("RecentFiles")
354402
size = settings.beginReadArray("Projects")
355403
for i in range(size):
356404
settings.setArrayIndex(i)
357-
p = settings.value("filepath")
358-
thumbnail = ""
359-
if p:
360-
# get the first image path from the project
361-
try:
362-
with open(p) as file:
363-
file = json.load(file)
364-
# find the first camerainit node
365-
file = file["graph"]
366-
for node in file:
367-
if file[node]["nodeType"] == "CameraInit" and file[node]["inputs"].get("viewpoints"):
368-
if len(file[node]["inputs"]["viewpoints"]) > 0:
369-
thumbnail = file[node]["inputs"]["viewpoints"][0]["path"]
370-
break
371-
except FileNotFoundError:
372-
pass
373-
p = {"path": p, "thumbnail": thumbnail}
405+
path = settings.value("filepath")
406+
if path:
407+
p = {"path": path, "thumbnail": self._retrieveThumbnailPath(path)}
374408
projects.append(p)
375409
settings.endArray()
376410
settings.endGroup()
377411
return projects
378412

413+
@Slot()
414+
def updateRecentProjectFilesThumbnails(self) -> None:
415+
"""
416+
If there are thumbnails that may be retrievable (meaning the list of projects has been updated minimally),
417+
update the list of recent project files by reading the QSettings and retrieving the thumbnails if they are
418+
available.
419+
"""
420+
if not self._updatedRecentProjectFilesThumbnails:
421+
self._updateRecentProjectFilesThumbnails()
422+
self._updatedRecentProjectFilesThumbnails = True
423+
424+
def _updateRecentProjectFilesThumbnails(self) -> None:
425+
for project in self._recentProjectFiles:
426+
path = project["path"]
427+
project["thumbnail"] = self._retrieveThumbnailPath(path)
428+
379429
@Slot(str)
380430
@Slot(QUrl)
381-
def addRecentProjectFile(self, projectFile):
431+
def addRecentProjectFile(self, projectFile) -> None:
432+
"""
433+
Add a project file to the list of recent project files.
434+
The function ensures that the file is not present more than once in the list and trims it so it
435+
never exceeds a set number of projects.
436+
QSettings are updated accordingly.
437+
The update of the list of recent projects files is minimal: the filepath is added, but there is no
438+
attempt to retrieve its corresponding thumbnail.
439+
440+
Args:
441+
projectFile (str or QUrl): path to the project file to add to the list
442+
"""
382443
if not isinstance(projectFile, (QUrl, str)):
383444
raise TypeError("Unexpected data type: {}".format(projectFile.__class__))
384445
if isinstance(projectFile, QUrl):
@@ -390,37 +451,47 @@ def addRecentProjectFile(self, projectFile):
390451
if not projectFileNorm:
391452
projectFileNorm = QUrl.fromLocalFile(projectFile).toLocalFile()
392453

393-
projects = self._recentProjectFiles()
394-
projects = [p["path"] for p in projects]
454+
# Get the list of recent projects without re-reading the QSettings
455+
projects = self._recentProjectFiles
395456

396-
# remove duplicates while preserving order
397-
from collections import OrderedDict
398-
uniqueProjects = OrderedDict.fromkeys(projects)
399-
projects = list(uniqueProjects)
400-
# remove previous usage of the value
401-
if projectFileNorm in uniqueProjects:
402-
projects.remove(projectFileNorm)
403-
# add the new value in the first place
404-
projects.insert(0, projectFileNorm)
457+
# Checks whether the path is already in the list of recent projects
458+
filepaths = [p["path"] for p in projects]
459+
if projectFileNorm in filepaths:
460+
idx = filepaths.index(projectFileNorm)
461+
del projects[idx] # If so, delete its entry
462+
463+
# Insert the newest entry at the top of the list
464+
projects.insert(0, {"path": projectFileNorm, "thumbnail": ""})
405465

406-
# keep only the 40 first elements
407-
projects = projects[0:40]
466+
# Only keep the first 40 projects
467+
maxNbProjects = 40
468+
if len(projects) > maxNbProjects:
469+
projects = projects[0:maxNbProjects]
408470

471+
# Update the general settings
409472
settings = QSettings()
410473
settings.beginGroup("RecentFiles")
411474
settings.beginWriteArray("Projects")
412475
for i, p in enumerate(projects):
413476
settings.setArrayIndex(i)
414-
settings.setValue("filepath", p)
477+
settings.setValue("filepath", p["path"])
415478
settings.endArray()
416479
settings.endGroup()
417480
settings.sync()
418481

482+
# Update the final list of recent projects
483+
self._recentProjectFiles = projects
484+
self._updatedRecentProjectFilesThumbnails = False # Thumbnails may not be up-to-date
419485
self.recentProjectFilesChanged.emit()
420486

421487
@Slot(str)
422488
@Slot(QUrl)
423-
def removeRecentProjectFile(self, projectFile):
489+
def removeRecentProjectFile(self, projectFile) -> None:
490+
"""
491+
Remove a given project file from the list of recent project files.
492+
If the provided filepath is not already present in the list of recent project files, nothing is done.
493+
Otherwise, it is effectively removed and the QSettings are updated accordingly.
494+
"""
424495
if not isinstance(projectFile, (QUrl, str)):
425496
raise TypeError("Unexpected data type: {}".format(projectFile.__class__))
426497
if isinstance(projectFile, QUrl):
@@ -432,28 +503,30 @@ def removeRecentProjectFile(self, projectFile):
432503
if not projectFileNorm:
433504
projectFileNorm = QUrl.fromLocalFile(projectFile).toLocalFile()
434505

435-
projects = self._recentProjectFiles()
436-
projects = [p["path"] for p in projects]
506+
# Get the list of recent projects without re-reading the QSettings
507+
projects = self._recentProjectFiles
437508

438-
# remove duplicates while preserving order
439-
from collections import OrderedDict
440-
uniqueProjects = OrderedDict.fromkeys(projects)
441-
projects = list(uniqueProjects)
442-
# remove previous usage of the value
443-
if projectFileNorm not in uniqueProjects:
509+
# Ensure the filepath is in the list of recent projects
510+
filepaths = [p["path"] for p in projects]
511+
if projectFileNorm not in filepaths:
444512
return
445513

446-
projects.remove(projectFileNorm)
514+
# Delete it from the list
515+
idx = filepaths.index(projectFileNorm)
516+
del projects[idx]
447517

518+
# Update the general settings
448519
settings = QSettings()
449520
settings.beginGroup("RecentFiles")
450521
settings.beginWriteArray("Projects")
451522
for i, p in enumerate(projects):
452523
settings.setArrayIndex(i)
453-
settings.setValue("filepath", p)
524+
settings.setValue("filepath", p["path"])
454525
settings.endArray()
455526
settings.sync()
456527

528+
# Update the final list of recent projects
529+
self._recentProjectFiles = projects
457530
self.recentProjectFilesChanged.emit()
458531

459532
def _recentImportedImagesFolders(self):
@@ -620,7 +693,7 @@ def _defaultSequencePlayerEnabled(self):
620693
recentImportedImagesFoldersChanged = Signal()
621694
pipelineTemplateFiles = Property("QVariantList", _pipelineTemplateFiles, notify=pipelineTemplateFilesChanged)
622695
pipelineTemplateNames = Property("QVariantList", _pipelineTemplateNames, notify=pipelineTemplateFilesChanged)
623-
recentProjectFiles = Property("QVariantList", _recentProjectFiles, notify=recentProjectFilesChanged)
696+
recentProjectFiles = Property("QVariantList", lambda self: self._recentProjectFiles, notify=recentProjectFilesChanged)
624697
recentImportedImagesFolders = Property("QVariantList", _recentImportedImagesFolders, notify=recentImportedImagesFoldersChanged)
625698
default8bitViewerEnabled = Property(bool, _default8bitViewerEnabled, constant=True)
626699
defaultSequencePlayerEnabled = Property(bool, _defaultSequencePlayerEnabled, constant=True)

meshroom/ui/qml/Homepage.qml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,12 @@ Page {
309309
cellHeight: cellWidth
310310
anchors.margins: 10
311311

312-
model: [{ "path": null, "thumbnail": null}].concat(MeshroomApp.recentProjectFiles)
312+
model: {
313+
// Request latest thumbnail paths
314+
if (mainStack.currentItem instanceof Homepage)
315+
MeshroomApp.updateRecentProjectFilesThumbnails()
316+
return [{"path": null, "thumbnail": null}].concat(MeshroomApp.recentProjectFiles)
317+
}
313318

314319
// Update grid item when corresponding thumbnail is computed
315320
Connections {

0 commit comments

Comments
 (0)