Suppress file change prompts for unchanged contents

Filesystem watcher events can be triggered by metadata updates or identical rewrites, so forwarding every event directly to the reload dialog produces false notifications.

Track a content hash after loading, saving, and observing external edits. Only prompt for different readable contents, ignore stale and duplicate events, and restore the file watch after atomic replacement even when the contents are unchanged.

Validation: macOS debug build and native Qt watcher checks covering timestamp updates, identical replacements, same-size edits, repeated changes, own-save baselines, stale events, and empty files.
This commit is contained in:
Leonardo Zide committed 2026-10-07 09:49:25 -07:00
1 parent 8197a28517
commit b3c325dbc3
2 files changed
+44 -1

No files matched your search

+41 -1
View File
@@ -76,7 +76,11 @@ Project::Project(bool IsPreview)
mModels.emplace_back(mActiveModel);
if (!mIsPreview && gMainWindow)
QObject::connect(&mFileWatcher, &QFileSystemWatcher::fileChanged, gMainWindow, &lcMainWindow::ProjectFileChanged);
QObject::connect(&mFileWatcher, &QFileSystemWatcher::fileChanged, gMainWindow, [this](const QString& Path)
{
if (HasFileChanged(Path))
gMainWindow->ProjectFileChanged(Path);
});
}
Project::~Project()
@@ -571,6 +575,36 @@ void Project::ShowModelListDialog()
Model->RefreshPreview();
}
QByteArray Project::ReadFileHash(const QString& FileName)
{
QFile File(FileName);
QCryptographicHash Hash(QCryptographicHash::Sha256);
if (!File.open(QIODevice::ReadOnly) || !Hash.addData(&File))
return QByteArray();
return Hash.result();
}
bool Project::HasFileChanged(const QString& Path)
{
if (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);
const QByteArray FileHash = ReadFileHash(Path);
if (FileHash.isEmpty() || FileHash == mFileHash)
return false;
// Remember the observed contents so duplicate events do not prompt again.
mFileHash = FileHash;
return true;
}
void Project::SetFileName(const QString& FileName)
{
if (mFileName == FileName)
@@ -607,6 +641,9 @@ bool Project::Load(const QString& FileName, bool ShowErrors)
QString Extension = FileInfo.suffix().toLower();
QByteArray FileData = File.readAll();
if (!mIsPreview)
mFileHash = QCryptographicHash::hash(FileData, QCryptographicHash::Sha256);
bool LoadDAT;
if (Extension == QLatin1String("dat") || Extension == QLatin1String("ldr") || Extension == QLatin1String("mpd"))
@@ -787,6 +824,9 @@ lcResult<void> Project::Save(const QString& FileName)
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
}
if (!mIsPreview)
mFileHash = ReadFileHash(FileName);
SetFileName(FileName);
if (DirectoryChanged)
+3
View File
@@ -137,6 +137,8 @@ public:
void UpdatePieceInfo(PieceInfo* Info) const;
private:
static QByteArray ReadFileHash(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);
@@ -152,6 +154,7 @@ protected:
bool mDeferModelMeshRequests = false;
bool mModified;
QString mFileName;
QByteArray mFileHash;
QFileSystemWatcher mFileWatcher;
std::vector<std::unique_ptr<lcModel>> mModels;