diff -r c595cfb4791c -r 31540c1f22ea src/document.cc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/document.cc Mon Jan 20 15:04:26 2014 +0200
@@ -0,0 +1,1435 @@
+/*
+ * LDForge: LDraw parts authoring CAD
+ * Copyright (C) 2013, 2014 Santeri Piippo
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include
+#include
+#include
+#include
+#include "main.h"
+#include "config.h"
+#include "document.h"
+#include "misc.h"
+#include "gui.h"
+#include "history.h"
+#include "dialogs.h"
+#include "gldraw.h"
+#include "misc/invokationDeferer.h"
+#include "moc_document.cpp"
+
+cfg (String, io_ldpath, "");
+cfg (List, io_recentfiles, {});
+extern_cfg (String, net_downloadpath);
+extern_cfg (Bool, gl_logostuds);
+
+static bool g_loadingMainFile = false;
+static const int g_maxRecentFiles = 10;
+static bool g_aborted = false;
+static LDDocumentPointer g_logoedStud = null;
+static LDDocumentPointer g_logoedStud2 = null;
+
+LDDocument* LDDocument::m_curdoc = null;
+
+const QStringList g_specialSubdirectories ({ "s", "48", "8" });
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+namespace LDPaths
+{
+ static QString pathError;
+
+ struct
+ {
+ QString LDConfigPath;
+ QString partsPath, primsPath;
+ } pathInfo;
+
+ void initPaths()
+ {
+ if (!tryConfigure (io_ldpath))
+ {
+ LDrawPathDialog dlg (false);
+
+ if (!dlg.exec())
+ exit (0);
+
+ io_ldpath = dlg.filename();
+ }
+ }
+
+ bool tryConfigure (QString path)
+ {
+ QDir dir;
+
+ if (!dir.cd (path))
+ {
+ pathError = "Directory does not exist.";
+ return false;
+ }
+
+ QStringList mustHave = { "LDConfig.ldr", "parts", "p" };
+ QStringList contents = dir.entryList (mustHave);
+
+ if (contents.size() != mustHave.size())
+ {
+ pathError = "Not an LDraw directory! Must
have LDConfig.ldr, parts/ and p/.";
+ return false;
+ }
+
+ pathInfo.partsPath = fmt ("%1" DIRSLASH "parts", path);
+ pathInfo.LDConfigPath = fmt ("%1" DIRSLASH "LDConfig.ldr", path);
+ pathInfo.primsPath = fmt ("%1" DIRSLASH "p", path);
+
+ return true;
+ }
+
+ // Accessors
+ QString getError()
+ {
+ return pathError;
+ }
+
+ QString ldconfig()
+ {
+ return pathInfo.LDConfigPath;
+ }
+
+ QString prims()
+ {
+ return pathInfo.primsPath;
+ }
+
+ QString parts()
+ {
+ return pathInfo.partsPath;
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDDocument::LDDocument() :
+ m_gldata (new LDGLData)
+{
+ setImplicit (true);
+ setSavePosition (-1);
+ setListItem (null);
+ setHistory (new History);
+ m_History->setDocument (this);
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDDocument::~LDDocument()
+{
+ // Remove this file from the list of files. This MUST be done FIRST, otherwise
+ // a ton of other functions will think this file is still valid when it is not!
+ g_loadedFiles.removeOne (this);
+
+ m_History->setIgnoring (true);
+
+ // Clear everything from the model
+ for (LDObject* obj : getObjects())
+ obj->deleteSelf();
+
+ // Clear the cache as well
+ for (LDObject* obj : getCache())
+ obj->deleteSelf();
+
+ delete m_History;
+ delete m_gldata;
+
+ // If we just closed the current file, we need to set the current
+ // file as something else.
+ if (this == getCurrentDocument())
+ {
+ bool found = false;
+
+ // Try find an explicitly loaded file - if we can't find one,
+ // we need to create a new file to switch to.
+ for (LDDocument* file : g_loadedFiles)
+ {
+ if (!file->isImplicit())
+ {
+ LDDocument::setCurrent (file);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ newFile();
+ }
+
+ if (this == g_logoedStud)
+ g_logoedStud = null;
+ elif (this == g_logoedStud2)
+ g_logoedStud2 = null;
+
+ g_win->updateDocumentList();
+ log ("Closed %1", getName());
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDDocument* findDocument (QString name)
+{
+ for (LDDocument * file : g_loadedFiles)
+ if (!file->getName().isEmpty() && file->getName() == name)
+ return file;
+
+ return null;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+QString dirname (QString path)
+{
+ long lastpos = path.lastIndexOf (DIRSLASH);
+
+ if (lastpos > 0)
+ return path.left (lastpos);
+
+#ifndef _WIN32
+ if (path[0] == DIRSLASH_CHAR)
+ return DIRSLASH;
+#endif // _WIN32
+
+ return "";
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+QString basename (QString path)
+{
+ long lastpos = path.lastIndexOf (DIRSLASH);
+
+ if (lastpos != -1)
+ return path.mid (lastpos + 1);
+
+ return path;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+static QString findLDrawFilePath (QString relpath, bool subdirs)
+{
+ QString fullPath;
+
+ // LDraw models use Windows-style path separators. If we're not on Windows,
+ // replace the path separator now before opening any files. Qt expects
+ // forward-slashes as directory separators.
+#ifndef WIN32
+ relpath.replace ("\\", "/");
+#endif // WIN32
+
+ // Try find it relative to other currently open documents. We want a file
+ // in the immediate vicinity of a current model to override stock LDraw stuff.
+ QString reltop = basename (dirname (relpath));
+
+ for (LDDocument* doc : g_loadedFiles)
+ {
+ if (doc->getFullPath().isEmpty())
+ continue;
+
+ QString partpath = fmt ("%1/%2", dirname (doc->getFullPath()), relpath);
+ QFile f (partpath);
+
+ if (f.exists())
+ {
+ // ensure we don't mix subfiles and 48-primitives with non-subfiles and non-48
+ QString proptop = basename (dirname (partpath));
+
+ bool bogus = false;
+
+ for (QString s : g_specialSubdirectories)
+ {
+ if ((proptop == s && reltop != s) || (reltop == s && proptop != s))
+ {
+ bogus = true;
+ break;
+ }
+ }
+
+ if (!bogus)
+ return partpath;
+ }
+ }
+
+ if (QFile::exists (relpath))
+ return relpath;
+
+ // Try with just the LDraw path first
+ fullPath = fmt ("%1" DIRSLASH "%2", io_ldpath, relpath);
+
+ if (QFile::exists (fullPath))
+ return fullPath;
+
+ if (subdirs)
+ {
+ // Look in sub-directories: parts and p. Also look in net_downloadpath, since that's
+ // where we download parts from the PT to.
+ for (const QString& topdir : initlist ({ io_ldpath, net_downloadpath }))
+ {
+ for (const QString& subdir : initlist ({ "parts", "p" }))
+ {
+ fullPath = fmt ("%1" DIRSLASH "%2" DIRSLASH "%3", topdir, subdir, relpath);
+
+ if (QFile::exists (fullPath))
+ return fullPath;
+ }
+ }
+ }
+
+ // Did not find the file.
+ return "";
+}
+
+QFile* openLDrawFile (QString relpath, bool subdirs, QString* pathpointer)
+{
+ log ("Opening %1...\n", relpath);
+ QString path = findLDrawFilePath (relpath, subdirs);
+
+ if (pathpointer != null)
+ *pathpointer = path;
+
+ if (path.isEmpty())
+ return null;
+
+ QFile* fp = new QFile (path);
+
+ if (fp->open (QIODevice::ReadOnly))
+ return fp;
+
+ fp->deleteLater();
+ return null;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDFileLoader::start()
+{
+ setDone (false);
+ setProgress (0);
+ setAborted (false);
+
+ if (isOnForeground())
+ {
+ g_aborted = false;
+
+ // Show a progress dialog if we're loading the main document.here so we can
+ // show progress updates and keep the WM posted that we're still here.
+ // Of course we cannot exec() the dialog because then the dialog would
+ // block.
+ dlg = new OpenProgressDialog (g_win);
+ dlg->setNumLines (getLines().size());
+ dlg->setModal (true);
+ dlg->show();
+
+ // Connect the loader in so we can show updates
+ connect (this, SIGNAL (workDone()), dlg, SLOT (accept()));
+ connect (dlg, SIGNAL (rejected()), this, SLOT (abort()));
+ }
+ else
+ dlg = null;
+
+ // Begin working
+ work (0);
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDFileLoader::work (int i)
+{
+ // User wishes to abort, so stop here now.
+ if (isAborted())
+ {
+ for (LDObject* obj : m_Objects)
+ obj->deleteSelf();
+
+ m_Objects.clear();
+ setDone (true);
+ return;
+ }
+
+ // Parse up to 300 lines per iteration
+ int max = i + 300;
+
+ for (; i < max && i < (int) getLines().size(); ++i)
+ {
+ QString line = getLines()[i];
+
+ // Trim the trailing newline
+ QChar c;
+
+ while (line.endsWith ("\n") || line.endsWith ("\r"))
+ line.chop (1);
+
+ LDObject* obj = parseLine (line);
+
+ // Check for parse errors and warn about tthem
+ if (obj->getType() == LDObject::EError)
+ {
+ log ("Couldn't parse line #%1: %2", getProgress() + 1, static_cast (obj)->reason);
+
+ if (getWarnings() != null)
+ (*getWarnings())++;
+ }
+
+ m_Objects << obj;
+ setProgress (i);
+
+ // If we have a dialog pointer, update the progress now
+ if (isOnForeground())
+ dlg->updateProgress (i);
+ }
+
+ // If we're done now, tell the environment we're done and stop.
+ if (i >= ((int) getLines().size()) - 1)
+ {
+ emit workDone();
+ setDone (true);
+ return;
+ }
+
+ // Otherwise, continue, by recursing back.
+ if (!isDone())
+ {
+ // If we have a dialog to show progress output to, we cannot just call
+ // work() again immediately as the dialog needs some processor cycles as
+ // well. Thus, take a detour through the event loop by using the
+ // meta-object system.
+ //
+ // This terminates the loop here and control goes back to the function
+ // which called the file loader. It will keep processing the event loop
+ // until we're ready (see loadFileContents), thus the event loop will
+ // eventually catch the invokation we throw here and send us back. Though
+ // it's not technically recursion anymore, more like a for loop. :P
+ if (isOnForeground())
+ QMetaObject::invokeMethod (this, "work", Qt::QueuedConnection, Q_ARG (int, i));
+ else
+ work (i);
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDFileLoader::abort()
+{
+ setAborted (true);
+
+ if (isOnForeground())
+ g_aborted = true;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDObjectList loadFileContents (QFile* fp, int* numWarnings, bool* ok)
+{
+ QStringList lines;
+ LDObjectList objs;
+
+ if (numWarnings)
+ *numWarnings = 0;
+
+ // Read in the lines
+ while (fp->atEnd() == false)
+ lines << QString::fromUtf8 (fp->readLine());
+
+ LDFileLoader* loader = new LDFileLoader;
+ loader->setWarnings (numWarnings);
+ loader->setLines (lines);
+ loader->setOnForeground (g_loadingMainFile);
+ loader->start();
+
+ // After start() returns, if the loader isn't done yet, it's delaying
+ // its next iteration through the event loop. We need to catch this here
+ // by telling the event loop to tick, which will tick the file loader again.
+ // We keep doing this until the file loader is ready.
+ while (loader->isDone() == false)
+ qApp->processEvents();
+
+ // If we wanted the success value, supply that now
+ if (ok)
+ *ok = !loader->isAborted();
+
+ objs = loader->getObjects();
+ return objs;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDDocument* openDocument (QString path, bool search)
+{
+ // Convert the file name to lowercase since some parts contain uppercase
+ // file names. I'll assume here that the library will always use lowercase
+ // file names for the actual parts..
+ QFile* fp;
+ QString fullpath;
+
+ if (search)
+ fp = openLDrawFile (path.toLower(), true, &fullpath);
+ else
+ {
+ fp = new QFile (path);
+ fullpath = path;
+
+ if (!fp->open (QIODevice::ReadOnly))
+ {
+ delete fp;
+ return null;
+ }
+ }
+
+ if (!fp)
+ return null;
+
+ LDDocument* load = new LDDocument;
+ load->setFullPath (fullpath);
+ load->setName (LDDocument::shortenName (load->getFullPath()));
+ dlog ("name: %1 (%2)", load->getName(), load->getFullPath());
+ g_loadedFiles << load;
+
+ // Don't take the file loading as actual edits to the file
+ load->getHistory()->setIgnoring (true);
+
+ int numWarnings;
+ bool ok;
+ LDObjectList objs = loadFileContents (fp, &numWarnings, &ok);
+ fp->close();
+ fp->deleteLater();
+
+ if (!ok)
+ {
+ g_loadedFiles.removeOne (load);
+ delete load;
+ return null;
+ }
+
+ load->addObjects (objs);
+
+ if (g_loadingMainFile)
+ {
+ LDDocument::setCurrent (load);
+ g_win->R()->setFile (load);
+ log (QObject::tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings);
+ }
+
+ load->getHistory()->setIgnoring (false);
+ return load;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+bool LDDocument::isSafeToClose()
+{
+ typedef QMessageBox msgbox;
+ setlocale (LC_ALL, "C");
+
+ // If we have unsaved changes, warn and give the option of saving.
+ if (hasUnsavedChanges())
+ {
+ QString message = fmt (tr ("There are unsaved changes to %1. Should it be saved?"),
+ (getName().length() > 0) ? getName() : tr (""));
+
+ int button = msgbox::question (g_win, tr ("Unsaved Changes"), message,
+ (msgbox::Yes | msgbox::No | msgbox::Cancel), msgbox::Cancel);
+
+ switch (button)
+ {
+ case msgbox::Yes:
+ {
+ // If we don't have a file path yet, we have to ask the user for one.
+ if (getName().length() == 0)
+ {
+ QString newpath = QFileDialog::getSaveFileName (g_win, tr ("Save As"),
+ getCurrentDocument()->getName(), tr ("LDraw files (*.dat *.ldr)"));
+
+ if (newpath.length() == 0)
+ return false;
+
+ setName (newpath);
+ }
+
+ if (!save())
+ {
+ message = fmt (tr ("Failed to save %1 (%2)\nDo you still want to close?"),
+ getName(), strerror (errno));
+
+ if (msgbox::critical (g_win, tr ("Save Failure"), message,
+ (msgbox::Yes | msgbox::No), msgbox::No) == msgbox::No)
+ {
+ return false;
+ }
+ }
+ } break;
+
+ case msgbox::Cancel:
+ return false;
+
+ default:
+ break;
+ }
+ }
+
+ return true;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void closeAll()
+{
+ // Remove all loaded files and the objects they contain
+ QList files = g_loadedFiles;
+
+ for (LDDocument* file : files)
+ delete file;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void newFile()
+{
+ // Create a new anonymous file and set it to our current
+ LDDocument* f = new LDDocument;
+ f->setName ("");
+ f->setImplicit (false);
+ g_loadedFiles << f;
+ LDDocument::setCurrent (f);
+ LDDocument::closeInitialFile();
+ g_win->R()->setFile (f);
+ g_win->doFullRefresh();
+ g_win->updateTitle();
+ g_win->updateActions();
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void addRecentFile (QString path)
+{
+ auto& rfiles = io_recentfiles;
+ int idx = rfiles.indexOf (path);
+
+ // If this file already is in the list, pop it out.
+ if (idx != -1)
+ {
+ if (rfiles.size() == 1)
+ return; // only recent file - abort and do nothing
+
+ // Pop it out.
+ rfiles.removeAt (idx);
+ }
+
+ // If there's too many recent files, drop one out.
+ while (rfiles.size() > (g_maxRecentFiles - 1))
+ rfiles.removeAt (0);
+
+ // Add the file
+ rfiles << path;
+
+ Config::save();
+ g_win->updateRecentFilesMenu();
+}
+
+// =============================================================================
+// Open an LDraw file and set it as the main model
+// -----------------------------------------------------------------------------
+void openMainFile (QString path)
+{
+ g_loadingMainFile = true;
+
+ // If there's already a file with the same name, this file must replace it.
+ LDDocument* documentToReplace = null;
+ QString shortName = LDDocument::shortenName (path);
+
+ for (LDDocument* doc : g_loadedFiles)
+ {
+ if (doc->getName() == shortName)
+ {
+ documentToReplace = doc;
+ break;
+ }
+ }
+
+ // We cannot open this file if the document this would replace is not
+ // safe to close.
+ if (documentToReplace != null && documentToReplace->isSafeToClose() == false)
+ {
+ g_loadingMainFile = false;
+ return;
+ }
+
+ LDDocument* file = openDocument (path, false);
+
+ if (!file)
+ {
+ // Loading failed, thus drop down to a new file since we
+ // closed everything prior.
+ newFile();
+
+ if (!g_aborted)
+ {
+ // Tell the user loading failed.
+ setlocale (LC_ALL, "C");
+ critical (fmt (QObject::tr ("Failed to open %1: %2"), path, strerror (errno)));
+ }
+
+ g_loadingMainFile = false;
+ return;
+ }
+
+ file->setImplicit (false);
+
+ // Replace references to the old file with the new file.
+ if (documentToReplace != null)
+ {
+ for (LDDocumentPointer* ptr : documentToReplace->getReferences())
+ { dlog ("ptr: %1 (%2)\n",
+ ptr, ptr->getPointer() ? ptr->getPointer()->getName() : "");
+
+ ptr->operator= (file);
+ }
+
+ assert (documentToReplace->countReferences() == 0);
+ delete documentToReplace;
+ }
+
+ // If we have an anonymous, unchanged file open as the only open file
+ // (aside of the one we just opened), close it now.
+ LDDocument::closeInitialFile();
+
+ // Rebuild the object tree view now.
+ LDDocument::setCurrent (file);
+ g_win->doFullRefresh();
+
+ // Add it to the recent files list.
+ addRecentFile (path);
+ g_loadingMainFile = false;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+bool LDDocument::save (QString savepath)
+{
+ if (!savepath.length())
+ savepath = getFullPath();
+
+ QFile f (savepath);
+
+ if (!f.open (QIODevice::WriteOnly))
+ return false;
+
+ // If the second object in the list holds the file name, update that now.
+ // Only do this if the file is explicitly open.
+ LDObject* nameObject = getObject (1);
+
+ if (!isImplicit() && nameObject != null && nameObject->getType() == LDObject::EComment)
+ {
+ LDComment* nameComment = static_cast (nameObject);
+
+ if (nameComment->text.left (6) == "Name: ")
+ {
+ QString newname = shortenName (savepath);
+ nameComment->text = fmt ("Name: %1", newname);
+ g_win->buildObjList();
+ }
+ }
+
+ // File is open, now save the model to it. Note that LDraw requires files to
+ // have DOS line endings, so we terminate the lines with \r\n.
+ for (LDObject* obj : getObjects())
+ f.write ((obj->raw() + "\r\n").toUtf8());
+
+ // File is saved, now clean up.
+ f.close();
+
+ // We have successfully saved, update the save position now.
+ setSavePosition (getHistory()->getPosition());
+ setFullPath (savepath);
+ setName (shortenName (savepath));
+
+ g_win->updateDocumentListItem (this);
+ g_win->updateTitle();
+ return true;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+class LDParseError : public std::exception
+{
+ PROPERTY (private, QString, Error, STR_OPS, STOCK_WRITE)
+ PROPERTY (private, QString, Line, STR_OPS, STOCK_WRITE)
+
+ public:
+ LDParseError (QString line, QString a) : m_Error (a), m_Line (line) {}
+
+ const char* what() const throw()
+ {
+ return getError().toLocal8Bit().constData();
+ }
+};
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void checkTokenCount (QString line, const QStringList& tokens, int num)
+{
+ if (tokens.size() != num)
+ throw LDParseError (line, fmt ("Bad amount of tokens, expected %1, got %2", num, tokens.size()));
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void checkTokenNumbers (QString line, const QStringList& tokens, int min, int max)
+{
+ bool ok;
+
+ // Check scientific notation, e.g. 7.99361e-15
+ QRegExp scient ("\\-?[0-9]+\\.[0-9]+e\\-[0-9]+");
+
+ for (int i = min; i <= max; ++i)
+ {
+ tokens[i].toDouble (&ok);
+
+ if (!ok && !scient.exactMatch (tokens[i]))
+ throw LDParseError (line, fmt ("Token #%1 was `%2`, expected a number (matched length: %3)", (i + 1), tokens[i], scient.matchedLength()));
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+static Vertex parseVertex (QStringList& s, const int n)
+{
+ Vertex v;
+
+ for_axes (ax)
+ v[ax] = s[n + ax].toDouble();
+
+ return v;
+}
+
+// =============================================================================
+// This is the LDraw code parser function. It takes in a string containing LDraw
+// code and returns the object parsed from it. parseLine never returns null,
+// the object will be LDError if it could not be parsed properly.
+// -----------------------------------------------------------------------------
+LDObject* parseLine (QString line)
+{
+ try
+ {
+ QStringList tokens = line.split (" ", QString::SkipEmptyParts);
+
+ if (tokens.size() <= 0)
+ {
+ // Line was empty, or only consisted of whitespace
+ return new LDEmpty;
+ }
+
+ if (tokens[0].length() != 1 || tokens[0][0].isDigit() == false)
+ throw LDParseError (line, "Illogical line code");
+
+ int num = tokens[0][0].digitValue();
+
+ switch (num)
+ {
+ case 0:
+ {
+ // Comment
+ QString comm = line.mid (line.indexOf ("0") + 1).simplified();
+
+ // Handle BFC statements
+ if (tokens.size() > 2 && tokens[1] == "BFC")
+ {
+ for (int i = 0; i < LDBFC::NumStatements; ++i)
+ if (comm == fmt ("BFC %1", LDBFC::statements [i]))
+ return new LDBFC ( (LDBFC::Type) i);
+
+ // MLCAD is notorious for stuffing these statements in parts it
+ // creates. The above block only handles valid statements, so we
+ // need to handle MLCAD-style invertnext, clip and noclip separately.
+ struct
+ {
+ QString a;
+ LDBFC::Type b;
+ } BFCData[] =
+ {
+ { "INVERTNEXT", LDBFC::InvertNext },
+ { "NOCLIP", LDBFC::NoClip },
+ { "CLIP", LDBFC::Clip }
+ };
+
+ for (const auto& i : BFCData)
+ if (comm == "BFC CERTIFY " + i.a)
+ return new LDBFC (i.b);
+ }
+
+ if (tokens.size() > 2 && tokens[1] == "!LDFORGE")
+ {
+ // Handle LDForge-specific types, they're embedded into comments too
+ if (tokens[2] == "VERTEX")
+ {
+ // Vertex (0 !LDFORGE VERTEX)
+ checkTokenCount (line, tokens, 7);
+ checkTokenNumbers (line, tokens, 3, 6);
+
+ LDVertex* obj = new LDVertex;
+ obj->setColor (tokens[3].toLong());
+
+ for_axes (ax)
+ obj->pos[ax] = tokens[4 + ax].toDouble(); // 4 - 6
+
+ return obj;
+ } elif (tokens[2] == "OVERLAY")
+ {
+ checkTokenCount (line, tokens, 9);;
+ checkTokenNumbers (line, tokens, 5, 8);
+
+ LDOverlay* obj = new LDOverlay;
+ obj->setFileName (tokens[3]);
+ obj->setCamera (tokens[4].toLong());
+ obj->setX (tokens[5].toLong());
+ obj->setY (tokens[6].toLong());
+ obj->setWidth (tokens[7].toLong());
+ obj->setHeight (tokens[8].toLong());
+ return obj;
+ }
+ }
+
+ // Just a regular comment:
+ LDComment* obj = new LDComment;
+ obj->text = comm;
+ return obj;
+ }
+
+ case 1:
+ {
+ // Subfile
+ checkTokenCount (line, tokens, 15);
+ checkTokenNumbers (line, tokens, 1, 13);
+
+ // Try open the file. Disable g_loadingMainFile temporarily since we're
+ // not loading the main file now, but the subfile in question.
+ bool tmp = g_loadingMainFile;
+ g_loadingMainFile = false;
+ LDDocument* load = getDocument (tokens[14]);
+ g_loadingMainFile = tmp;
+
+ // If we cannot open the file, mark it an error. Note we cannot use LDParseError
+ // here because the error object needs the document reference.
+ if (!load)
+ {
+ LDError* obj = new LDError (line, fmt ("Could not open %1", tokens[14]));
+ obj->setFileReferenced (tokens[14]);
+ return obj;
+ }
+
+ LDSubfile* obj = new LDSubfile;
+ obj->setColor (tokens[1].toLong());
+ obj->setPosition (parseVertex (tokens, 2)); // 2 - 4
+
+ Matrix transform;
+
+ for (int i = 0; i < 9; ++i)
+ transform[i] = tokens[i + 5].toDouble(); // 5 - 13
+
+ obj->setTransform (transform);
+ obj->setFileInfo (load);
+ return obj;
+ }
+
+ case 2:
+ {
+ checkTokenCount (line, tokens, 8);
+ checkTokenNumbers (line, tokens, 1, 7);
+
+ // Line
+ LDLine* obj = new LDLine;
+ obj->setColor (tokens[1].toLong());
+
+ for (int i = 0; i < 2; ++i)
+ obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 7
+
+ return obj;
+ }
+
+ case 3:
+ {
+ checkTokenCount (line, tokens, 11);
+ checkTokenNumbers (line, tokens, 1, 10);
+
+ // Triangle
+ LDTriangle* obj = new LDTriangle;
+ obj->setColor (tokens[1].toLong());
+
+ for (int i = 0; i < 3; ++i)
+ obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 10
+
+ return obj;
+ }
+
+ case 4:
+ case 5:
+ {
+ checkTokenCount (line, tokens, 14);
+ checkTokenNumbers (line, tokens, 1, 13);
+
+ // Quadrilateral / Conditional line
+ LDObject* obj;
+
+ if (num == 4)
+ obj = new LDQuad;
+ else
+ obj = new LDCondLine;
+
+ obj->setColor (tokens[1].toLong());
+
+ for (int i = 0; i < 4; ++i)
+ obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 13
+
+ return obj;
+ }
+
+ default: // Strange line we couldn't parse
+ throw LDError (line, "Unknown line code number");
+ }
+ }
+ catch (LDParseError& e)
+ {
+ return new LDError (e.getLine(), e.getError());
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDDocument* getDocument (QString filename)
+{
+ // Try find the file in the list of loaded files
+ LDDocument* doc = findDocument (filename);
+
+ // If it's not loaded, try open it
+ if (!doc)
+ doc = openDocument (filename, true);
+
+ return doc;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void reloadAllSubfiles()
+{
+ if (!getCurrentDocument())
+ return;
+
+ g_loadedFiles.clear();
+ g_loadedFiles << getCurrentDocument();
+
+ // Go through all objects in the current file and reload the subfiles
+ for (LDObject* obj : getCurrentDocument()->getObjects())
+ {
+ if (obj->getType() == LDObject::ESubfile)
+ {
+ LDSubfile* ref = static_cast (obj);
+ LDDocument* fileInfo = getDocument (ref->getFileInfo()->getName());
+
+ if (fileInfo)
+ ref->setFileInfo (fileInfo);
+ else
+ ref->replace (new LDError (ref->raw(), fmt ("Could not open %1", ref->getFileInfo()->getName())));
+ }
+
+ // Reparse gibberish files. It could be that they are invalid because
+ // of loading errors. Circumstances may be different now.
+ if (obj->getType() == LDObject::EError)
+ obj->replace (parseLine (static_cast (obj)->contents));
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+int LDDocument::addObject (LDObject* obj)
+{
+ getHistory()->add (new AddHistory (getObjects().size(), obj));
+ m_Objects << obj;
+
+ if (obj->getType() == LDObject::EVertex)
+ m_Vertices << obj;
+
+#ifdef DEBUG
+ if (!isImplicit())
+ dlog ("Added object #%1 (%2)\n", obj->getID(), obj->getTypeName());
+#endif
+
+ obj->setFile (this);
+ return getObjectCount() - 1;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::addObjects (const LDObjectList objs)
+{
+ for (LDObject* obj : objs)
+ if (obj)
+ addObject (obj);
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::insertObj (int pos, LDObject* obj)
+{
+ getHistory()->add (new AddHistory (pos, obj));
+ m_Objects.insert (pos, obj);
+ obj->setFile (this);
+
+#ifdef DEBUG
+ if (!isImplicit())
+ dlog ("Inserted object #%1 (%2) at %3\n", obj->getID(), obj->getTypeName(), pos);
+#endif
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::forgetObject (LDObject* obj)
+{
+ int idx = obj->getIndex();
+ obj->unselect();
+ assert (m_Objects[idx] == obj);
+
+ if (!getHistory()->isIgnoring())
+ getHistory()->add (new DelHistory (idx, obj));
+
+ m_Objects.removeAt (idx);
+ obj->setFile (null);
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+bool safeToCloseAll()
+{
+ for (LDDocument* f : g_loadedFiles)
+ if (!f->isSafeToClose())
+ return false;
+
+ return true;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::setObject (int idx, LDObject* obj)
+{
+ assert (idx >= 0 && idx < m_Objects.size());
+
+ // Mark this change to history
+ if (!m_History->isIgnoring())
+ {
+ QString oldcode = getObject (idx)->raw();
+ QString newcode = obj->raw();
+ *m_History << new EditHistory (idx, oldcode, newcode);
+ }
+
+ m_Objects[idx]->unselect();
+ m_Objects[idx]->setFile (null);
+ obj->setFile (this);
+ m_Objects[idx] = obj;
+}
+
+// =============================================================================
+// Close all implicit files with no references
+// -----------------------------------------------------------------------------
+void LDDocument::closeUnused()
+{
+ for (LDDocument* file : g_loadedFiles)
+ if (file->isImplicit() && file->countReferences() == 0)
+ delete file;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDObject* LDDocument::getObject (int pos) const
+{
+ if (m_Objects.size() <= pos)
+ return null;
+
+ return m_Objects[pos];
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+int LDDocument::getObjectCount() const
+{
+ return getObjects().size();
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+bool LDDocument::hasUnsavedChanges() const
+{
+ return !isImplicit() && getHistory()->getPosition() != getSavePosition();
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+QString LDDocument::getDisplayName()
+{
+ if (!getName().isEmpty())
+ return getName();
+
+ if (!getDefaultName().isEmpty())
+ return "[" + getDefaultName() + "]";
+
+ return tr ("");
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDObjectList LDDocument::inlineContents (LDSubfile::InlineFlags flags)
+{
+ // Possibly substitute with logoed studs:
+ // stud.dat -> stud-logo.dat
+ // stud2.dat -> stud-logo2.dat
+ if (gl_logostuds && (flags & LDSubfile::RendererInline))
+ {
+ // Ensure logoed studs are loaded first
+ loadLogoedStuds();
+
+ if (getName() == "stud.dat" && g_logoedStud)
+ return g_logoedStud->inlineContents (flags);
+ elif (getName() == "stud2.dat" && g_logoedStud2)
+ return g_logoedStud2->inlineContents (flags);
+ }
+
+ LDObjectList objs, objcache;
+
+ bool deep = flags & LDSubfile::DeepInline,
+ doCache = flags & LDSubfile::CacheInline;
+
+ if (m_needsCache)
+ {
+ clearCache();
+ doCache = true;
+ }
+
+ // If we have this cached, just create a copy of that
+ if (deep && getCache().isEmpty() == false)
+ {
+ for (LDObject* obj : getCache())
+ objs << obj->createCopy();
+ }
+ else
+ {
+ if (!deep)
+ doCache = false;
+
+ for (LDObject* obj : getObjects())
+ {
+ // Skip those without scemantic meaning
+ if (!obj->isScemantic())
+ continue;
+
+ // Got another sub-file reference, inline it if we're deep-inlining. If not,
+ // just add it into the objects normally. Also, we only cache immediate
+ // subfiles and this is not one. Yay, recursion!
+ if (deep && obj->getType() == LDObject::ESubfile)
+ {
+ LDSubfile* ref = static_cast (obj);
+
+ // We only want to cache immediate subfiles, so shed the caching
+ // flag when recursing deeper in hierarchy.
+ LDObjectList otherobjs = ref->inlineContents (flags & ~ (LDSubfile::CacheInline));
+
+ for (LDObject* otherobj : otherobjs)
+ {
+ // Cache this object, if desired
+ if (doCache)
+ objcache << otherobj->createCopy();
+
+ objs << otherobj;
+ }
+ }
+ else
+ {
+ if (doCache)
+ objcache << obj->createCopy();
+
+ objs << obj->createCopy();
+ }
+ }
+
+ if (doCache)
+ setCache (objcache);
+ }
+
+ return objs;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDDocument* LDDocument::current()
+{
+ return m_curdoc;
+}
+
+// =============================================================================
+// Sets the given file as the current one on display. At some point in time this
+// was an operation completely unheard of. ;)
+//
+// TODO: f can be temporarily null. This probably should not be the case.
+// -----------------------------------------------------------------------------
+void LDDocument::setCurrent (LDDocument* f)
+{
+ // Implicit files were loaded for caching purposes and must never be set
+ // current.
+ if (f && f->isImplicit())
+ return;
+
+ m_curdoc = f;
+
+ if (g_win && f)
+ {
+ // A ton of stuff needs to be updated
+ g_win->updateDocumentListItem (f);
+ g_win->buildObjList();
+ g_win->updateTitle();
+ g_win->R()->setFile (f);
+ g_win->R()->repaint();
+ log ("Changed file to %1", f->getDisplayName());
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+int LDDocument::countExplicitFiles()
+{
+ int count = 0;
+
+ for (LDDocument* f : g_loadedFiles)
+ if (f->isImplicit() == false)
+ count++;
+
+ return count;
+}
+
+// =============================================================================
+// This little beauty closes the initial file that was open at first when opening
+// a new file over it.
+// -----------------------------------------------------------------------------
+void LDDocument::closeInitialFile()
+{
+ if (
+ countExplicitFiles() == 2 &&
+ g_loadedFiles[0]->getName().isEmpty() &&
+ g_loadedFiles[1]->getName().isEmpty() == false &&
+ !g_loadedFiles[0]->hasUnsavedChanges()
+ )
+ delete g_loadedFiles[0];
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void loadLogoedStuds()
+{
+ if (g_logoedStud && g_logoedStud2)
+ return;
+
+ delete g_logoedStud;
+ delete g_logoedStud2;
+
+ g_logoedStud = openDocument ("stud-logo.dat", true);
+ g_logoedStud2 = openDocument ("stud2-logo.dat", true);
+
+ log (LDDocument::tr ("Logoed studs loaded.\n"));
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::addToSelection (LDObject* obj) // [protected]
+{
+ if (obj->isSelected())
+ return;
+
+ assert (obj->getFile() == this);
+ m_sel << obj;
+ obj->setSelected (true);
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::removeFromSelection (LDObject* obj) // [protected]
+{
+ if (!obj->isSelected())
+ return;
+
+ assert (obj->getFile() == this);
+ m_sel.removeOne (obj);
+ obj->setSelected (false);
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::clearSelection()
+{
+ for (LDObject* obj : m_sel)
+ removeFromSelection (obj);
+
+ assert (m_sel.isEmpty());
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+const LDObjectList& LDDocument::getSelection() const
+{
+ return m_sel;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::swapObjects (LDObject* one, LDObject* other)
+{
+ int a = m_Objects.indexOf (one);
+ int b = m_Objects.indexOf (other);
+ assert (a != b && a != -1 && b != -1);
+ m_Objects[b] = one;
+ m_Objects[a] = other;
+ addToHistory (new SwapHistory (one->getID(), other->getID()));
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+QString LDDocument::shortenName (QString a) // [static]
+{
+ QString shortname = basename (a);
+ QString topdirname = basename (dirname (a));
+
+ if (g_specialSubdirectories.contains (topdirname))
+ shortname.prepend (topdirname + "\\");
+
+ return shortname;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::addReference (LDDocumentPointer* ptr)
+{
+ pushToReferences (ptr);
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void LDDocument::removeReference (LDDocumentPointer* ptr)
+{
+ removeFromReferences (ptr);
+
+ if (getReferences().size() == 0)
+ invokeLater (closeUnused);
+}
\ No newline at end of file