src/documentmanager.cpp

changeset 148
e1ced2523cad
parent 147
37f936073cac
child 150
b6cbba6e29a1
equal deleted inserted replaced
147:37f936073cac 148:e1ced2523cad
17 */ 17 */
18 18
19 #include <QFile> 19 #include <QFile>
20 #include <QDir> 20 #include <QDir>
21 #include <QFileInfo> 21 #include <QFileInfo>
22 #include <QSaveFile>
22 #include "documentmanager.h" 23 #include "documentmanager.h"
23 #include "modeleditcontext.h" 24 #include "modeleditcontext.h"
24 #include "linetypes/comment.h" 25 #include "linetypes/comment.h"
25 #include "linetypes/subfilereference.h" 26 #include "linetypes/subfilereference.h"
26 #include "parser.h" 27 #include "parser.h"
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 }

mercurial