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