src/file.cpp

Sat, 15 Jun 2013 04:20:44 +0300

author
Santeri Piippo <crimsondusk64@gmail.com>
date
Sat, 15 Jun 2013 04:20:44 +0300
changeset 290
be0c367e7420
parent 289
d7bf5c11d299
child 292
4779ca562d5e
permissions
-rw-r--r--

Added primitive scanning, replaced parts list in subfile add dialog with it

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013 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 <qprogressbar.h>
#include <QDir>
#include <qthread.h>

#include <stdlib.h>
#include "common.h"
#include "config.h"
#include "file.h"
#include "misc.h"
#include "bbox.h"
#include "gui.h"
#include "history.h"
#include "dialogs.h"
#include "gldraw.h"
#include "string.h"

cfg (str, io_ldpath, "");
cfg (str, io_recentfiles, "");

static bool g_loadingMainFile = false;
PrimitiveLister* g_activePrimLister = null;
bool g_primListerMutex = false;
vector<PrimitiveInfo> g_Primitives;

// =============================================================================
namespace LDPaths {
	static str pathError;
	
	struct {
		str LDConfigPath;
		str partsPath, primsPath;
	} pathInfo;
	
	void initPaths () {
		if (!tryConfigure (io_ldpath)) {
			LDrawPathDialog dlg (false);
			
			if (!dlg.exec ())
				exit (0);
			
			io_ldpath = dlg.filename ();
		}
	}
	
