mirror of
https://github.com/leozide/leocad.git
synced 2026-10-27 06:20:55 +00:00
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:
1 parent
b3c325dbc3
commit
7e9283fec0
5 files changed
+167
-49
No files matched your search
@@ -296,6 +296,7 @@ void lcApplication::SetProject(Project* Project)
|
||||
|
||||
delete mProject;
|
||||
mProject = Project;
|
||||
mProject->UpdateFileWatcher();
|
||||
|
||||
if (gMainWindow)
|
||||
gMainWindow->ScheduleAssetUpdate();
|
||||
|
||||
+26
-26
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user