34 { |
35 { |
35 } |
36 } |
36 |
37 |
37 /** |
38 /** |
38 * @brief Creates a new model. |
39 * @brief Creates a new model. |
39 * @returns the name to the new model |
40 * @returns the ID of the new model |
40 */ |
41 */ |
41 QString DocumentManager::newModel() |
42 ModelId DocumentManager::newModel() |
42 { |
43 { |
|
44 const ModelId modelId{++this->modelIdCounter}; |
43 const QString name = makeNewModelName(); |
45 const QString name = makeNewModelName(); |
44 this->openModels.emplace(name, new Model); |
46 this->openModels[modelId] = ModelInfo{ |
45 return name; |
47 .model = std::make_unique<Model>(), |
|
48 .opentype = OpenType::ManuallyOpened, |
|
49 }; |
|
50 return modelId; |
46 } |
51 } |
47 |
52 |
48 /** |
53 /** |
49 * @brief Looks for a model by name |
54 * @brief Looks for a model by name |
50 * @param name Name of the model |
55 * @param name Name of the model |
51 * @returns model or null |
56 * @returns model or null |
52 * ' |
57 */ |
53 */ |
58 Model* DocumentManager::findDependencyByName(const ModelId modelId, const QString& name) |
54 Model* DocumentManager::findModelByName(const QString& name) |
59 { |
55 { |
60 const auto modelsIterator = this->openModels.find(modelId); |
56 const auto iterator = this->openModels.find(name); |
61 if (modelsIterator != std::end(this->openModels)) |
57 if (iterator == std::end(this->openModels)) |
62 { |
|
63 const auto& dependencies = modelsIterator->second.dependencies; |
|
64 const auto dependenciesIterator = dependencies.find(name); |
|
65 if (dependenciesIterator != dependencies.end()) |
|
66 { |
|
67 ModelInfo& modelInfo = this->openModels[dependenciesIterator->second]; |
|
68 return modelInfo.model.get(); |
|
69 } |
|
70 else |
|
71 { |
|
72 return nullptr; |
|
73 } |
|
74 } |
|
75 else |
58 { |
76 { |
59 return nullptr; |
77 return nullptr; |
60 } |
78 } |
61 else |
79 } |
|
80 |
|
81 /** |
|
82 * @brief Gets a model pointer by id or nullptr if not found |
|
83 * @param modelId id of model to find |
|
84 * @returns model pointer or null |
|
85 */ |
|
86 Model *DocumentManager::getModelById(ModelId modelId) |
|
87 { |
|
88 const auto iterator = this->openModels.find(modelId); |
|
89 if (iterator != this->openModels.end()) |
62 { |
90 { |
63 return iterator->second.model.get(); |
91 return iterator->second.model.get(); |
|
92 } |
|
93 else |
|
94 { |
|
95 return nullptr; |
64 } |
96 } |
65 } |
97 } |
66 |
98 |
67 QString pathToName(const QFileInfo& path) |
99 QString pathToName(const QFileInfo& path) |
68 { |
100 { |
88 /** |
120 /** |
89 * @brief Tries to open the model at the specified path |
121 * @brief Tries to open the model at the specified path |
90 * @param path Path to the model to open |
122 * @param path Path to the model to open |
91 * @param errorStream Where to write any errors |
123 * @param errorStream Where to write any errors |
92 * @param openType rationale behind opening this file |
124 * @param openType rationale behind opening this file |
93 * @returns file name or "" on error |
125 * @returns model id, or no value on error |
94 */ |
126 */ |
95 QString DocumentManager::openModel(const QString& path, QTextStream& errorStream, const OpenType openType) |
127 std::optional<ModelId> DocumentManager::openModel( |
96 { |
128 const QString& path, |
|
129 QTextStream& errorStream, |
|
130 const OpenType openType |
|
131 ) { |
97 QFile file{path}; |
132 QFile file{path}; |
98 const QString name = pathToName(path); |
133 const QString name = pathToName(path); |
99 file.open(QFile::ReadOnly | QFile::Text); |
134 file.open(QFile::ReadOnly | QFile::Text); |
100 std::unique_ptr<Model> newModel = std::make_unique<Model>(path); |
135 std::unique_ptr<Model> newModel = std::make_unique<Model>(this); |
101 QTextStream textStream{&file}; |
136 QTextStream textStream{&file}; |
102 Model::EditContext editor = newModel->edit(); |
137 Model::EditContext editor = newModel->edit(); |
103 Parser parser{file}; |
138 Parser parser{file}; |
104 parser.parseBody(editor); |
139 parser.parseBody(editor); |
105 QString result; |
140 std::optional<ModelId> result; |
106 if (file.error() == QFile::NoError) |
141 if (file.error() == QFile::NoError) |
107 { |
142 { |
108 openModels[name] = {std::move(newModel), openType}; |
143 const ModelId modelId{++this->modelIdCounter}; |
109 result = name; |
144 this->openModels[modelId] = {std::move(newModel), path, openType}; |
|
145 result = modelId; |
110 } |
146 } |
111 else |
147 else |
112 { |
148 { |
113 errorStream << file.errorString(); |
149 errorStream << file.errorString(); |
114 } |
150 } |
119 { |
155 { |
120 untitledNameCounter += 1; |
156 untitledNameCounter += 1; |
121 return "untitled-" + QString::number(untitledNameCounter); |
157 return "untitled-" + QString::number(untitledNameCounter); |
122 } |
158 } |
123 |
159 |
|
160 void DocumentManager::loadDependenciesForAllModels(const LibraryManager& libraries, QTextStream& errorStream) |
|
161 { |
|
162 for (const auto& modelInfoPair : this->openModels) |
|
163 { |
|
164 this->loadDependenciesForModel(modelInfoPair.first, modelInfoPair.second.path, libraries, errorStream); |
|
165 } |
|
166 } |
|
167 |
|
168 struct DocumentManager::LoadDepedenciesBag |
|
169 { |
|
170 const LibraryManager& libraries; |
|
171 QStringList missing; |
|
172 QSet<ModelId> processed; |
|
173 QTextStream& errorStream; |
|
174 }; |
|
175 |
124 void DocumentManager::loadDependenciesForModel( |
176 void DocumentManager::loadDependenciesForModel( |
125 const QString& modelName, |
177 const ModelId modelId, |
126 const QString& path, |
178 const QString& path, |
127 const LibraryManager& libraries, |
179 const LibraryManager& libraries, |
128 QTextStream& errorStream) |
180 QTextStream& errorStream) |
129 { |
181 { |
130 QStringList missing; |
182 LoadDepedenciesBag bag { |
131 QStringList processed; |
183 .libraries = libraries, |
132 loadDependenciesForModel(modelName, path, libraries, missing, processed, errorStream); |
184 .missing = {}, |
133 if (not missing.empty()) |
185 .processed = {}, |
134 { |
186 .errorStream = errorStream, |
135 missing.sort(Qt::CaseInsensitive); |
187 }; |
|
188 this->loadDependenciesForModel(modelId, path, bag); |
|
189 if (not bag.missing.empty()) |
|
190 { |
|
191 bag.missing.sort(Qt::CaseInsensitive); |
136 errorStream << utility::format( |
192 errorStream << utility::format( |
137 "The following files could not be opened: %1", |
193 "The following files could not be opened: %1", |
138 missing.join(", ")); |
194 bag.missing.join(", ")); |
139 } |
195 } |
140 } |
196 } |
141 |
197 |
142 void DocumentManager::closeDocument(const QString &name) |
198 void DocumentManager::closeDocument(const ModelId modelId) |
143 { |
199 { |
144 const auto& it = this->openModels.find(name); |
200 ModelInfo* modelInfo = findInMap(this->openModels, modelId); |
145 if (it != this->openModels.end()) |
201 if (modelInfo != nullptr) |
146 { |
202 { |
147 this->openModels.erase(it); |
203 modelInfo->opentype = OpenType::AutomaticallyOpened; |
148 } |
204 this->prune(); |
149 QSet<QString> referenced; |
205 } |
150 for (const auto& it : this->openModels) |
206 } |
151 { |
207 |
152 if (it.second.opentype == OpenType::ManuallyOpened) |
208 const QString *DocumentManager::modelPath(ModelId modelId) const |
153 { |
209 { |
154 this->collectReferences(referenced, it.first, it.second.model.get()); |
210 const auto iterator = this->openModels.find(modelId); |
155 } |
211 if (iterator != this->openModels.end()) |
156 } |
212 { |
157 |
213 return &iterator->second.path; |
158 } |
214 } |
159 |
215 else |
160 void DocumentManager::collectReferences(QSet<QString>& referenced, const QString &name, const Model *model) |
216 { |
161 { |
217 return nullptr; |
162 if (not referenced.contains(name)) |
218 } |
163 { |
219 } |
164 referenced.insert(name); |
220 |
165 model->apply<ldraw::SubfileReference>([&](const ldraw::SubfileReference* referenceObject) |
221 /** |
166 { |
222 * @brief Changes the path of the specified model. Since the name of the file may change, |
167 const ldraw::id_t id = referenceObject->id; |
223 * changing the path can cause dependencies to be resolved differently. As such, dependencies |
168 const QString& referenceName = model->getObjectProperty<ldraw::Property::ReferenceName>(id); |
224 * need to be resolved for all files after this operation. |
169 auto it = this->openModels.find(referenceName); |
225 * @param modelId Model to change the path of |
170 if (it != this->openModels.end()) |
226 * @param newPath New path |
|
227 * @param libraries Library manager for the purpose of dependency resolving |
|
228 * @param errorStream Where to write any errors regarding dependency resolving |
|
229 */ |
|
230 void DocumentManager::setModelPath( |
|
231 const ModelId modelId, |
|
232 const QString &newPath, |
|
233 const LibraryManager &libraries, |
|
234 QTextStream &errorStream) |
|
235 { |
|
236 auto modelInfoPair = this->openModels.find(modelId); |
|
237 if (true |
|
238 and modelInfoPair != this->openModels.end() |
|
239 and modelInfoPair->second.opentype == OpenType::ManuallyOpened |
|
240 ) { |
|
241 modelInfoPair->second.path = newPath; |
|
242 this->loadDependenciesForAllModels(libraries, errorStream); |
|
243 } |
|
244 } |
|
245 |
|
246 bool DocumentManager::saveModel(const ModelId modelId, QTextStream &errors) |
|
247 { |
|
248 const QString* const path = this->modelPath(modelId); |
|
249 if (path != nullptr) |
|
250 { |
|
251 QSaveFile file{*path}; |
|
252 file.setDirectWriteFallback(true); |
|
253 if (file.open(QSaveFile::WriteOnly)) |
|
254 { |
|
255 // if path is not nullptr, getModelById will always return a value as well |
|
256 this->getModelById(modelId)->save(&file); |
|
257 const bool commitSucceeded = file.commit(); |
|
258 if (not commitSucceeded) |
171 { |
259 { |
172 const Model* const model = it->second.model.get(); |
260 errors << tr("Could not save: %1").arg(file.errorString()); |
173 this->collectReferences(referenced, referenceName, model); |
261 return false; |
174 } |
262 } |
175 }); |
263 else |
176 } |
264 { |
|
265 return true; |
|
266 } |
|
267 } |
|
268 else |
|
269 { |
|
270 errors << tr("Could not open %1 for writing: %2") |
|
271 .arg(file.fileName()) |
|
272 .arg(file.errorString()); |
|
273 return false; |
|
274 } |
|
275 } |
|
276 else |
|
277 { |
|
278 errors << tr("Bad model ID %1").arg(modelId.value); |
|
279 return false; |
|
280 } |
|
281 } |
|
282 |
|
283 /** |
|
284 * @brief Searches the open models for the specified model and returns its id if found |
|
285 * @param model model to look for |
|
286 * @return id or no value if not found |
|
287 */ |
|
288 std::optional<ModelId> DocumentManager::findIdForModel(const Model *model) const |
|
289 { |
|
290 std::optional<ModelId> result; |
|
291 for (auto it = this->openModels.begin(); it != this->openModels.end(); ++it) |
|
292 { |
|
293 if (it->second.model.get() == model) |
|
294 { |
|
295 result = it->first; |
|
296 break; |
|
297 } |
|
298 } |
|
299 return result; |
|
300 } |
|
301 |
|
302 /** |
|
303 * @brief Cleans up and erases models that are no longer required. |
|
304 */ |
|
305 void DocumentManager::prune() |
|
306 { |
|
307 for (auto it = this->openModels.begin(); it != this->openModels.end(); ++it) |
|
308 { |
|
309 // Find models that are not edited by the user and are not needed by any other model |
|
310 if (true |
|
311 and it->second.opentype == OpenType::AutomaticallyOpened |
|
312 and not this->isReferencedByAnything(it->first) |
|
313 ) { |
|
314 // Remove the model |
|
315 this->openModels.erase(it); |
|
316 // We need to start over now. It is possible that other models that previously |
|
317 // were referenced by the model we just erased have become prunable. |
|
318 // Moreover, our iterator is invalid now and we cannot continue in this for loop. |
|
319 this->prune(); |
|
320 break; |
|
321 } |
|
322 } |
|
323 } |
|
324 |
|
325 /** |
|
326 * @brief Finds out whether the specified model id is referenced by any other model |
|
327 * @param modelId |
|
328 * @returns bool |
|
329 */ |
|
330 bool DocumentManager::isReferencedByAnything(const ModelId modelId) const |
|
331 { |
|
332 for (auto& haystackModelPair : this->openModels) |
|
333 { |
|
334 if (haystackModelPair.first != modelId) |
|
335 { |
|
336 for (auto& dependencyPair : haystackModelPair.second.dependencies) |
|
337 { |
|
338 if (dependencyPair.second == modelId) |
|
339 { |
|
340 return true; |
|
341 } |
|
342 } |
|
343 } |
|
344 } |
|
345 return false; |
177 } |
346 } |
178 |
347 |
179 static QString findFile(QString referenceName, const QString& path, const LibraryManager& libraries) |
348 static QString findFile(QString referenceName, const QString& path, const LibraryManager& libraries) |
180 { |
349 { |
181 // Try to find the file in the same place as the model itself |
350 // Try to find the file in the same place as the model itself |
189 } |
358 } |
190 return referencedFilePath; |
359 return referencedFilePath; |
191 } |
360 } |
192 |
361 |
193 void DocumentManager::loadDependenciesForModel( |
362 void DocumentManager::loadDependenciesForModel( |
194 const QString& modelName, |
363 const ModelId modelId, |
195 const QString &path, |
364 const QString &path, |
196 const LibraryManager& libraries, |
365 LoadDepedenciesBag& bag) |
197 QStringList& missing, |
366 { |
198 QStringList& processed, |
367 QSet<QString> failedToOpen; |
199 QTextStream& errorStream) |
|
200 { |
|
201 struct LoadingError |
368 struct LoadingError |
202 { |
369 { |
203 QString message; |
370 QString message; |
204 }; |
371 }; |
205 processed.append(modelName); |
372 bag.processed.insert(modelId); |
206 Model* model = this->findModelByName(modelName); |
373 if (not this->openModels.contains(modelId)) |
207 for (int i = 0; i < model->size(); i += 1) |
374 { |
208 { |
375 bag.errorStream << tr("bad model ID %1").arg(modelId.value); |
209 const QString referenceName = model->getObjectProperty(i, ldraw::Property::ReferenceName).toString(); |
376 return; |
|
377 } |
|
378 ModelInfo& modelInfo = this->openModels[modelId]; |
|
379 modelInfo.dependencies.clear(); |
|
380 for (int i = 0; i < modelInfo.model->size(); i += 1) |
|
381 { |
|
382 const QString referenceName = modelInfo.model->getObjectProperty(i, ldraw::Property::ReferenceName).toString(); |
210 if (not referenceName.isEmpty() |
383 if (not referenceName.isEmpty() |
211 and openModels.find(referenceName) == std::end(openModels) |
384 and modelInfo.dependencies.count(referenceName) == 0 |
212 and not missing.contains(referenceName)) |
385 and not failedToOpen.contains(referenceName)) |
213 { |
386 { |
214 try |
387 try |
215 { |
388 { |
216 const QString referencedFilePath = findFile(referenceName, path, libraries); |
389 const QString referencedFilePath = ::findFile(referenceName, path, bag.libraries); |
217 if (referencedFilePath.isEmpty()) |
390 if (referencedFilePath.isEmpty()) |
218 { |
391 { |
219 throw LoadingError{utility::format("'%1' was not found.", referenceName)}; |
392 throw LoadingError{tr("could not find '%1'").arg(referenceName)}; |
220 } |
393 } |
221 QString errorString; |
394 QString loadErrorString; |
222 QTextStream localErrorStream{&errorString}; |
395 QTextStream localErrorStream{&loadErrorString}; |
223 QString resultName = this->openModel( |
396 const std::optional<ModelId> modelIdOpt = this->openModel( |
224 referencedFilePath, |
397 referencedFilePath, |
225 localErrorStream, |
398 localErrorStream, |
226 OpenType::AutomaticallyOpened); |
399 OpenType::AutomaticallyOpened); |
227 if (resultName.isEmpty()) |
400 if (not modelIdOpt.has_value()) |
228 { |
401 { |
229 throw LoadingError{utility::format( |
402 const QString& errorMessage = tr("could not load '%1': %2") |
230 "could not load '%1': %2", |
403 .arg(referencedFilePath) |
231 referencedFilePath, |
404 .arg(loadErrorString); |
232 errorString)}; |
405 throw LoadingError{errorMessage}; |
233 } |
406 } |
234 if (not processed.contains(referenceName)) |
407 modelInfo.dependencies[referenceName] = modelIdOpt.value(); |
|
408 if (not bag.processed.contains(modelIdOpt.value())) |
235 { |
409 { |
236 loadDependenciesForModel(referenceName, path, libraries, missing, processed, errorStream); |
410 this->loadDependenciesForModel(modelIdOpt.value(), referencedFilePath, bag); |
237 } |
411 } |
238 } |
412 } |
239 catch(const LoadingError& error) |
413 catch(const LoadingError& error) |
240 { |
414 { |
241 errorStream << error.message << "\n"; |
415 bag.errorStream << error.message << "\n"; |
242 missing.append(referenceName); |
416 failedToOpen.insert(referenceName); |
243 processed.append(referenceName); |
417 bag.missing.append(referenceName); |
244 } |
418 } |
245 } |
419 } |
246 } |
420 } |
247 } |
421 } |