Monitor only the active project file

Watching referenced and inactive projects can produce reload prompts outside the current document. Register watches only when a project becomes active, and retain content baselines so unchanged writes and application saves stay quiet.

Debounce file and directory events, retry transient read failures, and restore monitoring after replacement or deletion and recreation. Validate reload ownership around the dialog and scope candidate projects so failed reloads release their resources. Check read errors before clearing existing model data.
This commit is contained in:
Leonardo Zide committed 2026-10-08 10:12:14 -07:00
1 parent b3c325dbc3
commit 7e9283fec0
5 files changed
+167 -49

No files matched your search

+1
View File
@@ -296,6 +296,7 @@ void lcApplication::SetProject(Project* Project)
delete mProject;
mProject = Project;
mProject->UpdateFileWatcher();
if (gMainWindow)
gMainWindow->ScheduleAssetUpdate();
+26 -26
View File
@@ -1301,47 +1301,47 @@ void lcMainWindow::ColorButtonClicked()
void lcMainWindow::ProjectFileChanged(const QString& Path)
{
static bool Ignore;
Project* CurrentProject = lcGetActiveProject();
if (Ignore)
if (Ignore || !CurrentProject || Path.isEmpty() || QFileInfo(Path) != QFileInfo(CurrentProject->GetFileName()))
return;
QString Text = tr("The file '%1' has been modified by another application, do you want to reload it?").arg(QDir::toNativeSeparators(Path));
Project* CurrentProject = lcGetActiveProject();
const QString CurrentFileName = CurrentProject->GetFileName();
Ignore = true;
if (QMessageBox::question(this, tr("File Changed"), Text, QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
{
Ignore = false;
CurrentProject->MarkAsModified();
UpdateTitle();
if (CurrentProject == lcGetActiveProject() && CurrentFileName == CurrentProject->GetFileName())
{
CurrentProject->MarkAsModified();
UpdateTitle();
}
return;
}
if (CurrentProject != lcGetActiveProject() || CurrentFileName != CurrentProject->GetFileName())
{
Ignore = false;
return;
}
std::unique_ptr<Project> NewProject = std::make_unique<Project>();
if (NewProject->Load(Path, true))
{
QByteArray TabLayout = GetTabLayout();
gApplication->SetProject(NewProject.release());
RestoreTabLayout(TabLayout);
lcView::UpdateAllViews();
}
Ignore = false;
QFileInfo FileInfo(Path);
if (FileInfo == QFileInfo(CurrentProject->GetFileName()))
{
Project* NewProject = new Project;
if (NewProject->Load(Path, true))
{
QByteArray TabLayout = GetTabLayout();
gApplication->SetProject(NewProject);
RestoreTabLayout(TabLayout);
lcView::UpdateAllViews();
}
}
else
{
PieceInfo* Info = lcGetPiecesLibrary()->FindPiece(FileInfo.fileName().toLatin1(), CurrentProject, false, true);
if (Info && Info->IsProject())
Info->GetProject()->Load(Path, true);
}
}
void lcMainWindow::Print(QPrinter* Printer)
+108 -22
View File
@@ -75,12 +75,20 @@ Project::Project(bool IsPreview)
mActiveModel->SetSaved();
mModels.emplace_back(mActiveModel);
if (!mIsPreview && gMainWindow)
QObject::connect(&mFileWatcher, &QFileSystemWatcher::fileChanged, gMainWindow, [this](const QString& Path)
if (!mIsPreview)
{
mFileCheckTimer.setSingleShot(true);
mFileCheckTimer.setInterval(150);
QObject::connect(&mFileWatcher, &QFileSystemWatcher::fileChanged, &mFileWatcher, [this]() { mFileCheckTimer.start(); });
QObject::connect(&mFileWatcher, &QFileSystemWatcher::directoryChanged, &mFileWatcher, [this]() { mFileCheckTimer.start(); });
QObject::connect(&mFileCheckTimer, &QTimer::timeout, &mFileWatcher, [this]()
{
if (HasFileChanged(Path))
const QString Path = mFileName;
if (HasFileChanged(Path) && gMainWindow)
gMainWindow->ProjectFileChanged(Path);
});
}
}
Project::~Project()
@@ -575,48 +583,107 @@ void Project::ShowModelListDialog()
Model->RefreshPreview();
}
QByteArray Project::ReadFileHash(const QString& FileName)
Project::FileState Project::ReadFileState(const QString& FileName)
{
FileState State;
State.Exists = QFileInfo(FileName).isFile();
if (!State.Exists)
return State;
QFile File(FileName);
QCryptographicHash Hash(QCryptographicHash::Sha256);
State.Readable = File.open(QIODevice::ReadOnly) && Hash.addData(&File);
if (!File.open(QIODevice::ReadOnly) || !Hash.addData(&File))
return QByteArray();
if (State.Readable)
State.Hash = Hash.result();
return Hash.result();
return State;
}
bool Project::HasFileChanged(const QString& Path)
{
if (Path != mFileName)
if (mIsPreview || this != lcGetActiveProject() || Path.isEmpty() || Path != mFileName)
return false;
// Atomic replacements remove the old watch, even when the contents are unchanged.
if (!mFileWatcher.files().contains(Path) && QFileInfo::exists(Path))
mFileWatcher.addPath(Path);
UpdateFileWatcher();
const FileState State = ReadFileState(Path);
const QByteArray FileHash = ReadFileHash(Path);
if (State.Exists && !State.Readable && ++mFileReadAttempts < 3)
{
mFileCheckTimer.start();
return false;
}
if (FileHash.isEmpty() || FileHash == mFileHash)
mFileReadAttempts = 0;
if (State.Hash == mFileHash && State.Exists == mFileExists && State.Readable == mFileReadable)
return false;
// Remember the observed contents so duplicate events do not prompt again.
mFileHash = FileHash;
mFileHash = State.Hash;
mFileExists = State.Exists;
mFileReadable = State.Readable;
return true;
}
void Project::UpdateFileWatcher()
{
QStringList Files;
QStringList Directories;
const bool Active = !mIsPreview && this == lcGetActiveProject() && !mFileName.isEmpty();
const bool WasWatching = !mFileWatcher.files().isEmpty() || !mFileWatcher.directories().isEmpty();
if (Active)
{
if (QFileInfo(mFileName).isFile())
Files.push_back(mFileName);
QDir Directory(QFileInfo(mFileName).absolutePath());
while (!Directory.exists())
{
const QString Parent = QFileInfo(Directory.absolutePath()).absolutePath();
if (Parent == Directory.absolutePath())
break;
Directory.setPath(Parent);
}
if (Directory.exists())
Directories.push_back(Directory.absolutePath());
}
for (const QString& Path : mFileWatcher.files())
if (!Files.contains(Path))
mFileWatcher.removePath(Path);
for (const QString& Path : mFileWatcher.directories())
if (!Directories.contains(Path))
mFileWatcher.removePath(Path);
for (const QString& Path : Files)
if (!mFileWatcher.files().contains(Path))
mFileWatcher.addPath(Path);
for (const QString& Path : Directories)
if (!mFileWatcher.directories().contains(Path))
mFileWatcher.addPath(Path);
if (!Active)
mFileCheckTimer.stop();
else if (!WasWatching)
mFileCheckTimer.start();
}
void Project::SetFileName(const QString& FileName)
{
if (mFileName == FileName)
return;
if (!mIsPreview && !mFileName.isEmpty())
mFileWatcher.removePath(mFileName);
if (!mIsPreview && !FileName.isEmpty())
mFileWatcher.addPath(FileName);
mFileName = FileName;
UpdateFileWatcher();
}
bool Project::Load(const QString& FileName, bool ShowErrors)
@@ -634,15 +701,28 @@ bool Project::Load(const QString& FileName, bool ShowErrors)
return false;
}
QByteArray FileData = File.readAll();
if (File.error() != QFileDevice::NoError)
{
if (ShowErrors)
QMessageBox::warning(parent, tr("Error"), tr("Error reading file '%1':\n%2").arg(FileName, File.errorString()));
return false;
}
mModels.clear();
lcGetPiecesLibrary()->ReleaseProjectPieces(this);
SetFileName(FileName);
QFileInfo FileInfo(FileName);
QString Extension = FileInfo.suffix().toLower();
QByteArray FileData = File.readAll();
if (!mIsPreview)
{
mFileHash = QCryptographicHash::hash(FileData, QCryptographicHash::Sha256);
mFileExists = true;
mFileReadable = true;
mFileReadAttempts = 0;
}
bool LoadDAT;
@@ -825,7 +905,13 @@ lcResult<void> Project::Save(const QString& FileName)
}
if (!mIsPreview)
mFileHash = ReadFileHash(FileName);
{
const FileState State = ReadFileState(FileName);
mFileHash = State.Hash;
mFileExists = State.Exists;
mFileReadable = State.Readable;
mFileReadAttempts = 0;
}
SetFileName(FileName);
+12 -1
View File
@@ -102,6 +102,7 @@ public:
}
QString GetImageFileName(bool AllowCurrentFolder) const;
void UpdateFileWatcher();
lcInstructions* GetInstructions();
@@ -137,7 +138,13 @@ public:
void UpdatePieceInfo(PieceInfo* Info) const;
private:
static QByteArray ReadFileHash(const QString& FileName);
struct FileState
{
QByteArray Hash;
bool Exists = false;
bool Readable = false;
};
static FileState ReadFileState(const QString& FileName);
bool HasFileChanged(const QString& Path);
static QString MakeExportNameFragment(const QString& Name);
static QString MakeUniqueExportIdentifier(const QString& BaseName, std::set<QString>& UsedNames, const QString& ReservedSuffix);
@@ -155,7 +162,11 @@ protected:
bool mModified;
QString mFileName;
QByteArray mFileHash;
bool mFileExists = false;
bool mFileReadable = false;
int mFileReadAttempts = 0;
QFileSystemWatcher mFileWatcher;
QTimer mFileCheckTimer;
std::vector<std::unique_ptr<lcModel>> mModels;
// Maps normalized local filenames to PieceInfo objects visible in this project.