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 /** |
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 } |