src/documentmanager.cpp

changeset 212
27259810da6d
parent 206
654661eab7f3
child 213
ee5758ddb6d2
equal deleted inserted replaced
211:b27b90fb993f 212:27259810da6d
21 #include <QFileInfo> 21 #include <QFileInfo>
22 #include <QSaveFile> 22 #include <QSaveFile>
23 #include "documentmanager.h" 23 #include "documentmanager.h"
24 #include "parser.h" 24 #include "parser.h"
25 25
26 /** 26 DocumentManager::DocumentManager()
27 * @brief Constructs a new document manager
28 * @param parent Parent object
29 */
30 DocumentManager::DocumentManager(QObject* parent) :
31 QObject{parent}
32 { 27 {
33 } 28 }
34 29
35 /** 30 /**
36 * @brief Creates a new model. 31 * @brief Creates a new model.
37 * @returns the ID of the new model 32 * @returns the ID of the new model
38 */ 33 */
39 ModelId DocumentManager::newModel() 34 ModelId DocumentManager::newModel()
40 { 35 {
41 const ModelId modelId{++this->modelIdCounter}; 36 const ModelId modelId{++this->modelIdCounter};
42 const QString name = makeNewModelName(); 37 this->openModels[modelId].id = modelId;
43 this->openModels[modelId] = ModelInfo{ 38 this->openModels[modelId].opentype = OpenType::ManuallyOpened;
44 .model = std::make_unique<Model>(this),
45 .id = modelId,
46 .opentype = OpenType::ManuallyOpened,
47 };
48 this->makePolygonCacheForModel(modelId); 39 this->makePolygonCacheForModel(modelId);
49 return modelId; 40 return modelId;
50 } 41 }
51 42
52 /**
53 * @brief Looks for a model by name
54 * @param name Name of the model
55 * @returns model or null
56 */
57 Model* DocumentManager::findDependencyByName(const ModelId modelId, const QString& name) 43 Model* DocumentManager::findDependencyByName(const ModelId modelId, const QString& name)
58 { 44 {
59 const auto modelsIterator = this->openModels.find(modelId); 45 const auto modelsIterator = this->openModels.find(modelId);
60 if (modelsIterator != std::end(this->openModels)) 46 if (modelsIterator != std::end(this->openModels)) {
61 {
62 const auto& dependencies = modelsIterator->second.dependencies; 47 const auto& dependencies = modelsIterator->second.dependencies;
63 const auto dependenciesIterator = dependencies.find(name); 48 const auto dependenciesIterator = dependencies.find(name);
64 if (dependenciesIterator != dependencies.end()) 49 if (dependenciesIterator != dependencies.end()) {
65 {
66 ModelInfo& modelInfo = this->openModels[dependenciesIterator->second]; 50 ModelInfo& modelInfo = this->openModels[dependenciesIterator->second];
67 return modelInfo.model.get(); 51 return modelInfo.model.get();
68 } 52 }
69 else 53 else {
70 {
71 return nullptr; 54 return nullptr;
72 } 55 }
73 } 56 }
74 else 57 else {
75 {
76 return nullptr; 58 return nullptr;
77 } 59 }
78 } 60 }
79 61
80 /** 62 /**
129 const OpenType openType 111 const OpenType openType
130 ) { 112 ) {
131 QFile file{path}; 113 QFile file{path};
132 const QString name = pathToName(path); 114 const QString name = pathToName(path);
133 file.open(QFile::ReadOnly | QFile::Text); 115 file.open(QFile::ReadOnly | QFile::Text);
134 std::unique_ptr<Model> newModel = std::make_unique<Model>(this); 116 std::unique_ptr<Model> newModel = std::make_unique<Model>(nullptr);
135 QTextStream textStream{&file}; 117 QTextStream textStream{&file};
136 Parser parser{file}; 118 Parser parser{file};
137 parser.parseBody(*newModel); 119 parser.parseBody(*newModel);
138 std::optional<ModelId> result; 120 std::optional<ModelId> result;
139 if (file.error() == QFile::NoError) 121 if (file.error() == QFile::NoError)
140 { 122 {
141 const ModelId modelId{++this->modelIdCounter}; 123 const ModelId modelId{++this->modelIdCounter};
142 this->openModels[modelId] = {std::move(newModel), modelId, path, openType}; 124 this->openModels[modelId] = {
125 .model = std::move(newModel),
126 .id = modelId,
127 .path = path,
128 .opentype = openType,
129 .polygonCache = {},
130 };
143 this->makePolygonCacheForModel(modelId); 131 this->makePolygonCacheForModel(modelId);
144 result = modelId; 132 result = modelId;
145 } 133 }
146 else 134 else
147 { 135 {
148 errorStream << file.errorString(); 136 errorStream << file.errorString();
149 } 137 }
150 return result; 138 return result;
151 } 139 }
152 140
153 QString DocumentManager::makeNewModelName()
154 {
155 untitledNameCounter += 1;
156 return "untitled-" + QString::number(untitledNameCounter);
157 }
158
159 void DocumentManager::loadDependenciesForAllModels(const LibraryManager& libraries, QTextStream& errorStream)
160 {
161 for (const auto& modelInfoPair : this->openModels)
162 {
163 this->loadDependenciesForModel(modelInfoPair.first, modelInfoPair.second.path, libraries, errorStream);
164 }
165 }
166
167 struct DocumentManager::LoadDepedenciesBag
168 {
169 const LibraryManager& libraries;
170 QStringList missing;
171 QSet<ModelId> processed;
172 QTextStream& errorStream;
173 };
174
175 void DocumentManager::loadDependenciesForModel(
176 const ModelId modelId,
177 const QString& path,
178 const LibraryManager& libraries,
179 QTextStream& errorStream)
180 {
181 LoadDepedenciesBag bag {
182 .libraries = libraries,
183 .missing = {},
184 .processed = {},
185 .errorStream = errorStream,
186 };
187 this->loadDependenciesForModel(modelId, path, bag);
188 if (not bag.missing.empty())
189 {
190 bag.missing.sort(Qt::CaseInsensitive);
191 errorStream << tr("The following files could not be opened: %1")
192 .arg(bag.missing.join(", "));
193 }
194 }
195
196 void DocumentManager::closeDocument(const ModelId modelId) 141 void DocumentManager::closeDocument(const ModelId modelId)
197 { 142 {
198 ModelInfo* modelInfo = findInMap(this->openModels, modelId); 143 ModelInfo* modelInfo = findInMap(this->openModels, modelId);
199 if (modelInfo != nullptr) 144 if (modelInfo != nullptr)
200 { 145 {
214 { 159 {
215 return nullptr; 160 return nullptr;
216 } 161 }
217 } 162 }
218 163
219 /** 164 //! \brief Changes the path of the specified model. This can cause dependencies
220 * @brief Changes the path of the specified model. Since the name of the file may change, 165 //! to be resolved differently. As such, dependencies need to be resolved for
221 * changing the path can cause dependencies to be resolved differently. As such, dependencies 166 //! all files after this operation.
222 * need to be resolved for all files after this operation.
223 * @param modelId Model to change the path of
224 * @param newPath New path
225 * @param libraries Library manager for the purpose of dependency resolving
226 * @param errorStream Where to write any errors regarding dependency resolving
227 */
228 void DocumentManager::setModelPath( 167 void DocumentManager::setModelPath(
229 const ModelId modelId, 168 const ModelId modelId,
230 const QString &newPath, 169 const QString &newPath,
231 const LibraryManager &libraries, 170 const LibraryManager &libraries,
232 QTextStream &errorStream) 171 QTextStream &errorStream)
233 { 172 {
234 auto modelInfoPair = this->openModels.find(modelId); 173 ModelInfo* info = findInMap(this->openModels, modelId);
235 if (true 174 if (info != nullptr and info->opentype == OpenType::ManuallyOpened) {
236 and modelInfoPair != this->openModels.end() 175 info->path = newPath;
237 and modelInfoPair->second.opentype == OpenType::ManuallyOpened 176 const MissingDependencies missing = this->loadDependenciesForAllModels(libraries);
238 ) { 177 if (not missing.empty()) {
239 modelInfoPair->second.path = newPath; 178 errorStream << errorStringFromMissingDependencies(missing);
240 this->loadDependenciesForAllModels(libraries, errorStream); 179 }
241 } 180 }
242 } 181 }
243 182
244 bool DocumentManager::saveModel(const ModelId modelId, QTextStream &errors) 183 bool DocumentManager::saveModel(const ModelId modelId, QTextStream &errors)
245 { 184 {
246 const QString* const path = this->modelPath(modelId); 185 ModelInfo* info = findInMap(this->openModels, modelId);
247 if (path != nullptr) 186 if (info != nullptr)
248 { 187 {
249 QSaveFile file{*path}; 188 QSaveFile file{info->path};
250 file.setDirectWriteFallback(true); 189 file.setDirectWriteFallback(true);
251 if (file.open(QSaveFile::WriteOnly)) 190 if (file.open(QSaveFile::WriteOnly)) {
252 { 191 ::save(info->model.get(), &file);
253 // if path is not nullptr, getModelById will always return a value as well
254 ::save(*this->getModelById(modelId), &file);
255 const bool commitSucceeded = file.commit(); 192 const bool commitSucceeded = file.commit();
256 if (not commitSucceeded) 193 if (not commitSucceeded) {
257 { 194 errors << QObject::tr("Could not save: %1").arg(file.errorString());
258 errors << tr("Could not save: %1").arg(file.errorString());
259 return false; 195 return false;
260 } 196 }
261 else 197 else {
262 {
263 return true; 198 return true;
264 } 199 }
265 } 200 }
266 else 201 else {
267 { 202 errors << QObject::tr("Could not open %1 for writing: %2")
268 errors << tr("Could not open %1 for writing: %2")
269 .arg(file.fileName(), file.errorString()); 203 .arg(file.fileName(), file.errorString());
270 return false; 204 return false;
271 } 205 }
272 } 206 }
273 else 207 else {
274 { 208 errors << QObject::tr("Bad model ID %1").arg(modelId.value);
275 errors << tr("Bad model ID %1").arg(modelId.value);
276 return false; 209 return false;
277 } 210 }
278 } 211 }
279 212
280 /** 213 /**
296 return result; 229 return result;
297 } 230 }
298 231
299 PolygonCache *DocumentManager::getPolygonCacheForModel(ModelId modelId) 232 PolygonCache *DocumentManager::getPolygonCacheForModel(ModelId modelId)
300 { 233 {
301 auto it = this->polygonCaches.find(modelId); 234 ModelInfo* info = findInMap(this->openModels, modelId);
302 if (it != this->polygonCaches.end()) 235 if (info != nullptr) {
303 { 236 return &info->polygonCache;
304 return &it->second; 237 }
305 } 238 else {
306 else
307 {
308 return nullptr; 239 return nullptr;
309 } 240 }
241 }
242
243 const DocumentManager::ModelInfo *DocumentManager::infoForModel(ModelId modelId) const
244 {
245 return findInMap(this->openModels, modelId);
246 }
247
248 QString errorStringFromMissingDependencies(const DocumentManager::MissingDependencies& missing)
249 {
250 QString missingString;
251 forValueInMap(missing, [&missingString](const QString& path){
252 missingString = joined(missingString, QStringLiteral(", "), path);
253 });
254 return QObject::tr("The following files could not be opened: %1")
255 .arg(missingString);
310 } 256 }
311 257
312 /** 258 /**
313 * @brief Cleans up and erases models that are no longer required. 259 * @brief Cleans up and erases models that are no longer required.
314 */ 260 */
319 // Find models that are not edited by the user and are not needed by any other model 265 // Find models that are not edited by the user and are not needed by any other model
320 if (true 266 if (true
321 and it->second.opentype == OpenType::AutomaticallyOpened 267 and it->second.opentype == OpenType::AutomaticallyOpened
322 and not this->isReferencedByAnything(it->first) 268 and not this->isReferencedByAnything(it->first)
323 ) { 269 ) {
324 // Remove its polygon cache
325 const auto polygonCache = this->polygonCaches.find(it->first);
326 if (polygonCache != this->polygonCaches.end())
327 {
328 this->polygonCaches.erase(polygonCache);
329 }
330 // Remove the model 270 // Remove the model
331 this->openModels.erase(it); 271 this->openModels.erase(it);
332 // We need to start over now. It is possible that other models that previously 272 // We need to start over now. It is possible that other models that
333 // were referenced by the model we just erased have become prunable. 273 // previously were referenced by the model we just erased have
334 // Moreover, our iterator is invalid now and we cannot continue in this for loop. 274 // become prunable. Moreover, our iterator is invalid now and we
275 // cannot continue in this loop.
335 this->prune(); 276 this->prune();
336 break; 277 break;
337 } 278 }
338 } 279 }
339 } 280 }
364 void DocumentManager::makePolygonCacheForModel(const ModelId modelId) 305 void DocumentManager::makePolygonCacheForModel(const ModelId modelId)
365 { 306 {
366 Model* model = this->getModelById(modelId); 307 Model* model = this->getModelById(modelId);
367 if (model != nullptr) 308 if (model != nullptr)
368 { 309 {
369 this->polygonCaches[modelId] = {}; 310 const auto modelModified = [this, model]{
370 connect(model, &Model::dataChanged, this, &DocumentManager::modelModified); 311 const std::optional<ModelId> modelId = this->findIdForModel(model);
371 connect(model, &Model::rowsInserted, this, &DocumentManager::modelModified); 312 if (modelId.has_value()) {
372 connect(model, &Model::rowsRemoved, this, &DocumentManager::modelModified); 313 ModelInfo* info = findInMap(this->openModels, *modelId);
373 } 314 if (info != nullptr) {
374 } 315 info->polygonCache.needRecache = true;
375 316 }
376 void DocumentManager::modelModified() 317 }
377 { 318 };
378 Model* const model = qobject_cast<Model*>(this->sender()); 319 QObject::connect(model, &Model::dataChanged, modelModified);
379 const std::optional<ModelId> modelId = this->findIdForModel(model); 320 QObject::connect(model, &Model::rowsInserted, modelModified);
380 if (modelId.has_value()) { 321 QObject::connect(model, &Model::rowsRemoved, modelModified);
381 this->polygonCaches[*modelId].needRecache = true; 322 }
382 } 323 }
383 } 324
384 325 static QString findFile(
385 static QString findFile(QString referenceName, const QString& path, const LibraryManager& libraries) 326 QString referenceName,
327 const QString& modelPath,
328 const LibraryManager& libraries)
386 { 329 {
387 // Try to find the file in the same place as the model itself 330 // Try to find the file in the same place as the model itself
388 referenceName.replace("\\", "/"); 331 referenceName.replace("\\", "/");
389 const QDir dir = QFileInfo{path}.dir(); 332 const QDir dir = QFileInfo{modelPath}.dir();
390 QString referencedFilePath = dir.filePath(referenceName); 333 QString referencedFilePath = dir.filePath(referenceName);
391 if (not QFileInfo{referencedFilePath}.exists()) 334 if (not QFileInfo{referencedFilePath}.exists())
392 { 335 {
393 // Look for it in the libraries 336 // Look for it in the libraries
394 referencedFilePath = libraries.findFile(referenceName); 337 referencedFilePath = libraries.findFile(referenceName);
395 } 338 }
396 return referencedFilePath; 339 return referencedFilePath;
397 } 340 }
398 341
399 template<typename T> 342 static std::set<QString> referenceNames(const Model* model)
400 void iterate(const Model& model, std::function<void(const T&)> fn) 343 {
401 { 344 std::set<QString> result;
402 for (int i = 0; i < model.size(); ++i) { 345 iterate<Colored<SubfileReference>>(*model, [&result](const SubfileReference& ref){
403 if (std::holds_alternative<T>(model[i])) { 346 result.insert(ref.name);
404 fn(std::get<T>(model[i])); 347 });
405 } 348 return result;
406 } 349 }
407 } 350
408 351 struct Dependency
409 void DocumentManager::loadDependenciesForModel( 352 {
410 const ModelId modelId, 353 QString name;
411 const QString &path, 354 QString path;
412 LoadDepedenciesBag& bag) 355 bool operator<(const Dependency& other) const
413 { 356 {
414 QSet<QString> failedToOpen; 357 if (this->name != other.name) {
415 struct LoadingError 358 return this->name < other.name;
416 { 359 }
417 QString message; 360 else {
418 }; 361 return this->path < other.path;
419 bag.processed.insert(modelId); 362 }
420 if (not this->openModels.contains(modelId)) 363 }
421 { 364 };
422 bag.errorStream << tr("bad model ID %1").arg(modelId.value); 365
423 return; 366 static std::set<Dependency> resolveReferencePaths(
424 } 367 const DocumentManager::ModelInfo* modelInfo,
425 ModelInfo& modelInfo = this->openModels[modelId]; 368 const LibraryManager* libraries)
426 modelInfo.dependencies.clear(); 369 {
427 iterate<Colored<SubfileReference>>(*modelInfo.model, [&](const SubfileReference& ref) { 370 std::set<Dependency> result;
428 const QString referenceName = ref.name; 371 const std::set<QString> refNames = referenceNames(modelInfo->model.get());
429 if (not referenceName.isEmpty() 372 if (modelInfo != nullptr) {
430 and modelInfo.dependencies.count(referenceName) == 0 373 for (const QString& name : refNames) {
431 and not failedToOpen.contains(referenceName)) 374 const QString path = findFile(name, modelInfo->path, *libraries);
432 { 375 if (not path.isEmpty()) {
433 try 376 result.insert(Dependency{.name = name, .path = path});
434 { 377 }
435 const QString referencedFilePath = ::findFile(referenceName, path, bag.libraries); 378 }
436 if (referencedFilePath.isEmpty()) 379 }
437 { 380 return result;
438 throw LoadingError{tr("could not find '%1'").arg(referenceName)}; 381 }
439 } 382
383 static void loadDependenciesForModel(
384 DocumentManager::ModelInfo* info,
385 DocumentManager* documents,
386 const LibraryManager* libraries,
387 std::map<QString, QString>& missing)
388 {
389 bool repeat = true;
390 info->dependencies.clear();
391 while (repeat) {
392 repeat = false;
393 const std::set<Dependency> dependencies = resolveReferencePaths(info, libraries);
394 for (const Dependency& dep : dependencies) {
395 if (not info->dependencies.contains(dep.name) and not missing.contains(dep.path)) {
440 QString loadErrorString; 396 QString loadErrorString;
441 QTextStream localErrorStream{&loadErrorString}; 397 QTextStream localErrorStream{&loadErrorString};
442 const std::optional<ModelId> modelIdOpt = this->openModel( 398 const std::optional<ModelId> modelIdOpt = documents->openModel(
443 referencedFilePath, 399 dep.path,
444 localErrorStream, 400 localErrorStream,
445 OpenType::AutomaticallyOpened); 401 OpenType::AutomaticallyOpened);
446 if (not modelIdOpt.has_value()) 402 if (not modelIdOpt.has_value()) {
447 { 403 const QString& errorMessage = QObject::tr("could not load '%1': %2")
448 const QString& errorMessage = tr("could not load '%1': %2") 404 .arg(dep.path, loadErrorString);
449 .arg(referencedFilePath) 405 missing[dep.path] = errorMessage;
450 .arg(loadErrorString);
451 throw LoadingError{errorMessage};
452 } 406 }
453 modelInfo.dependencies[referenceName] = modelIdOpt.value(); 407 else {
454 if (not bag.processed.contains(modelIdOpt.value())) 408 info->dependencies[dep.name] = modelIdOpt.value();
455 { 409 repeat = true;
456 this->loadDependenciesForModel(modelIdOpt.value(), referencedFilePath, bag);
457 } 410 }
458 } 411 }
459 catch(const LoadingError& error) 412 }
460 { 413 }
461 bag.errorStream << error.message << "\n"; 414 }
462 failedToOpen.insert(referenceName); 415
463 bag.missing.append(referenceName); 416 std::map<QString, QString> DocumentManager::loadDependenciesForAllModels(const LibraryManager& libraries)
464 } 417 {
465 } 418 std::map<QString, QString> missing;
466 }); 419 for (auto& modelInfoPair : this->openModels)
467 } 420 {
421 loadDependenciesForModel(&modelInfoPair.second, this, &libraries, missing);
422 }
423 this->prune();
424 return missing;
425 }

mercurial