	bool tryConfigure (str 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
	str getError () { return pathError; }
	str ldconfig () { return pathInfo.LDConfigPath; }
	str prims () { return pathInfo.primsPath; }
	str parts () { return pathInfo.partsPath; }
}

// =============================================================================
LDOpenFile::LDOpenFile () {
	setImplicit (true);
	setSavePos (-1);
	m_history.setFile (this);
}

// =============================================================================
LDOpenFile::~LDOpenFile () {
	// Clear everything from the model
	for (LDObject* obj : m_objs)
		delete obj;
	
	// Clear the cache as well
	for (LDObject* obj : m_cache)
		delete obj;
}

// =============================================================================
LDOpenFile* findLoadedFile (str name) {
	for (LDOpenFile* file : g_loadedFiles)
		if (file->name () == name)
			return file;
	
	return null;
}

// =============================================================================
str dirname (str 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 "";
}

// =============================================================================
str basename (str path) {
	long lastpos = path.lastIndexOf (DIRSLASH);
	
	if (lastpos != -1)
		return path.mid (lastpos + 1);
	
	return path;
}

// =============================================================================
File* openLDrawFile (str relpath, bool subdirs) {
	print ("%1: Try to open %2\n", __func__, relpath);
	File* f = new File;
	
#ifndef WIN32
	relpath.replace ("\\", "/");
#endif // WIN32
	
	if (g_curfile != null) {
		str partpath = fmt ("%1" DIRSLASH "%2", dirname (g_curfile->name ()), relpath);
		print ("try %1\n", partpath);
		
		if (f->open (partpath, File::Read))
			return f;
	}
	
	print ("try %1\n", relpath);
	if (f->open (relpath, File::Read))
		return f;
	
	str fullPath;
	if (io_ldpath.value.length () > 0) {
		// Try with just the LDraw path first
		fullPath = fmt ("%1" DIRSLASH "%2", io_ldpath, relpath);
		print ("try %1\n", fullPath);
		
		if (f->open (fullPath, File::Read))
			return f;
		
		if (subdirs) {
			for (auto subdir : initlist<const char*> ({"parts", "p"})) {
				fullPath = fmt ("%1" DIRSLASH "%2" DIRSLASH "%3",
					io_ldpath, subdir, relpath);
				
				printf ("try %s\n", qchars (fullPath));
				if (f->open (fullPath, File::Read))
					return f;
			}
		}
	}
	
	delete f;
	return null;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void FileLoader::work () {
	m_progress = 0;
	abortflag = false;
	
	for (str line : *PROP_NAME (file)) {
		// Trim the trailing newline
		qchar c;
		while ((c = line[line.length () - 1]) == '\n' || c == '\r')
			line.chop (1);
		
		LDObject* obj = parseLine (line);
		assert (obj != null);
		
		// Check for parse errors and warn about tthem
		if (obj->getType () == LDObject::Gibberish) {
			logf (LOG_Warning, "Couldn't parse line #%lu: %s\n",
				m_progress + 1, qchars (static_cast<LDGibberish*> (obj)->reason));
			
			logf (LOG_Warning, "- Line was: %s\n", qchars (line));
			
			if (m_warningsPointer)
				(*m_warningsPointer)++;
		}
		
		m_objs << obj;
		m_progress++;
		emit progressUpdate (m_progress);
		
		if (abortflag) {
			// We were flagged for abortion, so abort.
			for (LDObject* obj : m_objs)
				delete obj;
			
			m_objs.clear ();
			return;
		}
	}
	
	emit workDone ();
	m_done = true;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
vector<LDObject*> loadFileContents (File* f, ulong* numWarnings, bool* ok) {
	vector<str> lines;
	vector<LDObject*> objs;
	
	if (numWarnings)
		*numWarnings = 0;
	
	FileLoader* loader = new FileLoader;
	loader->setFile (f);
	loader->setWarningsPointer (numWarnings);
	
	// Calculate the amount of lines
	ulong numLines = 0;
	for (str line : *f) {
		(void) line;
		numLines++;
	}
	
	f->rewind ();
	
	if (g_loadingMainFile) {
		// Show a progress dialog if we're loading the main file here and move
		// the actual work to a separate thread as this can be a rather intensive
		// operation and if we don't respond quickly enough, the program can be
		// deemed inresponsive.. which is a bad thing.
		
		// Init the thread and move the loader into it
		QThread* loaderThread = new QThread;
		QObject::connect (loaderThread, SIGNAL (started ()), loader, SLOT (work ()));
		QObject::connect (loaderThread, SIGNAL (finished ()), loader, SLOT (deleteLater ()));
		loader->moveToThread (loaderThread);
		loaderThread->start ();
		
		// Now create a progress dialog for the operation
		OpenProgressDialog* dlg = new OpenProgressDialog (g_win);
		dlg->setNumLines (numLines);
		
		// Connect the loader in so we can actually show updates
		QObject::connect (loader, SIGNAL (progressUpdate (int)), dlg, SLOT (updateProgress (int)));
		QObject::connect (loader, SIGNAL (workDone ()), dlg, SLOT (accept ()));
		
		// Show the dialog. If the user hits cancel, tell the loader to abort.
		if (!dlg->exec ())
			loader->abortflag = true;
	} else
		loader->work ();
	
	// If we wanted the success value, supply that now
	if (ok)
		*ok = loader->done ();
	
	// If the loader was done, return the objects it generated
	if (loader->done ())
		objs = loader->objs ();
	
	return objs;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
LDOpenFile* openDATFile (str 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..
	File* f;
	if (search)
		f = openLDrawFile (path.toLower (), true);
	else {
		f = new File (path, File::Read);
		
		if (!*f) {
			delete f;
			f = null;
		}
	}
	
	if (!f)
		return null;
	
	LDOpenFile* oldLoad = g_curfile;
	LDOpenFile* load = new LDOpenFile;
	load->setName (path);
	
	if (g_loadingMainFile) {
		g_curfile = load;
		g_win->R ()->setFile (load);
	}
	
	ulong numWarnings;
	bool ok;
	vector<LDObject*> objs = loadFileContents (f, &numWarnings, &ok);
	
	if (!ok) {
		load = oldLoad;
		return null;
	}
	
	for (LDObject* obj : objs)
		load->addObject (obj);
	
	delete f;
	g_loadedFiles << load;
	
	logf ("File %s parsed successfully (%lu warning%s).\n",
		qchars (path), numWarnings, plural (numWarnings));
	
	return load;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
bool LDOpenFile::safeToClose () {
	setlocale (LC_ALL, "C");
	
	// If we have unsaved changes, warn and give the option of saving.
	if (!implicit () && history ().pos () != savePos ()) {
		switch (QMessageBox::question (g_win, "Unsaved Changes",
			fmt ("There are unsaved changes to %1. Should it be saved?",
			(name ().length () > 0) ? name () : "<anonymous>"),
			(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel), QMessageBox::Cancel))
		{
		case QMessageBox::Yes:
			// If we don't have a file path yet, we have to ask the user for one.
			if (name ().length () == 0) {
				str newpath = QFileDialog::getSaveFileName (g_win, "Save As",
					g_curfile->name (), "LDraw files (*.dat *.ldr)");
				
				if (newpath.length () == 0)
					return false;
				
				setName (newpath);
			}
			
			if (!save ()) {
				str errormsg = fmt ("Failed to save %1: %2\nDo you still want to close?",
					name (), strerror (errno));
				
				if (QMessageBox::critical (g_win, "Save Failure", errormsg,
					(QMessageBox::Yes | QMessageBox::No), QMessageBox::No) == QMessageBox::No)
				{
					return false;
				}
			}
			
			break;
		
		case QMessageBox::Cancel:
			return false;
		
		default:
			break;
		}
	}
	
	return true;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void closeAll () {
	if (!g_loadedFiles.size())
		return;
	
	// Remove all loaded files and the objects they contain
	for (LDOpenFile* file : g_loadedFiles)
		delete file;
	
	// Clear the array
	g_loadedFiles.clear();
	g_curfile = null;
	
	g_win->R ()->setFile (null);
	g_win->fullRefresh ();
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void newFile () {
	// Create a new anonymous file and set it to our current
	closeAll ();
	
	LDOpenFile* f = new LDOpenFile;
	f->setName ("");
	f->setImplicit (false);
	g_loadedFiles << f;
	g_curfile = f;
	
	g_BBox.reset ();
	g_win->R ()->setFile (f);
	g_win->fullRefresh ();
	g_win->updateTitle ();
	f->history ().updateActions ();
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void addRecentFile (str path) {
	QStringList rfiles = io_recentfiles.value.split ('@');
	
	int idx = 0;
	
	for (str& it : rfiles) {
		if (it == path)
			break;
		
		idx++;
	}
	
	// If this file already is in the list, pop it out.
	if (idx != rfiles.size ()) {
		if (rfiles.size () == 1)
			return; // only recent file - do nothing
		
		// Pop it out.
		rfiles.removeAt (idx);
	}
	
	// If there's too many recent files, drop one out.
	while (rfiles.size () > (5 - 1))
		rfiles.removeAt (0);
	
	// Add the file
	rfiles.push_back (path);
	
	// Rebuild the config string
	io_recentfiles = rfiles.join ("@");
	
	config::save ();
	g_win->updateRecentFilesMenu ();
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void openMainFile (str path) {
	g_loadingMainFile = true;
	closeAll ();
	
	LDOpenFile* file = openDATFile (path, false);
	
	if (!file) {
		// Loading failed, thus drop down to a new file since we
		// closed everything prior.
		newFile ();
		
		// Tell the user loading failed.
		setlocale (LC_ALL, "C");
		critical (fmt ("Failed to open %1: %2", path, strerror (errno)));
		
		g_loadingMainFile = false;
		return;
	}
	
	file->setImplicit (false);
	g_curfile = file;
	
	// Recalculate the bounding box
	g_BBox.calculate();
	
	// Rebuild the object tree view now.
	g_win->fullRefresh ();
	g_win->updateTitle ();
	g_win->R ()->setFile (file);
	g_win->R ()->resetAngles ();
	
	// Add it to the recent files list.
	addRecentFile (path);
	g_loadingMainFile = false;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
bool LDOpenFile::save (str savepath) {
	if (!savepath.length ())
		savepath = name ();
	
	File f (savepath, File::Write);
	
	if (!f)
		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. If it's saved into a directory
	// called "s" or "48", prepend that into the name.
	LDComment* fpathComment = null;
	if (!implicit () && objs ().size () >= 2 && object (1)->getType () == LDObject::Comment) {
		fpathComment = static_cast<LDComment*> (object (1));
		
		if (fpathComment->text.left (6) == "Name: ") {
			str newfname;
			str dir = basename (dirname (savepath));
			
			if (dir == "s" || dir == "48")
				newfname = dir + "\\";
			
			newfname += basename (savepath);
			fpathComment->text = fmt ("Name: %1", newfname);
			g_win->buildObjList ();
		}
	}
	
	// Write all entries now
	for (LDObject* obj : objs ()) {
		// LDraw requires files to have DOS line endings
		f.write (obj->raw () + "\r\n");
	}
	
	f.close ();
	
	// We have successfully saved, update the save position now.
	setSavePos (history ().pos ());
	setName (savepath);
	
	g_win->updateTitle ();
	return true;
}

#define CHECK_TOKEN_COUNT(N) \
	if (tokens.size() != N) \
		return new LDGibberish (line, "Bad amount of tokens");

#define CHECK_TOKEN_NUMBERS(MIN,MAX) \
	for (ushort i = MIN; i <= MAX; ++i) \
		if (!isNumber (tokens[i])) \
			return new LDGibberish (line, fmt ("Token #%1 was `%2`, expected a number", \
				(i + 1), tokens[i]));

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
static vertex parseVertex (QStringList& s, const ushort n) {
	// Disable the locale while parsing the line or atof's behavior changes
	// between locales (i.e. fails to read decimals properly). That is
	// quite undesired...
	setlocale (LC_NUMERIC, "C");
	
	vertex v;
	for (const Axis ax : g_Axes)
		v[ax] = s[n + ax].toFloat ();
	
	return v;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
LDObject* parseLine (str line) {
	QStringList tokens = line.split (" ", str::SkipEmptyParts);
	
	if (!tokens.size ()) {
		// Line was empty, or only consisted of whitespace
		return new LDEmpty;
	}
	
	if (tokens[0].length () != 1)
		return new LDGibberish (line, "Illogical line code");
	
	const qchar c = tokens[0][0];
	switch (c.toAscii () - '0') {
	case 0:
		{
			// Comment
			str comm;
			for (int i = 1; i < tokens.size(); ++i) {
				comm += tokens[i];
				
				if (i != tokens.size() - 1)
					comm += ' ';
			}
			
			// Handle BFC statements
			if (tokens.size() > 2 && tokens[1] == "BFC") {
				for (short 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 separately.
				if (comm == "BFC CERTIFY INVERTNEXT")
					return new LDBFC (LDBFC::InvertNext);
			}
			
			if (tokens.size() > 2 && tokens[1] == "!LDFORGE") {
				// Handle LDForge-specific types, they're embedded into comments
				
				if (tokens[2] == "VERTEX") {
					// Vertex (0 !LDFORGE VERTEX)
					CHECK_TOKEN_COUNT (7)
					CHECK_TOKEN_NUMBERS (3, 6)
					
					LDVertex* obj = new LDVertex;
					obj->setColor (tokens[3].toLong ());
					
					for (const Axis ax : g_Axes)
						obj->pos[ax] = tokens[4 + ax].toDouble (); // 4 - 6
					
					return obj;
				}
				
				if (tokens[2] == "RADIAL") {
					CHECK_TOKEN_COUNT (20)
					CHECK_TOKEN_NUMBERS (4, 19)
					
					LDRadial::Type eType = LDRadial::NumTypes;
					
					for (int i = 0; i < LDRadial::NumTypes; ++i) {
						if (str (LDRadial::radialTypeName ((LDRadial::Type) i)).toUpper ().remove (' ') == tokens[3]) {
							eType = (LDRadial::Type) i;
							break;
						}
					}
					
					if (eType == LDRadial::NumTypes)
						return new LDGibberish (line, fmt ("Unknown radial type %1", tokens[3]));
					
					LDRadial* obj = new LDRadial;
					
					obj->setType (eType);
					obj->setColor (tokens[4].toLong ());
					obj->setSegments (tokens[5].toLong ());
					obj->setDivisions (tokens[6].toLong ());
					obj->setNumber (tokens[7].toLong ());
					obj->setPosition (parseVertex (tokens, 8)); // 8 - 10
					
					matrix transform;
					for (short i = 0; i < 9; ++i)
						transform[i] = tokens[i + 11].toDouble (); // 11 - 19
					
					obj->setTransform (transform);
					return obj;
				}
			}
			
			LDComment* obj = new LDComment;
			obj->text = comm;
			return obj;
		}
	
	case 1:
		{
			// Subfile
			CHECK_TOKEN_COUNT (15)
			CHECK_TOKEN_NUMBERS (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;
			LDOpenFile* load = getFile (tokens[14]);
			g_loadingMainFile = tmp;
			
			// If we cannot open the file, mark it an error
			if (!load)
				return new LDGibberish (line, "Could not open referred file");
			
			LDSubfile* obj = new LDSubfile;
			obj->setColor (tokens[1].toLong ());
			obj->setPosition (parseVertex (tokens, 2)); // 2 - 4
			
			matrix transform;
			for (short i = 0; i < 9; ++i)
				transform[i] = tokens[i + 5].toDouble (); // 5 - 13
			
			obj->setTransform (transform);
			obj->setFileInfo (load);
			return obj;
		}
	
	case 2:
		{
			CHECK_TOKEN_COUNT (8)
			CHECK_TOKEN_NUMBERS (1, 7)
			
			// Line
			LDLine* obj = new LDLine;
			obj->setColor (tokens[1].toLong ());
			for (short i = 0; i < 2; ++i)
				obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 7
			return obj;
		}
	
	case 3:
		{
			CHECK_TOKEN_COUNT (11)
			CHECK_TOKEN_NUMBERS (1, 10)
			
			// Triangle
			LDTriangle* obj = new LDTriangle;
			obj->setColor (tokens[1].toLong ());
			
			for (short i = 0; i < 3; ++i)
				obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 10
			
			return obj;
		}
	
	case 4:
		{
			CHECK_TOKEN_COUNT (14)
			CHECK_TOKEN_NUMBERS (1, 13)
			
			// Quadrilateral
			LDQuad* obj = new LDQuad;
			obj->setColor (tokens[1].toLong ());
			
			for (short i = 0; i < 4; ++i)
				obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 13
			
			return obj;
		}
	
	case 5:
		{
			CHECK_TOKEN_COUNT (14)
			CHECK_TOKEN_NUMBERS (1, 13)
			
			// Conditional line
			LDCondLine* obj = new LDCondLine;
			obj->setColor (tokens[1].toLong ());
			
			for (short i = 0; i < 4; ++i)
				obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 13
			
			return obj;
		}
	
	default: // Strange line we couldn't parse
		return new LDGibberish (line, "Unknown line code number");
	}
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
LDOpenFile* getFile (str fname) {
	// Try find the file in the list of loaded files
	LDOpenFile* load = findLoadedFile (fname);
	
	// If it's not loaded, try open it
	if (!load)
		load = openDATFile (fname, true);
	
	return load;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void reloadAllSubfiles () {
	if (!g_curfile)
		return;
	
	g_loadedFiles.clear ();
	g_loadedFiles << g_curfile;
	
	// Go through all objects in the current file and reload the subfiles
	for (LDObject* obj : g_curfile->objs ()) {
		if (obj->getType() == LDObject::Subfile) {
			LDSubfile* ref = static_cast<LDSubfile*> (obj);
			LDOpenFile* fileInfo = getFile (ref->fileInfo ()->name ());
			
			if (fileInfo)
				ref->setFileInfo (fileInfo);
			else {
				// Couldn't load the file, mark it an error
				ref->replace (new LDGibberish (ref->raw (), "Could not open referred file"));
			}
		}
		
		// Reparse gibberish files. It could be that they are invalid because
		// of loading errors. Circumstances may be different now.
		if (obj->getType () == LDObject::Gibberish)
			obj->replace (parseLine (static_cast<LDGibberish*> (obj)->contents));
	}
	
	// Close all files left unused
	LDOpenFile::closeUnused ();
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
ulong LDOpenFile::addObject (LDObject* obj) {
	PROP_NAME (history).add (new AddHistory (PROP_NAME (objs).size (), obj));
	PROP_NAME (objs) << obj;
	
	if (obj->getType () == LDObject::Vertex)
		PROP_NAME (vertices) << obj;
	
	if (this == g_curfile)
		g_BBox.calcObject (obj);
	
	return numObjs () - 1;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void LDOpenFile::insertObj (const ulong pos, LDObject* obj) {
	m_history.add (new AddHistory (pos, obj));
	m_objs.insert (pos, obj);
	
	if (this == g_curfile)
		g_BBox.calcObject (obj);
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void LDOpenFile::forgetObject (LDObject* obj) {
	ulong idx = obj->getIndex (this);
	m_history.add (new DelHistory (idx, obj));
	m_objs.erase (idx);
	
	// Update the bounding box
	if (this == g_curfile)
		g_BBox.calculate ();
}

// =============================================================================
bool safeToCloseAll () {
	for (LDOpenFile* f : g_loadedFiles)
		if (!f->safeToClose ())
			return false;
	
	return true;
}

// =============================================================================
void LDOpenFile::setObject (ulong idx, LDObject* obj) {
	assert (idx < numObjs ());
	
	str oldcode = m_objs[idx]->raw ();
	str newcode = obj->raw ();
	m_history << new EditHistory (idx, oldcode, newcode);
	
	m_objs[idx] = obj;
}

static vector<LDOpenFile*> getFilesUsed (LDOpenFile* node) {
	vector<LDOpenFile*> filesUsed;
	
	for (LDObject* obj : *node) {
		if (obj->getType () != LDObject::Subfile)
			continue;
		
		LDSubfile* ref = static_cast<LDSubfile*> (obj);
		filesUsed << ref->fileInfo ();
		filesUsed << getFilesUsed (ref->fileInfo ());
	}
	
	return filesUsed;
}

// =============================================================================
// Find out which files are unused and close them.
void LDOpenFile::closeUnused () {
	vector<LDOpenFile*> filesUsed = getFilesUsed (g_curfile);
	
	// Also, anything that's explicitly opened must not be closed
	for (LDOpenFile* file : g_loadedFiles)
		if (file->implicit () == false)
			filesUsed << file;
	
	// Remove duplicated entries
	filesUsed.makeUnique ();
	
	// Close all open files that aren't in filesUsed
	for (LDOpenFile* file : g_loadedFiles) {
		bool isused = false;
		
		for (LDOpenFile* usedFile : filesUsed) {
			if (file == usedFile) {
				isused = true;
				break;
			}
		}
		
		if (!isused)
			delete file;
	}
	
	g_loadedFiles.clear ();
	g_loadedFiles << filesUsed;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void recursiveGetFilenames (QDir dir, vector<str>& fnames) {
	QFileInfoList flist = dir.entryInfoList ();
	for (const QFileInfo& info : flist) {
		if (info.fileName () == "." || info.fileName () == "..")
			continue; // skip . and ..
		
		if (info.isDir ())
			recursiveGetFilenames (QDir (info.absoluteFilePath ()), fnames);
		else
			fnames << info.absoluteFilePath ();
	}
}

void PrimitiveLister::work () {
	g_activePrimLister = this;
	m_prims.clear ();
	
	QDir dir (LDPaths::prims ());
	assert (dir.exists ());
	
	ulong baselen = dir.absolutePath ().length ();
	
	vector<str> fnames;
	recursiveGetFilenames (dir, fnames);
	emit starting (fnames.size ());
	
	ulong i = 0;
	for (str fname : fnames) {
		File f (fname, File::Read);
		
		PrimitiveInfo info;
		info.name = fname.mid (baselen + 1); // make full path relative
		info.name.replace ('/', '\\'); // use DOS backslashes, they're expected
		
		if (!f.readLine (info.title))
			info.title = "";
		
		info.title = info.title.simplified ();
		
		if (info.title[0] == '0') {
			info.title.remove (0, 1); // remove 0
			info.title = info.title.simplified ();
		}
		
		m_prims << info;
		emit update (++i);
	}
	
	// Save to a config file
	File conf (config::dirpath () + "prims.cfg", File::Write);
	for (PrimitiveInfo& info : m_prims)
		fprint (conf, "%1 %2\n", info.name, info.title);
	
	conf.close ();
	
	g_primListerMutex = true;
	g_Primitives = m_prims;
	g_primListerMutex = false;
	
	g_activePrimLister = null;
	emit workDone ();
}

void PrimitiveLister::start () {
	if (g_activePrimLister)
		return;
	
	PrimitiveLister* lister = new PrimitiveLister;
	QThread* listerThread = new QThread;
	lister->moveToThread (listerThread);
	connect (lister, SIGNAL (starting (ulong)), g_win, SLOT (primitiveLoaderStart (ulong)));
	connect (lister, SIGNAL (update (ulong)), g_win, SLOT (primitiveLoaderUpdate (ulong)));
	connect (lister, SIGNAL (workDone ()), g_win, SLOT (primitiveLoaderEnd ()));
	connect (listerThread, SIGNAL (started ()), lister, SLOT (work ()));
	connect (listerThread, SIGNAL (finished ()), lister, SLOT (deleteLater ()));
	listerThread->start ();
}

void loadPrimitiveInfo () {
	g_Primitives.clear ();
	
	// Try to load prims.cfg
	File conf (config::dirpath () + "prims.cfg", File::Read);
	if (!conf) {
		// No prims.cfg, build it
		PrimitiveLister::start ();
		return;
	}
	
	for (str line : conf) {
		int space = line.indexOf (" ");
		if (space == -1)
			continue;
		
		PrimitiveInfo info;
		info.name = line.left (space);
		info.title = line.mid (space + 1);
		g_Primitives << info;
	}
}

mercurial