src/lddocument.cpp

changeset 1145
02264bf0108d
parent 1135
8e0691be0b6f
child 1156
c20ee66b6705
equal deleted inserted replaced
1144:4f226fd97826 1145:02264bf0108d
1 /*
2 * LDForge: LDraw parts authoring CAD
3 * Copyright (C) 2013 - 2017 Teemu Piippo
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <QMessageBox>
20 #include <QFileDialog>
21 #include "lddocument.h"
22 #include "miscallenous.h"
23 #include "mainwindow.h"
24 #include "canvas.h"
25 #include "documentloader.h"
26 #include "dialogs/openprogressdialog.h"
27 #include "documentmanager.h"
28 #include "linetypes/comment.h"
29
30 LDDocument::LDDocument (DocumentManager* parent) :
31 Model {parent},
32 HierarchyElement (parent),
33 m_history (new EditHistory (this)),
34 m_savePosition(-1),
35 m_tabIndex(-1),
36 m_manager (parent) {}
37
38 LDDocument::~LDDocument()
39 {
40 m_isBeingDestroyed = true;
41 delete m_history;
42 }
43
44 QString LDDocument::name() const
45 {
46 return m_name;
47 }
48
49 void LDDocument::setName (QString value)
50 {
51 m_name = value;
52 }
53
54 EditHistory* LDDocument::history() const
55 {
56 return m_history;
57 }
58
59 QString LDDocument::fullPath()
60 {
61 return m_fullPath;
62 }
63
64 void LDDocument::setFullPath (QString value)
65 {
66 m_fullPath = value;
67 }
68
69 int LDDocument::tabIndex() const
70 {
71 return m_tabIndex;
72 }
73
74 void LDDocument::setTabIndex (int value)
75 {
76 m_tabIndex = value;
77 }
78
79 const QList<LDPolygon>& LDDocument::polygonData() const
80 {
81 return m_polygonData;
82 }
83
84 long LDDocument::savePosition() const
85 {
86 return m_savePosition;
87 }
88
89 void LDDocument::setSavePosition (long value)
90 {
91 m_savePosition = value;
92 }
93
94 QString LDDocument::defaultName() const
95 {
96 return m_defaultName;
97 }
98
99 void LDDocument::setDefaultName (QString value)
100 {
101 m_defaultName = value;
102 }
103
104 void LDDocument::setFrozen(bool value)
105 {
106 m_isFrozen = value;
107 }
108
109 bool LDDocument::isFrozen() const
110 {
111 return m_isFrozen;
112 }
113
114 void LDDocument::addHistoryStep()
115 {
116 history()->addStep();
117 }
118
119 void LDDocument::undo()
120 {
121 history()->undo();
122 }
123
124 void LDDocument::redo()
125 {
126 history()->redo();
127 }
128
129 void LDDocument::clearHistory()
130 {
131 history()->clear();
132 }
133
134 void LDDocument::addToHistory (AbstractHistoryEntry* entry)
135 {
136 history()->add (entry);
137 }
138
139 void LDDocument::close()
140 {
141 if (not isFrozen())
142 {
143 setFrozen(true);
144 m_manager->documentClosed(this);
145 }
146 }
147
148 // =============================================================================
149 //
150 // Performs safety checks. Do this before closing any files!
151 //
152 bool LDDocument::isSafeToClose()
153 {
154 using msgbox = QMessageBox;
155 setlocale (LC_ALL, "C");
156
157 // If we have unsaved changes, warn and give the option of saving.
158 if (hasUnsavedChanges())
159 {
160 QString message = format (tr ("There are unsaved changes to %1. Should it be saved?"), getDisplayName());
161
162 int button = msgbox::question (m_window, QObject::tr ("Unsaved Changes"), message,
163 (msgbox::Yes | msgbox::No | msgbox::Cancel), msgbox::Cancel);
164
165 switch (button)
166 {
167 case msgbox::Yes:
168 {
169 // If we don't have a file path yet, we have to ask the user for one.
170 if (name().isEmpty())
171 {
172 QString newpath = QFileDialog::getSaveFileName (m_window, QObject::tr ("Save As"),
173 name(), QObject::tr ("LDraw files (*.dat *.ldr)"));
174
175 if (newpath.isEmpty())
176 return false;
177
178 setName (newpath);
179 }
180
181 if (not save())
182 {
183 message = format (QObject::tr ("Failed to save %1 (%2)\nDo you still want to close?"),
184 name(), strerror (errno));
185
186 if (msgbox::critical (m_window, QObject::tr ("Save Failure"), message,
187 (msgbox::Yes | msgbox::No), msgbox::No) == msgbox::No)
188 {
189 return false;
190 }
191 }
192 break;
193 }
194
195 case msgbox::Cancel:
196 return false;
197
198 default:
199 break;
200 }
201 }
202
203 return true;
204 }
205
206 // =============================================================================
207 //
208 bool LDDocument::save (QString path, qint64* sizeptr)
209 {
210 if (isFrozen())
211 return false;
212
213 if (path.isEmpty())
214 path = fullPath();
215
216 // If the second object in the list holds the file name, update that now.
217 LDObject* nameObject = getObject (1);
218
219 if (nameObject and nameObject->type() == LDObjectType::Comment)
220 {
221 LDComment* nameComment = static_cast<LDComment*> (nameObject);
222
223 if (nameComment->text().left (6) == "Name: ")
224 {
225 QString newname = shortenName (path);
226 nameComment->setText (format ("Name: %1", newname));
227 m_window->buildObjectList();
228 }
229 }
230
231 QByteArray data;
232
233 if (sizeptr)
234 *sizeptr = 0;
235
236 // File is open, now save the model to it. Note that LDraw requires files to have DOS line endings.
237 for (LDObject* obj : objects())
238 {
239 QByteArray subdata ((obj->asText() + "\r\n").toUtf8());
240 data.append (subdata);
241
242 if (sizeptr)
243 *sizeptr += countof(subdata);
244 }
245
246 QFile f (path);
247
248 if (not f.open (QIODevice::WriteOnly))
249 return false;
250
251 f.write (data);
252 f.close();
253
254 // We have successfully saved, update the save position now.
255 setSavePosition (history()->position());
256 setFullPath (path);
257 setName (shortenName (path));
258 m_window->updateDocumentListItem (this);
259 m_window->updateTitle();
260 return true;
261 }
262
263 // =============================================================================
264 //
265 void LDDocument::reloadAllSubfiles()
266 {
267 print ("Reloading subfiles of %1", getDisplayName());
268
269 // Go through all objects in the current file and reload the subfiles
270 for (LDObject* obj : objects())
271 {
272 if (obj->type() == LDObjectType::SubfileReference)
273 {
274 LDSubfileReference* reference = static_cast<LDSubfileReference*> (obj);
275 LDDocument* fileInfo = m_documents->getDocumentByName (reference->fileInfo()->name());
276
277 if (fileInfo)
278 reference->setFileInfo (fileInfo);
279 else
280 emplaceReplacement<LDError>(reference, reference->asText(), format("Could not open %1", reference->fileInfo()->name()));
281 }
282
283 // Reparse gibberish files. It could be that they are invalid because
284 // of loading errors. Circumstances may be different now.
285 if (obj->type() == LDObjectType::Error)
286 replaceWithFromString(obj, static_cast<LDError*> (obj)->contents());
287 }
288
289 m_needsRecache = true;
290
291 if (this == m_window->currentDocument())
292 m_window->buildObjectList();
293 }
294
295 // =============================================================================
296 //
297 void LDDocument::insertObject (int pos, LDObject* obj)
298 {
299 Model::insertObject(pos, obj);
300 history()->add(new AddHistoryEntry {pos, obj});
301 connect(obj, SIGNAL(codeChanged(QString,QString)), this, SLOT(objectChanged(QString,QString)));
302
303 #ifdef DEBUG
304 if (not isFrozen())
305 dprint ("Inserted object #%1 (%2) at %3\n", obj->id(), obj->typeName(), pos);
306 #endif
307 }
308
309 void LDDocument::objectChanged(QString before, QString after)
310 {
311 LDObject* object = static_cast<LDObject*>(sender());
312 addToHistory(new EditHistoryEntry {object->lineNumber(), before, after});
313 redoVertices();
314 emit objectModified(object);
315 }
316
317 LDObject* LDDocument::withdrawAt(int position)
318 {
319 LDObject* object = getObject(position);
320
321 if (not isFrozen() and not m_isBeingDestroyed)
322 {
323 history()->add(new DelHistoryEntry {position, object});
324 m_objectVertices.remove(object);
325 }
326
327 m_selection.remove(object);
328 return Model::withdrawAt(position);
329 }
330
331 // =============================================================================
332 //
333 bool LDDocument::hasUnsavedChanges() const
334 {
335 return not isFrozen() and history()->position() != savePosition();
336 }
337
338 // =============================================================================
339 //
340 QString LDDocument::getDisplayName()
341 {
342 if (not name().isEmpty())
343 return name();
344
345 if (not defaultName().isEmpty())
346 return "[" + defaultName() + "]";
347
348 return QObject::tr ("untitled");
349 }
350
351 // =============================================================================
352 //
353 void LDDocument::initializeCachedData()
354 {
355 if (m_needsRecache)
356 {
357 m_vertices.clear();
358 Model model {m_documents};
359 inlineContents(model, true, true);
360
361 for (LDObject* obj : model.objects())
362 {
363 if (obj->type() == LDObjectType::SubfileReference)
364 {
365 print ("Warning: unable to inline %1 into %2",
366 static_cast<LDSubfileReference*> (obj)->fileInfo()->getDisplayName(),
367 getDisplayName());
368 continue;
369 }
370
371 LDPolygon* data = obj->getPolygon();
372
373 if (data)
374 {
375 m_polygonData << *data;
376 delete data;
377 }
378 }
379
380 m_needsRecache = false;
381 }
382
383 if (m_verticesOutdated)
384 {
385 m_objectVertices.clear();
386 Model model {m_documents};
387 inlineContents(model, true, false);
388
389 for (LDObject* object : model)
390 {
391 auto iterator = m_objectVertices.find (object);
392
393 if (iterator == m_objectVertices.end())
394 iterator = m_objectVertices.insert (object, QSet<Vertex>());
395 else
396 iterator->clear();
397
398 object->getVertices (*iterator);
399 }
400
401 m_vertices.clear();
402
403 for (const QSet<Vertex>& vertices : m_objectVertices)
404 m_vertices.unite(vertices);
405
406 m_verticesOutdated = false;
407 }
408 }
409
410 // =============================================================================
411 //
412 QList<LDPolygon> LDDocument::inlinePolygons()
413 {
414 initializeCachedData();
415 return polygonData();
416 }
417
418 // =============================================================================
419 // -----------------------------------------------------------------------------
420 void LDDocument::inlineContents(Model& model, bool deep, bool renderinline)
421 {
422 if (m_manager->preInline(this, model, deep, renderinline))
423 return; // Manager dealt with this inline
424
425 for (LDObject* object : objects())
426 {
427 // Skip those without scemantic meaning
428 if (not object->isScemantic())
429 continue;
430
431 // Got another sub-file reference, inline it if we're deep-inlining. If not,
432 // just add it into the objects normally. Yay, recursion!
433 if (deep and object->type() == LDObjectType::SubfileReference)
434 static_cast<LDSubfileReference*>(object)->inlineContents(model, deep, renderinline);
435 else
436 model.addFromString(object->asText());
437 }
438 }
439
440 // =============================================================================
441 //
442 void LDDocument::addToSelection (LDObject* obj) // [protected]
443 {
444 if (not m_selection.contains(obj) and obj->model() == this)
445 {
446 m_selection.insert(obj);
447 emit objectModified(obj);
448
449 // If this object is inverted with INVERTNEXT, select the INVERTNEXT as well.
450 LDBfc* invertnext;
451
452 if (obj->previousIsInvertnext(invertnext))
453 addToSelection(invertnext);
454 }
455 }
456
457 // =============================================================================
458 //
459 void LDDocument::removeFromSelection (LDObject* obj) // [protected]
460 {
461 if (m_selection.contains(obj))
462 {
463 m_selection.remove(obj);
464 emit objectModified(obj);
465
466 // If this object is inverted with INVERTNEXT, deselect the INVERTNEXT as well.
467 LDBfc* invertnext;
468
469 if (obj->previousIsInvertnext(invertnext))
470 removeFromSelection(invertnext);
471 }
472 }
473
474 // =============================================================================
475 //
476 void LDDocument::clearSelection()
477 {
478 for (LDObject* object : m_selection.toList())
479 removeFromSelection(object);
480 }
481
482 // =============================================================================
483 //
484 const QSet<LDObject*>& LDDocument::getSelection() const
485 {
486 return m_selection;
487 }
488
489 // =============================================================================
490 //
491 bool LDDocument::swapObjects (LDObject* one, LDObject* other)
492 {
493 if (Model::swapObjects(one, other))
494 {
495 addToHistory(new SwapHistoryEntry {one->id(), other->id()});
496 return true;
497 }
498 else
499 {
500 return false;
501 }
502 }
503
504 // =============================================================================
505 //
506 QString LDDocument::shortenName (QString a) // [static]
507 {
508 QString shortname = Basename (a);
509 QString topdirname = Basename (Dirname (a));
510
511 if (DocumentManager::specialSubdirectories.contains (topdirname))
512 shortname.prepend (topdirname + "\\");
513
514 return shortname;
515 }
516
517 // =============================================================================
518 //
519 const QSet<Vertex>& LDDocument::inlineVertices()
520 {
521 initializeCachedData();
522 return m_vertices;
523 }
524
525 void LDDocument::redoVertices()
526 {
527 m_verticesOutdated = true;
528 }

mercurial