src/document.cc

changeset 667
31540c1f22ea
parent 622
622c49e60348
--- /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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <QMessageBox>
+#include <QFileDialog>
+#include <QDir>
+#include <QApplication>
+#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<br />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<QString> ({ io_ldpath, net_downloadpath }))
+		{
+			for (const QString& subdir : initlist<QString> ({ "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<LDError*> (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 ("<anonymous>"));
+
+		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<LDDocument*> 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() : "<null>");
+
+			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<LDComment*> (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<LDSubfile*> (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<LDError*> (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 ("<anonymous>");
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+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<LDSubfile*> (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

mercurial