src/ldDocument.cc

changeset 655
b376645315ab
child 656
2a1c204df14d
child 706
d79083b9f74d
equal deleted inserted replaced
654:a74f2ff353b8 655:b376645315ab
1 /*
2 * LDForge: LDraw parts authoring CAD
3 * Copyright (C) 2013, 2014 Santeri 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 <QDir>
22 #include <QApplication>
23 #include "main.h"
24 #include "configuration.h"
25 #include "ldDocument.h"
26 #include "miscallenous.h"
27 #include "mainWindow.h"
28 #include "editHistory.h"
29 #include "dialogs.h"
30 #include "glRenderer.h"
31 #include "misc/invokeLater.h"
32
33 cfg (String, io_ldpath, "");
34 cfg (List, io_recentfiles, {});
35 extern_cfg (String, net_downloadpath);
36 extern_cfg (Bool, gl_logostuds);
37
38 static bool g_loadingMainFile = false;
39 static const int g_maxRecentFiles = 10;
40 static bool g_aborted = false;
41 static LDDocumentPointer g_logoedStud = null;
42 static LDDocumentPointer g_logoedStud2 = null;
43
44 LDDocument* LDDocument::m_curdoc = null;
45
46 const QStringList g_specialSubdirectories ({ "s", "48", "8" });
47
48 // =============================================================================
49 //
50 namespace LDPaths
51 {
52 static QString pathError;
53
54 struct
55 {
56 QString LDConfigPath;
57 QString partsPath, primsPath;
58 } pathInfo;
59
60 void initPaths()
61 {
62 if (!tryConfigure (io_ldpath))
63 {
64 LDrawPathDialog dlg (false);
65
66 if (!dlg.exec())
67 exit (0);
68
69 io_ldpath = dlg.filename();
70 }
71 }
72
73 bool tryConfigure (QString path)
74 {
75 QDir dir;
76
77 if (!dir.cd (path))
78 {
79 pathError = "Directory does not exist.";
80 return false;
81 }
82
83 QStringList mustHave = { "LDConfig.ldr", "parts", "p" };
84 QStringList contents = dir.entryList (mustHave);
85
86 if (contents.size() != mustHave.size())
87 {
88 pathError = "Not an LDraw directory! Must<br />have LDConfig.ldr, parts/ and p/.";
89 return false;
90 }
91
92 pathInfo.partsPath = format ("%1" DIRSLASH "parts", path);
93 pathInfo.LDConfigPath = format ("%1" DIRSLASH "LDConfig.ldr", path);
94 pathInfo.primsPath = format ("%1" DIRSLASH "p", path);
95
96 return true;
97 }
98
99 // Accessors
100 QString getError()
101 {
102 return pathError;
103 }
104
105 QString ldconfig()
106 {
107 return pathInfo.LDConfigPath;
108 }
109
110 QString prims()
111 {
112 return pathInfo.primsPath;
113 }
114
115 QString parts()
116 {
117 return pathInfo.partsPath;
118 }
119 }
120
121 // =============================================================================
122 //
123 LDDocument::LDDocument() :
124 m_gldata (new LDGLData)
125 {
126 setImplicit (true);
127 setSavePosition (-1);
128 setTabIndex (-1);
129 setHistory (new History);
130 history()->setDocument (this);
131 }
132
133 // =============================================================================
134 //
135 LDDocument::~LDDocument()
136 {
137 // Remove this file from the list of files. This MUST be done FIRST, otherwise
138 // a ton of other functions will think this file is still valid when it is not!
139 g_loadedFiles.removeOne (this);
140
141 m_history->setIgnoring (true);
142
143 // Clear everything from the model
144 for (LDObject* obj : objects())
145 obj->destroy();
146
147 // Clear the cache as well
148 for (LDObject* obj : cache())
149 obj->destroy();
150
151 delete m_history;
152 delete m_gldata;
153
154 // If we just closed the current file, we need to set the current
155 // file as something else.
156 if (this == getCurrentDocument())
157 {
158 bool found = false;
159
160 // Try find an explicitly loaded file - if we can't find one,
161 // we need to create a new file to switch to.
162 for (LDDocument* file : g_loadedFiles)
163 {
164 if (!file->isImplicit())
165 {
166 LDDocument::setCurrent (file);
167 found = true;
168 break;
169 }
170 }
171
172 if (!found)
173 newFile();
174 }
175
176 if (this == g_logoedStud)
177 g_logoedStud = null;
178 elif (this == g_logoedStud2)
179 g_logoedStud2 = null;
180
181 g_win->updateDocumentList();
182 print ("Closed %1", name());
183 }
184
185 // =============================================================================
186 //
187 LDDocument* findDocument (QString name)
188 {
189 for (LDDocument * file : g_loadedFiles)
190 if (!file->name().isEmpty() && file->name() == name)
191 return file;
192
193 return null;
194 }
195
196 // =============================================================================
197 //
198 QString dirname (QString path)
199 {
200 long lastpos = path.lastIndexOf (DIRSLASH);
201
202 if (lastpos > 0)
203 return path.left (lastpos);
204
205 #ifndef _WIN32
206 if (path[0] == DIRSLASH_CHAR)
207 return DIRSLASH;
208 #endif // _WIN32
209
210 return "";
211 }
212
213 // =============================================================================
214 //
215 QString basename (QString path)
216 {
217 long lastpos = path.lastIndexOf (DIRSLASH);
218
219 if (lastpos != -1)
220 return path.mid (lastpos + 1);
221
222 return path;
223 }
224
225 // =============================================================================
226 //
227 static QString findLDrawFilePath (QString relpath, bool subdirs)
228 {
229 QString fullPath;
230
231 // LDraw models use Windows-style path separators. If we're not on Windows,
232 // replace the path separator now before opening any files. Qt expects
233 // forward-slashes as directory separators.
234 #ifndef WIN32
235 relpath.replace ("\\", "/");
236 #endif // WIN32
237
238 // Try find it relative to other currently open documents. We want a file
239 // in the immediate vicinity of a current model to override stock LDraw stuff.
240 QString reltop = basename (dirname (relpath));
241
242 for (LDDocument* doc : g_loadedFiles)
243 {
244 if (doc->fullPath().isEmpty())
245 continue;
246
247 QString partpath = format ("%1/%2", dirname (doc->fullPath()), relpath);
248 QFile f (partpath);
249
250 if (f.exists())
251 {
252 // ensure we don't mix subfiles and 48-primitives with non-subfiles and non-48
253 QString proptop = basename (dirname (partpath));
254
255 bool bogus = false;
256
257 for (QString s : g_specialSubdirectories)
258 {
259 if ((proptop == s && reltop != s) || (reltop == s && proptop != s))
260 {
261 bogus = true;
262 break;
263 }
264 }
265
266 if (!bogus)
267 return partpath;
268 }
269 }
270
271 if (QFile::exists (relpath))
272 return relpath;
273
274 // Try with just the LDraw path first
275 fullPath = format ("%1" DIRSLASH "%2", io_ldpath, relpath);
276
277 if (QFile::exists (fullPath))
278 return fullPath;
279
280 if (subdirs)
281 {
282 // Look in sub-directories: parts and p. Also look in net_downloadpath, since that's
283 // where we download parts from the PT to.
284 for (const QString& topdir : QList<QString> ({ io_ldpath, net_downloadpath }))
285 {
286 for (const QString& subdir : QList<QString> ({ "parts", "p" }))
287 {
288 fullPath = format ("%1" DIRSLASH "%2" DIRSLASH "%3", topdir, subdir, relpath);
289
290 if (QFile::exists (fullPath))
291 return fullPath;
292 }
293 }
294 }
295
296 // Did not find the file.
297 return "";
298 }
299
300 QFile* openLDrawFile (QString relpath, bool subdirs, QString* pathpointer)
301 {
302 print ("Opening %1...\n", relpath);
303 QString path = findLDrawFilePath (relpath, subdirs);
304
305 if (pathpointer != null)
306 *pathpointer = path;
307
308 if (path.isEmpty())
309 return null;
310
311 QFile* fp = new QFile (path);
312
313 if (fp->open (QIODevice::ReadOnly))
314 return fp;
315
316 fp->deleteLater();
317 return null;
318 }
319
320 // =============================================================================
321 //
322 void LDFileLoader::start()
323 {
324 setDone (false);
325 setProgress (0);
326 setAborted (false);
327
328 if (isOnForeground())
329 {
330 g_aborted = false;
331
332 // Show a progress dialog if we're loading the main ldDocument.here so we can
333 // show progress updates and keep the WM posted that we're still here.
334 // Of course we cannot exec() the dialog because then the dialog would
335 // block.
336 dlg = new OpenProgressDialog (g_win);
337 dlg->setNumLines (lines().size());
338 dlg->setModal (true);
339 dlg->show();
340
341 // Connect the loader in so we can show updates
342 connect (this, SIGNAL (workDone()), dlg, SLOT (accept()));
343 connect (dlg, SIGNAL (rejected()), this, SLOT (abort()));
344 }
345 else
346 dlg = null;
347
348 // Begin working
349 work (0);
350 }
351
352 // =============================================================================
353 //
354 void LDFileLoader::work (int i)
355 {
356 // User wishes to abort, so stop here now.
357 if (isAborted())
358 {
359 for (LDObject* obj : m_objects)
360 obj->destroy();
361
362 m_objects.clear();
363 setDone (true);
364 return;
365 }
366
367 // Parse up to 300 lines per iteration
368 int max = i + 300;
369
370 for (; i < max && i < (int) lines().size(); ++i)
371 {
372 QString line = lines()[i];
373
374 // Trim the trailing newline
375 QChar c;
376
377 while (line.endsWith ("\n") || line.endsWith ("\r"))
378 line.chop (1);
379
380 LDObject* obj = parseLine (line);
381
382 // Check for parse errors and warn about tthem
383 if (obj->type() == LDObject::EError)
384 {
385 print ("Couldn't parse line #%1: %2", progress() + 1, static_cast<LDError*> (obj)->reason());
386
387 if (warnings() != null)
388 (*warnings())++;
389 }
390
391 m_objects << obj;
392 setProgress (i);
393
394 // If we have a dialog pointer, update the progress now
395 if (isOnForeground())
396 dlg->updateProgress (i);
397 }
398
399 // If we're done now, tell the environment we're done and stop.
400 if (i >= ((int) lines().size()) - 1)
401 {
402 emit workDone();
403 setDone (true);
404 return;
405 }
406
407 // Otherwise, continue, by recursing back.
408 if (!isDone())
409 {
410 // If we have a dialog to show progress output to, we cannot just call
411 // work() again immediately as the dialog needs some processor cycles as
412 // well. Thus, take a detour through the event loop by using the
413 // meta-object system.
414 //
415 // This terminates the loop here and control goes back to the function
416 // which called the file loader. It will keep processing the event loop
417 // until we're ready (see loadFileContents), thus the event loop will
418 // eventually catch the invokation we throw here and send us back. Though
419 // it's not technically recursion anymore, more like a for loop. :P
420 if (isOnForeground())
421 QMetaObject::invokeMethod (this, "work", Qt::QueuedConnection, Q_ARG (int, i));
422 else
423 work (i);
424 }
425 }
426
427 // =============================================================================
428 //
429 void LDFileLoader::abort()
430 {
431 setAborted (true);
432
433 if (isOnForeground())
434 g_aborted = true;
435 }
436
437 // =============================================================================
438 //
439 LDObjectList loadFileContents (QFile* fp, int* numWarnings, bool* ok)
440 {
441 QStringList lines;
442 LDObjectList objs;
443
444 if (numWarnings)
445 *numWarnings = 0;
446
447 // Read in the lines
448 while (fp->atEnd() == false)
449 lines << QString::fromUtf8 (fp->readLine());
450
451 LDFileLoader* loader = new LDFileLoader;
452 loader->setWarnings (numWarnings);
453 loader->setLines (lines);
454 loader->setOnForeground (g_loadingMainFile);
455 loader->start();
456
457 // After start() returns, if the loader isn't done yet, it's delaying
458 // its next iteration through the event loop. We need to catch this here
459 // by telling the event loop to tick, which will tick the file loader again.
460 // We keep doing this until the file loader is ready.
461 while (loader->isDone() == false)
462 qApp->processEvents();
463
464 // If we wanted the success value, supply that now
465 if (ok)
466 *ok = !loader->isAborted();
467
468 objs = loader->objects();
469 return objs;
470 }
471
472 // =============================================================================
473 //
474 LDDocument* openDocument (QString path, bool search)
475 {
476 // Convert the file name to lowercase since some parts contain uppercase
477 // file names. I'll assume here that the library will always use lowercase
478 // file names for the actual parts..
479 QFile* fp;
480 QString fullpath;
481
482 if (search)
483 fp = openLDrawFile (path.toLower(), true, &fullpath);
484 else
485 {
486 fp = new QFile (path);
487 fullpath = path;
488
489 if (!fp->open (QIODevice::ReadOnly))
490 {
491 delete fp;
492 return null;
493 }
494 }
495
496 if (!fp)
497 return null;
498
499 LDDocument* load = new LDDocument;
500 load->setFullPath (fullpath);
501 load->setName (LDDocument::shortenName (load->fullPath()));
502 dprint ("name: %1 (%2)", load->name(), load->fullPath());
503 g_loadedFiles << load;
504
505 // Don't take the file loading as actual edits to the file
506 load->history()->setIgnoring (true);
507
508 int numWarnings;
509 bool ok;
510 LDObjectList objs = loadFileContents (fp, &numWarnings, &ok);
511 fp->close();
512 fp->deleteLater();
513
514 if (!ok)
515 {
516 g_loadedFiles.removeOne (load);
517 delete load;
518 return null;
519 }
520
521 load->addObjects (objs);
522
523 if (g_loadingMainFile)
524 {
525 LDDocument::setCurrent (load);
526 g_win->R()->setDocument (load);
527 print (QObject::tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings);
528 }
529
530 load->history()->setIgnoring (false);
531 return load;
532 }
533
534 // =============================================================================
535 //
536 bool LDDocument::isSafeToClose()
537 {
538 typedef QMessageBox msgbox;
539 setlocale (LC_ALL, "C");
540
541 // If we have unsaved changes, warn and give the option of saving.
542 if (hasUnsavedChanges())
543 {
544 QString message = format (tr ("There are unsaved changes to %1. Should it be saved?"),
545 (name().length() > 0) ? name() : tr ("<anonymous>"));
546
547 int button = msgbox::question (g_win, tr ("Unsaved Changes"), message,
548 (msgbox::Yes | msgbox::No | msgbox::Cancel), msgbox::Cancel);
549
550 switch (button)
551 {
552 case msgbox::Yes:
553 {
554 // If we don't have a file path yet, we have to ask the user for one.
555 if (name().length() == 0)
556 {
557 QString newpath = QFileDialog::getSaveFileName (g_win, tr ("Save As"),
558 getCurrentDocument()->name(), tr ("LDraw files (*.dat *.ldr)"));
559
560 if (newpath.length() == 0)
561 return false;
562
563 setName (newpath);
564 }
565
566 if (!save())
567 {
568 message = format (tr ("Failed to save %1 (%2)\nDo you still want to close?"),
569 name(), strerror (errno));
570
571 if (msgbox::critical (g_win, tr ("Save Failure"), message,
572 (msgbox::Yes | msgbox::No), msgbox::No) == msgbox::No)
573 {
574 return false;
575 }
576 }
577 } break;
578
579 case msgbox::Cancel:
580 return false;
581
582 default:
583 break;
584 }
585 }
586
587 return true;
588 }
589
590 // =============================================================================
591 //
592 void closeAll()
593 {
594 // Remove all loaded files and the objects they contain
595 QList<LDDocument*> files = g_loadedFiles;
596
597 for (LDDocument* file : files)
598 delete file;
599 }
600
601 // =============================================================================
602 //
603 void newFile()
604 {
605 // Create a new anonymous file and set it to our current
606 LDDocument* f = new LDDocument;
607 f->setName ("");
608 f->setImplicit (false);
609 g_loadedFiles << f;
610 LDDocument::setCurrent (f);
611 LDDocument::closeInitialFile();
612 g_win->R()->setDocument (f);
613 g_win->doFullRefresh();
614 g_win->updateTitle();
615 g_win->updateActions();
616 }
617
618 // =============================================================================
619 //
620 void addRecentFile (QString path)
621 {
622 auto& rfiles = io_recentfiles;
623 int idx = rfiles.indexOf (path);
624
625 // If this file already is in the list, pop it out.
626 if (idx != -1)
627 {
628 if (rfiles.size() == 1)
629 return; // only recent file - abort and do nothing
630
631 // Pop it out.
632 rfiles.removeAt (idx);
633 }
634
635 // If there's too many recent files, drop one out.
636 while (rfiles.size() > (g_maxRecentFiles - 1))
637 rfiles.removeAt (0);
638
639 // Add the file
640 rfiles << path;
641
642 Config::save();
643 g_win->updateRecentFilesMenu();
644 }
645
646 // =============================================================================
647 // Open an LDraw file and set it as the main model
648 // =============================================================================
649 void openMainFile (QString path)
650 {
651 g_loadingMainFile = true;
652
653 // If there's already a file with the same name, this file must replace it.
654 LDDocument* documentToReplace = null;
655 QString shortName = LDDocument::shortenName (path);
656
657 for (LDDocument* doc : g_loadedFiles)
658 {
659 if (doc->name() == shortName)
660 {
661 documentToReplace = doc;
662 break;
663 }
664 }
665
666 // We cannot open this file if the document this would replace is not
667 // safe to close.
668 if (documentToReplace != null && documentToReplace->isSafeToClose() == false)
669 {
670 g_loadingMainFile = false;
671 return;
672 }
673
674 LDDocument* file = openDocument (path, false);
675
676 if (!file)
677 {
678 // Loading failed, thus drop down to a new file since we
679 // closed everything prior.
680 newFile();
681
682 if (!g_aborted)
683 {
684 // Tell the user loading failed.
685 setlocale (LC_ALL, "C");
686 critical (format (QObject::tr ("Failed to open %1: %2"), path, strerror (errno)));
687 }
688
689 g_loadingMainFile = false;
690 return;
691 }
692
693 file->setImplicit (false);
694
695 // Replace references to the old file with the new file.
696 if (documentToReplace != null)
697 {
698 for (LDDocumentPointer* ptr : documentToReplace->references())
699 { dprint ("ptr: %1 (%2)\n",
700 ptr, ptr->pointer() ? ptr->pointer()->name() : "<null>");
701
702 *ptr = file;
703 }
704
705 assert (documentToReplace->references().isEmpty());
706 delete documentToReplace;
707 }
708
709 // If we have an anonymous, unchanged file open as the only open file
710 // (aside of the one we just opened), close it now.
711 LDDocument::closeInitialFile();
712
713 // Rebuild the object tree view now.
714 LDDocument::setCurrent (file);
715 g_win->doFullRefresh();
716
717 // Add it to the recent files list.
718 addRecentFile (path);
719 g_loadingMainFile = false;
720 }
721
722 // =============================================================================
723 //
724 bool LDDocument::save (QString savepath)
725 {
726 if (!savepath.length())
727 savepath = fullPath();
728
729 QFile f (savepath);
730
731 if (!f.open (QIODevice::WriteOnly))
732 return false;
733
734 // If the second object in the list holds the file name, update that now.
735 // Only do this if the file is explicitly open.
736 LDObject* nameObject = getObject (1);
737
738 if (!isImplicit() && nameObject != null && nameObject->type() == LDObject::EComment)
739 {
740 LDComment* nameComment = static_cast<LDComment*> (nameObject);
741
742 if (nameComment->text().left (6) == "Name: ")
743 {
744 QString newname = shortenName (savepath);
745 nameComment->setText (format ("Name: %1", newname));
746 g_win->buildObjList();
747 }
748 }
749
750 // File is open, now save the model to it. Note that LDraw requires files to
751 // have DOS line endings, so we terminate the lines with \r\n.
752 for (LDObject* obj : objects())
753 f.write ((obj->asText() + "\r\n").toUtf8());
754
755 // File is saved, now clean up.
756 f.close();
757
758 // We have successfully saved, update the save position now.
759 setSavePosition (history()->position());
760 setFullPath (savepath);
761 setName (shortenName (savepath));
762
763 g_win->updateDocumentListItem (this);
764 g_win->updateTitle();
765 return true;
766 }
767
768 // =============================================================================
769 //
770 class LDParseError : public std::exception
771 {
772 PROPERTY (private, QString, error, setError, STOCK_WRITE)
773 PROPERTY (private, QString, line, setLine, STOCK_WRITE)
774
775 public:
776 LDParseError (QString line, QString a) :
777 m_error (a),
778 m_line (line) {}
779
780 const char* what() const throw()
781 {
782 return qPrintable (error());
783 }
784 };
785
786 // =============================================================================
787 //
788 void checkTokenCount (QString line, const QStringList& tokens, int num)
789 {
790 if (tokens.size() != num)
791 throw LDParseError (line, format ("Bad amount of tokens, expected %1, got %2", num, tokens.size()));
792 }
793
794 // =============================================================================
795 //
796 void checkTokenNumbers (QString line, const QStringList& tokens, int min, int max)
797 {
798 bool ok;
799
800 // Check scientific notation, e.g. 7.99361e-15
801 QRegExp scient ("\\-?[0-9]+\\.[0-9]+e\\-[0-9]+");
802
803 for (int i = min; i <= max; ++i)
804 {
805 tokens[i].toDouble (&ok);
806
807 if (!ok && !scient.exactMatch (tokens[i]))
808 throw LDParseError (line, format ("Token #%1 was `%2`, expected a number (matched length: %3)", (i + 1), tokens[i], scient.matchedLength()));
809 }
810 }
811
812 // =============================================================================
813 //
814 static Vertex parseVertex (QStringList& s, const int n)
815 {
816 Vertex v;
817
818 for_axes (ax)
819 v[ax] = s[n + ax].toDouble();
820
821 return v;
822 }
823
824 // =============================================================================
825 // This is the LDraw code parser function. It takes in a string containing LDraw
826 // code and returns the object parsed from it. parseLine never returns null,
827 // the object will be LDError if it could not be parsed properly.
828 // =============================================================================
829 LDObject* parseLine (QString line)
830 {
831 try
832 {
833 QStringList tokens = line.split (" ", QString::SkipEmptyParts);
834
835 if (tokens.size() <= 0)
836 {
837 // Line was empty, or only consisted of whitespace
838 return new LDEmpty;
839 }
840
841 if (tokens[0].length() != 1 || tokens[0][0].isDigit() == false)
842 throw LDParseError (line, "Illogical line code");
843
844 int num = tokens[0][0].digitValue();
845
846 switch (num)
847 {
848 case 0:
849 {
850 // Comment
851 QString comm = line.mid (line.indexOf ("0") + 1).simplified();
852
853 // Handle BFC statements
854 if (tokens.size() > 2 && tokens[1] == "BFC")
855 {
856 for (int i = 0; i < LDBFC::NumStatements; ++i)
857 if (comm == format ("BFC %1", LDBFC::k_statementStrings [i]))
858 return new LDBFC ( (LDBFC::Statement) i);
859
860 // MLCAD is notorious for stuffing these statements in parts it
861 // creates. The above block only handles valid statements, so we
862 // need to handle MLCAD-style invertnext, clip and noclip separately.
863 struct
864 {
865 QString a;
866 LDBFC::Statement b;
867 } BFCData[] =
868 {
869 { "INVERTNEXT", LDBFC::InvertNext },
870 { "NOCLIP", LDBFC::NoClip },
871 { "CLIP", LDBFC::Clip }
872 };
873
874 for (const auto& i : BFCData)
875 if (comm == "BFC CERTIFY " + i.a)
876 return new LDBFC (i.b);
877 }
878
879 if (tokens.size() > 2 && tokens[1] == "!LDFORGE")
880 {
881 // Handle LDForge-specific types, they're embedded into comments too
882 if (tokens[2] == "VERTEX")
883 {
884 // Vertex (0 !LDFORGE VERTEX)
885 checkTokenCount (line, tokens, 7);
886 checkTokenNumbers (line, tokens, 3, 6);
887
888 LDVertex* obj = new LDVertex;
889 obj->setColor (tokens[3].toLong());
890
891 for_axes (ax)
892 obj->pos[ax] = tokens[4 + ax].toDouble(); // 4 - 6
893
894 return obj;
895 } elif (tokens[2] == "OVERLAY")
896 {
897 checkTokenCount (line, tokens, 9);;
898 checkTokenNumbers (line, tokens, 5, 8);
899
900 LDOverlay* obj = new LDOverlay;
901 obj->setFileName (tokens[3]);
902 obj->setCamera (tokens[4].toLong());
903 obj->setX (tokens[5].toLong());
904 obj->setY (tokens[6].toLong());
905 obj->setWidth (tokens[7].toLong());
906 obj->setHeight (tokens[8].toLong());
907 return obj;
908 }
909 }
910
911 // Just a regular comment:
912 LDComment* obj = new LDComment;
913 obj->setText (comm);
914 return obj;
915 }
916
917 case 1:
918 {
919 // Subfile
920 checkTokenCount (line, tokens, 15);
921 checkTokenNumbers (line, tokens, 1, 13);
922
923 // Try open the file. Disable g_loadingMainFile temporarily since we're
924 // not loading the main file now, but the subfile in question.
925 bool tmp = g_loadingMainFile;
926 g_loadingMainFile = false;
927 LDDocument* load = getDocument (tokens[14]);
928 g_loadingMainFile = tmp;
929
930 // If we cannot open the file, mark it an error. Note we cannot use LDParseError
931 // here because the error object needs the document reference.
932 if (!load)
933 {
934 LDError* obj = new LDError (line, format ("Could not open %1", tokens[14]));
935 obj->setFileReferenced (tokens[14]);
936 return obj;
937 }
938
939 LDSubfile* obj = new LDSubfile;
940 obj->setColor (tokens[1].toLong());
941 obj->setPosition (parseVertex (tokens, 2)); // 2 - 4
942
943 Matrix transform;
944
945 for (int i = 0; i < 9; ++i)
946 transform[i] = tokens[i + 5].toDouble(); // 5 - 13
947
948 obj->setTransform (transform);
949 obj->setFileInfo (load);
950 return obj;
951 }
952
953 case 2:
954 {
955 checkTokenCount (line, tokens, 8);
956 checkTokenNumbers (line, tokens, 1, 7);
957
958 // Line
959 LDLine* obj = new LDLine;
960 obj->setColor (tokens[1].toLong());
961
962 for (int i = 0; i < 2; ++i)
963 obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 7
964
965 return obj;
966 }
967
968 case 3:
969 {
970 checkTokenCount (line, tokens, 11);
971 checkTokenNumbers (line, tokens, 1, 10);
972
973 // Triangle
974 LDTriangle* obj = new LDTriangle;
975 obj->setColor (tokens[1].toLong());
976
977 for (int i = 0; i < 3; ++i)
978 obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 10
979
980 return obj;
981 }
982
983 case 4:
984 case 5:
985 {
986 checkTokenCount (line, tokens, 14);
987 checkTokenNumbers (line, tokens, 1, 13);
988
989 // Quadrilateral / Conditional line
990 LDObject* obj;
991
992 if (num == 4)
993 obj = new LDQuad;
994 else
995 obj = new LDCondLine;
996
997 obj->setColor (tokens[1].toLong());
998
999 for (int i = 0; i < 4; ++i)
1000 obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 13
1001
1002 return obj;
1003 }
1004
1005 default: // Strange line we couldn't parse
1006 throw LDError (line, "Unknown line code number");
1007 }
1008 }
1009 catch (LDParseError& e)
1010 {
1011 return new LDError (e.line(), e.error());
1012 }
1013 }
1014
1015 // =============================================================================
1016 //
1017 LDDocument* getDocument (QString filename)
1018 {
1019 // Try find the file in the list of loaded files
1020 LDDocument* doc = findDocument (filename);
1021
1022 // If it's not loaded, try open it
1023 if (!doc)
1024 doc = openDocument (filename, true);
1025
1026 return doc;
1027 }
1028
1029 // =============================================================================
1030 //
1031 void reloadAllSubfiles()
1032 {
1033 if (!getCurrentDocument())
1034 return;
1035
1036 g_loadedFiles.clear();
1037 g_loadedFiles << getCurrentDocument();
1038
1039 // Go through all objects in the current file and reload the subfiles
1040 for (LDObject* obj : getCurrentDocument()->objects())
1041 {
1042 if (obj->type() == LDObject::ESubfile)
1043 {
1044 LDSubfile* ref = static_cast<LDSubfile*> (obj);
1045 LDDocument* fileInfo = getDocument (ref->fileInfo()->name());
1046
1047 if (fileInfo)
1048 ref->setFileInfo (fileInfo);
1049 else
1050 ref->replace (new LDError (ref->asText(), format ("Could not open %1", ref->fileInfo()->name())));
1051 }
1052
1053 // Reparse gibberish files. It could be that they are invalid because
1054 // of loading errors. Circumstances may be different now.
1055 if (obj->type() == LDObject::EError)
1056 obj->replace (parseLine (static_cast<LDError*> (obj)->contents()));
1057 }
1058 }
1059
1060 // =============================================================================
1061 //
1062 int LDDocument::addObject (LDObject* obj)
1063 {
1064 history()->add (new AddHistory (objects().size(), obj));
1065 m_objects << obj;
1066
1067 if (obj->type() == LDObject::EVertex)
1068 m_vertices << obj;
1069
1070 #ifdef DEBUG
1071 if (!isImplicit())
1072 dprint ("Added object #%1 (%2)\n", obj->id(), obj->typeName());
1073 #endif
1074
1075 obj->setDocument (this);
1076 return getObjectCount() - 1;
1077 }
1078
1079 // =============================================================================
1080 //
1081 void LDDocument::addObjects (const LDObjectList objs)
1082 {
1083 for (LDObject* obj : objs)
1084 if (obj)
1085 addObject (obj);
1086 }
1087
1088 // =============================================================================
1089 //
1090 void LDDocument::insertObj (int pos, LDObject* obj)
1091 {
1092 history()->add (new AddHistory (pos, obj));
1093 m_objects.insert (pos, obj);
1094 obj->setDocument (this);
1095
1096 #ifdef DEBUG
1097 if (!isImplicit())
1098 dprint ("Inserted object #%1 (%2) at %3\n", obj->id(), obj->typeName(), pos);
1099 #endif
1100 }
1101
1102 // =============================================================================
1103 //
1104 void LDDocument::forgetObject (LDObject* obj)
1105 {
1106 int idx = obj->lineNumber();
1107 obj->unselect();
1108 assert (m_objects[idx] == obj);
1109
1110 if (!history()->isIgnoring())
1111 history()->add (new DelHistory (idx, obj));
1112
1113 m_objects.removeAt (idx);
1114 obj->setDocument (null);
1115 }
1116
1117 // =============================================================================
1118 //
1119 bool safeToCloseAll()
1120 {
1121 for (LDDocument* f : g_loadedFiles)
1122 if (!f->isSafeToClose())
1123 return false;
1124
1125 return true;
1126 }
1127
1128 // =============================================================================
1129 //
1130 void LDDocument::setObject (int idx, LDObject* obj)
1131 {
1132 assert (idx >= 0 && idx < m_objects.size());
1133
1134 // Mark this change to history
1135 if (!m_history->isIgnoring())
1136 {
1137 QString oldcode = getObject (idx)->asText();
1138 QString newcode = obj->asText();
1139 *m_history << new EditHistory (idx, oldcode, newcode);
1140 }
1141
1142 m_objects[idx]->unselect();
1143 m_objects[idx]->setDocument (null);
1144 obj->setDocument (this);
1145 m_objects[idx] = obj;
1146 }
1147
1148 // =============================================================================
1149 //
1150 // Close all documents we don't need anymore
1151 //
1152 void LDDocument::closeUnused()
1153 {
1154 for (LDDocument* file : g_loadedFiles)
1155 if (file->isImplicit() && file->references().isEmpty())
1156 delete file;
1157 }
1158
1159 // =============================================================================
1160 //
1161 LDObject* LDDocument::getObject (int pos) const
1162 {
1163 if (m_objects.size() <= pos)
1164 return null;
1165
1166 return m_objects[pos];
1167 }
1168
1169 // =============================================================================
1170 //
1171 int LDDocument::getObjectCount() const
1172 {
1173 return objects().size();
1174 }
1175
1176 // =============================================================================
1177 //
1178 bool LDDocument::hasUnsavedChanges() const
1179 {
1180 return !isImplicit() && history()->position() != savePosition();
1181 }
1182
1183 // =============================================================================
1184 //
1185 QString LDDocument::getDisplayName()
1186 {
1187 if (!name().isEmpty())
1188 return name();
1189
1190 if (!defaultName().isEmpty())
1191 return "[" + defaultName() + "]";
1192
1193 return tr ("<anonymous>");
1194 }
1195
1196 // =============================================================================
1197 //
1198 LDObjectList LDDocument::inlineContents (LDSubfile::InlineFlags flags)
1199 {
1200 // Possibly substitute with logoed studs:
1201 // stud.dat -> stud-logo.dat
1202 // stud2.dat -> stud-logo2.dat
1203 if (gl_logostuds && (flags & LDSubfile::RendererInline))
1204 {
1205 // Ensure logoed studs are loaded first
1206 loadLogoedStuds();
1207
1208 if (name() == "stud.dat" && g_logoedStud)
1209 return g_logoedStud->inlineContents (flags);
1210 elif (name() == "stud2.dat" && g_logoedStud2)
1211 return g_logoedStud2->inlineContents (flags);
1212 }
1213
1214 LDObjectList objs, objcache;
1215
1216 bool deep = flags & LDSubfile::DeepInline,
1217 doCache = flags & LDSubfile::CacheInline;
1218
1219 if (m_needsCache)
1220 {
1221 m_cache.clear();
1222 doCache = true;
1223 }
1224
1225 // If we have this cached, just create a copy of that
1226 if (deep && cache().isEmpty() == false)
1227 {
1228 for (LDObject* obj : cache())
1229 objs << obj->createCopy();
1230 }
1231 else
1232 {
1233 if (!deep)
1234 doCache = false;
1235
1236 for (LDObject* obj : objects())
1237 {
1238 // Skip those without scemantic meaning
1239 if (!obj->isScemantic())
1240 continue;
1241
1242 // Got another sub-file reference, inline it if we're deep-inlining. If not,
1243 // just add it into the objects normally. Also, we only cache immediate
1244 // subfiles and this is not one. Yay, recursion!
1245 if (deep && obj->type() == LDObject::ESubfile)
1246 {
1247 LDSubfile* ref = static_cast<LDSubfile*> (obj);
1248
1249 // We only want to cache immediate subfiles, so shed the caching
1250 // flag when recursing deeper in hierarchy.
1251 LDObjectList otherobjs = ref->inlineContents (flags & ~ (LDSubfile::CacheInline));
1252
1253 for (LDObject* otherobj : otherobjs)
1254 {
1255 // Cache this object, if desired
1256 if (doCache)
1257 objcache << otherobj->createCopy();
1258
1259 objs << otherobj;
1260 }
1261 }
1262 else
1263 {
1264 if (doCache)
1265 objcache << obj->createCopy();
1266
1267 objs << obj->createCopy();
1268 }
1269 }
1270
1271 if (doCache)
1272 setCache (objcache);
1273 }
1274
1275 return objs;
1276 }
1277
1278 // =============================================================================
1279 //
1280 LDDocument* LDDocument::current()
1281 {
1282 return m_curdoc;
1283 }
1284
1285 // =============================================================================
1286 // Sets the given file as the current one on display. At some point in time this
1287 // was an operation completely unheard of. ;)
1288 //
1289 // TODO: f can be temporarily null. This probably should not be the case.
1290 // =============================================================================
1291 void LDDocument::setCurrent (LDDocument* f)
1292 {
1293 // Implicit files were loaded for caching purposes and must never be set
1294 // current.
1295 if (f && f->isImplicit())
1296 return;
1297
1298 m_curdoc = f;
1299
1300 if (g_win && f)
1301 {
1302 // A ton of stuff needs to be updated
1303 g_win->updateDocumentListItem (f);
1304 g_win->buildObjList();
1305 g_win->updateTitle();
1306 g_win->R()->setDocument (f);
1307 g_win->R()->repaint();
1308 print ("Changed file to %1", f->getDisplayName());
1309 }
1310 }
1311
1312 // =============================================================================
1313 //
1314 int LDDocument::countExplicitFiles()
1315 {
1316 int count = 0;
1317
1318 for (LDDocument* f : g_loadedFiles)
1319 if (f->isImplicit() == false)
1320 count++;
1321
1322 return count;
1323 }
1324
1325 // =============================================================================
1326 // This little beauty closes the initial file that was open at first when opening
1327 // a new file over it.
1328 // =============================================================================
1329 void LDDocument::closeInitialFile()
1330 {
1331 if (
1332 countExplicitFiles() == 2 &&
1333 g_loadedFiles[0]->name().isEmpty() &&
1334 g_loadedFiles[1]->name().isEmpty() == false &&
1335 !g_loadedFiles[0]->hasUnsavedChanges()
1336 )
1337 delete g_loadedFiles[0];
1338 }
1339
1340 // =============================================================================
1341 //
1342 void loadLogoedStuds()
1343 {
1344 if (g_logoedStud && g_logoedStud2)
1345 return;
1346
1347 delete g_logoedStud;
1348 delete g_logoedStud2;
1349
1350 g_logoedStud = openDocument ("stud-logo.dat", true);
1351 g_logoedStud2 = openDocument ("stud2-logo.dat", true);
1352
1353 print (LDDocument::tr ("Logoed studs loaded.\n"));
1354 }
1355
1356 // =============================================================================
1357 //
1358 void LDDocument::addToSelection (LDObject* obj) // [protected]
1359 {
1360 if (obj->isSelected())
1361 return;
1362
1363 assert (obj->document() == this);
1364 m_sel << obj;
1365 obj->setSelected (true);
1366 }
1367
1368 // =============================================================================
1369 //
1370 void LDDocument::removeFromSelection (LDObject* obj) // [protected]
1371 {
1372 if (!obj->isSelected())
1373 return;
1374
1375 assert (obj->document() == this);
1376 m_sel.removeOne (obj);
1377 obj->setSelected (false);
1378 }
1379
1380 // =============================================================================
1381 //
1382 void LDDocument::clearSelection()
1383 {
1384 for (LDObject* obj : m_sel)
1385 removeFromSelection (obj);
1386
1387 assert (m_sel.isEmpty());
1388 }
1389
1390 // =============================================================================
1391 //
1392 const LDObjectList& LDDocument::getSelection() const
1393 {
1394 return m_sel;
1395 }
1396
1397 // =============================================================================
1398 //
1399 void LDDocument::swapObjects (LDObject* one, LDObject* other)
1400 {
1401 int a = m_objects.indexOf (one);
1402 int b = m_objects.indexOf (other);
1403 assert (a != b && a != -1 && b != -1);
1404 m_objects[b] = one;
1405 m_objects[a] = other;
1406 addToHistory (new SwapHistory (one->id(), other->id()));
1407 }
1408
1409 // =============================================================================
1410 //
1411 QString LDDocument::shortenName (QString a) // [static]
1412 {
1413 QString shortname = basename (a);
1414 QString topdirname = basename (dirname (a));
1415
1416 if (g_specialSubdirectories.contains (topdirname))
1417 shortname.prepend (topdirname + "\\");
1418
1419 return shortname;
1420 }
1421
1422 // =============================================================================
1423 //
1424 void LDDocument::addReference (LDDocumentPointer* ptr)
1425 {
1426 m_references << ptr;
1427 }
1428
1429 // =============================================================================
1430 //
1431 void LDDocument::removeReference (LDDocumentPointer* ptr)
1432 {
1433 m_references.removeOne (ptr);
1434
1435 if (references().isEmpty())
1436 invokeLater (closeUnused);
1437 }

mercurial