src/document.cc

Mon, 13 Jan 2014 10:07:39 +0200

author
Santeri Piippo <crimsondusk64@gmail.com>
date
Mon, 13 Jan 2014 10:07:39 +0200
changeset 622
622c49e60348
parent 619
8bfe34c6ab1a
permissions
-rw-r--r--

- refactoring

/*
 *  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);
}

mercurial