/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include "EditorDefs.h" #include "EditorFileMonitor.h" // Editor #include "CryEdit.h" ////////////////////////////////////////////////////////////////////////// CEditorFileMonitor::CEditorFileMonitor() { GetIEditor()->RegisterNotifyListener(this); } ////////////////////////////////////////////////////////////////////////// CEditorFileMonitor::~CEditorFileMonitor() { CFileChangeMonitor::DeleteInstance(); } ////////////////////////////////////////////////////////////////////////// void CEditorFileMonitor::OnEditorNotifyEvent(EEditorNotifyEvent ev) { if (ev == eNotify_OnInit) { // We don't want the file monitor to be enabled while // in console mode... if (!GetIEditor()->IsInConsolewMode()) { MonitorDirectories(); } CFileChangeMonitor::Instance()->Subscribe(this); } else if (ev == eNotify_OnQuit) { CFileChangeMonitor::Instance()->StopMonitor(); GetIEditor()->UnregisterNotifyListener(this); } } ////////////////////////////////////////////////////////////////////////// bool CEditorFileMonitor::RegisterListener(IFileChangeListener* pListener, const char* sMonitorItem) { return RegisterListener(pListener, sMonitorItem, "*"); } ////////////////////////////////////////////////////////////////////////// static AZStd::string CanonicalizePath(const char* path) { auto canon = QFileInfo(path).canonicalFilePath(); return canon.isEmpty() ? AZStd::string(path) : AZStd::string(canon.toUtf8()); } ////////////////////////////////////////////////////////////////////////// bool CEditorFileMonitor::RegisterListener(IFileChangeListener* pListener, const char* sFolderRelativeToGame, const char* sExtension) { bool success = true; AZStd::string gameFolder = Path::GetEditingGameDataFolder().c_str(); AZStd::string naivePath; CFileChangeMonitor* fileChangeMonitor = CFileChangeMonitor::Instance(); AZ_Assert(fileChangeMonitor, "CFileChangeMonitor singleton missing."); naivePath += gameFolder; // Append slash in preparation for appending the second part. naivePath = PathUtil::AddSlash(naivePath); naivePath += sFolderRelativeToGame; AZ::StringFunc::Replace(naivePath, '/', '\\'); // Remove the final slash if the given item is a folder so the file change monitor correctly picks up on it. naivePath = PathUtil::RemoveSlash(naivePath); AZStd::string canonicalizedPath = CanonicalizePath(naivePath.c_str()); if (fileChangeMonitor->IsDirectory(canonicalizedPath.c_str()) || fileChangeMonitor->IsFile(canonicalizedPath.c_str())) { if (fileChangeMonitor->MonitorItem(canonicalizedPath.c_str())) { m_vecFileChangeCallbacks.push_back(SFileChangeCallback(pListener, sFolderRelativeToGame, sExtension)); } else { CryLogAlways("File Monitor: [%s] not found outside of PAK files. Monitoring disabled for this item", sFolderRelativeToGame); success = false; } } return success; } bool CEditorFileMonitor::UnregisterListener(IFileChangeListener* pListener) { bool bRet = false; // Note that we remove the listener, but we don't currently remove the monitored item // from the file monitor. This is fine, but inefficient std::vector::iterator iter = m_vecFileChangeCallbacks.begin(); while (iter != m_vecFileChangeCallbacks.end()) { if (iter->pListener == pListener) { iter = m_vecFileChangeCallbacks.erase(iter); bRet = true; } else { iter++; } } return bRet; } ////////////////////////////////////////////////////////////////////////// void CEditorFileMonitor::MonitorDirectories() { QString primaryCD = Path::AddPathSlash(QString(GetIEditor()->GetPrimaryCDFolder())); // NOTE: Instead of monitoring each sub-directory we monitor the whole root // folder. This is needed since if the sub-directory does not exist when // we register it it will never get monitored properly. CFileChangeMonitor::Instance()->MonitorItem(QStringLiteral("%1/%2/").arg(primaryCD).arg(QString::fromLatin1(Path::GetEditingGameDataFolder().c_str()))); // Add editor directory for scripts CFileChangeMonitor::Instance()->MonitorItem(QStringLiteral("%1/Editor/").arg(primaryCD)); } QString RemoveGameName(const QString &filename) { // Remove first part of path. File coming in has the game name included // eg (AutomatedTesting/Animations/Chicken/anim_chicken_flapping.i_caf)->(Animations/Chicken/anim_chicken_flapping.i_caf) int indexOfFirstSlash = filename.indexOf('/'); int indexOfFirstBackSlash = filename.indexOf('\\'); if (indexOfFirstSlash >= 0) { if (indexOfFirstBackSlash >= 0 && indexOfFirstBackSlash < indexOfFirstSlash) { indexOfFirstSlash = indexOfFirstBackSlash; } } else { indexOfFirstSlash = indexOfFirstBackSlash; } return filename.mid(indexOfFirstSlash + 1); } /////////////////////////////////////////////////////////////////////////// // Called when file monitor message is received void CEditorFileMonitor::OnFileMonitorChange(const SFileChangeInfo& rChange) { CCryEditApp* app = CCryEditApp::instance(); if (app == NULL || app->IsExiting()) { return; } // skip folders! if (QFileInfo(rChange.filename).isDir()) { return; } // Process updated file. // Make file relative to PrimaryCD folder. QString filename = rChange.filename; // Remove game directory if present in path. const QString rootPath = QDir::fromNativeSeparators(QString::fromLatin1(Path::GetEditingRootFolder().c_str())); if (filename.startsWith(rootPath, Qt::CaseInsensitive)) { filename = filename.right(filename.length() - rootPath.length()); } // Make sure there is no leading slash if (!filename.isEmpty() && (filename[0] == '\\' || filename[0] == '/')) { filename = filename.mid(1); } if (!filename.isEmpty()) { //remove game name. Make it relative to the game folder const QString filenameRelGame = RemoveGameName(filename); const int extIndex = filename.lastIndexOf('.'); const QString ext = filename.right(filename.length() - 1 - extIndex); // Check for File Monitor callback std::vector::iterator iter; for (iter = m_vecFileChangeCallbacks.begin(); iter != m_vecFileChangeCallbacks.end(); ++iter) { SFileChangeCallback& sCallback = *iter; // We compare against length of callback string, so we get directory matches as well as full filenames if (sCallback.pListener) { if (sCallback.extension == "*" || ext.compare(sCallback.extension, Qt::CaseInsensitive) == 0) { if (filenameRelGame.compare(sCallback.item, Qt::CaseInsensitive) == 0) { sCallback.pListener->OnFileChange(qPrintable(filenameRelGame), IFileChangeListener::EChangeType(rChange.changeType)); } else if (filename.compare(sCallback.item, Qt::CaseInsensitive) == 0) { sCallback.pListener->OnFileChange(qPrintable(filename), IFileChangeListener::EChangeType(rChange.changeType)); } } } } // Set this flag to make sure that the viewport will update at least once, // so that the changes will be shown, even if the app does not have focus. CCryEditApp::instance()->ForceNextIdleProcessing(); } }