src/file.cpp

Tue, 21 May 2013 14:24:10 +0300

author
Santeri Piippo <crimsondusk64@gmail.com>
date
Tue, 21 May 2013 14:24:10 +0300
changeset 234
79b120b0aa51
parent 232
4e44c92e21dd
child 235
bb0a7bcca7eb
permissions
-rw-r--r--

added basename since windows doesn't have 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 <QDir>

#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"

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

// =============================================================================
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.path ();
		}
	}
	
	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 ("%s" DIRSLASH "parts", path.chars ());
		pathInfo.LDConfigPath = fmt ("%s" DIRSLASH "LDConfig.ldr", path.chars ());
		pathInfo.primsPath = fmt ("%s" DIRSLASH "p", path.chars ());
		
		return true;
	}
	
	// Accessors
	str getError () { return pathError; }
	str ldconfig () { return pathInfo.LDConfigPath; }
	str prims () { return pathInfo.primsPath; }
	str parts () { return pathInfo.partsPath; }
}

// =============================================================================
LDOpenFile::LDOpenFile () {
	m_implicit = true;
	savePos = -1;
}

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

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

// =============================================================================
str dirname (str path) {
	long lastpos; // FIXME: why doesn't str::last () work here?
	for (lastpos = path.len () - 1; lastpos >= 0; --lastpos)
		if (path[(size_t) lastpos] == DIRSLASH_CHAR)
			break;
	
	if (lastpos > 0)
		return path.substr (0, lastpos);
	
	return "";
}

// =============================================================================
str basename (str path) {
	long lastpos; // FIXME: why doesn't str::last () work here?
	for (lastpos = path.len () - 1; lastpos >= 0; --lastpos)
		if (path[(size_t) lastpos] == DIRSLASH_CHAR)
			break;
	
	if (lastpos < (log) path.len () - 1)
		return path.substr (lastpos + 1, -1);
	
	return "";
}

// =============================================================================
FILE* openLDrawFile (str relpath, bool subdirs) {
	printf ("%s: Try to open %s\n", __func__, relpath.c ());
#ifndef WIN32
	relpath.replace ("\\", "/");
#endif // WIN32
	
	if (g_curfile != null) {
		str partpath = fmt ("%s" DIRSLASH "%s", dirname (g_curfile->m_filename).c (), relpath.c ());
		printf ("try %s\n", partpath.c ());
		FILE* fp = fopen (partpath, "r");
		
		if (fp != null)
			return fp;
	}
	
	printf ("try %s\n", relpath.chars ());
	FILE* fp = fopen (relpath, "r");
	str fullPath;
	
	if (fp != null)
		return fp;
	
	if (~io_ldpath.value) {
		// Try with just the LDraw path first
		fullPath = fmt ("%s" DIRSLASH "%s", io_ldpath.value.chars(), relpath.chars());
		printf ("try %s\n", fullPath.chars ());
		
		fp = fopen (fullPath, "r");
		if (fp != null)
			return fp;
		
		if (subdirs) {
			for (auto subdir : initlist<const char*> ({"parts", "p"})) {
				fullPath = fmt ("%s" DIRSLASH "%s" DIRSLASH "%s",
					io_ldpath.value.chars(), subdir, relpath.chars());
				
				printf ("try %s\n", fullPath.chars ());
				fp = fopen (fullPath.chars (), "r");
				
				if (fp)
					return fp;
			}
		}
	}
	
	return null;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
std::vector<LDObject*> loadFileContents (FILE* fp, ulong* numWarnings) {
	char line[1024];
	vector<str> lines;
	vector<LDObject*> objs;
	ulong lnum = 0;
	
	if (numWarnings)
		*numWarnings = 0;
	
	while (fgets (line, sizeof line, fp)) {
		// Trim the trailing newline
		str data = line;
		while (data[~data - 1] == '\n' || data[~data - 1] == '\r')
			data -= 1;
		
		LDObject* obj = parseLine (data);
		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",
				lnum, static_cast<LDGibberish*> (obj)->reason.chars());
			
			logf (LOG_Warning, "- Line was: %s\n", data.chars());
			
			if (numWarnings)
				(*numWarnings)++;
		}
		
		objs.push_back (obj);
		lnum++;
	}
	
	return objs;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
LDOpenFile* openDATFile (str path, bool search, bool mainfile) {
	logf ("Opening %s...\n", path.chars());
	
	// 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* fp;
	if (search)
		fp = openLDrawFile (-path, true);
	else
		fp = fopen (path, "r");
	
	if (!fp) {
		logf (LOG_Error, "Couldn't open %s: %s\n", path.chars (), strerror (errno));
		return null;
	}
	
	LDOpenFile* load = new LDOpenFile;
	load->m_filename = path;
	
	if (mainfile)
		g_curfile = load;
	
	ulong numWarnings;
	std::vector<LDObject*> objs = loadFileContents (fp, &numWarnings);
	
	for (LDObject* obj : objs)
		load->m_objs.push_back (obj);
	
	fclose (fp);
	g_loadedFiles.push_back (load);
	
	logf ("File %s parsed successfully (%lu warning%s).\n",
		path.chars(), 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 (!m_implicit && History::pos () != savePos) {
		switch (QMessageBox::question (g_win, "Unsaved Changes",
			fmt ("There are unsaved changes to %s. Should it be saved?",
			(m_filename.len () > 0) ? m_filename.chars () : "<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 (m_filename.len () == 0) {
				str path = QFileDialog::getSaveFileName (g_win, "Save As",
					g_curfile->m_filename, "LDraw files (*.dat *.ldr)");
				
				if (path.len () == 0)
					return false;
				
				m_filename = path;
			}
			
			if (!save ()) {
				str errormsg = fmt ("Failed to save %s: %s\nDo you still want to close?",
					m_filename.chars (), strerror (lastError));
				
				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->fullRefresh ();
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void newFile () {
	// Create a new anonymous file and set it to our current
	closeAll ();
	
	LDOpenFile* f = new LDOpenFile;
	f->m_filename = "";
	f->m_implicit = false;
	g_loadedFiles.push_back (f);
	g_curfile = f;
	
	History::clear ();
	
	g_BBox.reset ();
	g_win->fullRefresh ();
	g_win->updateTitle ();
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void addRecentFile (str path) {
	size_t pos = io_recentfiles.value.first (path);
	
	// If this file already is in the list, pop it out.
	if (pos != npos) {
		if (~io_recentfiles.value == ~path)
			return; // only recent file - do nothing
		
		// Pop it out.
		str front = io_recentfiles.value.substr (0, pos);
		str back;
		
		if (pos + ~path + 1 < io_recentfiles.value.len ())
			back = io_recentfiles.value.substr (pos + ~path + 1, -1);
		else
			back = "";
		
		io_recentfiles.value = front + back;
	}
	
	// If there's too many recent files, drop one out.
	while (io_recentfiles.value.count ('@') > 3)
		io_recentfiles.value = io_recentfiles.value.substr (io_recentfiles.value.first ("@") + 1, -1);
	
	// Add the file
	if (~io_recentfiles.value > 0)
		io_recentfiles.value += "@";
	
	io_recentfiles += path;
	
	config::save ();
	g_win->updateRecentFilesMenu ();
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void openMainFile (str path) {
	closeAll ();
	
	LDOpenFile* file = openDATFile (path, false, true);
	
	if (!file) {
		// Tell the user loading failed.
		setlocale (LC_ALL, "C");
		critical (fmt ("Failed to open %s: %s", path.chars(), strerror (errno)));
		return;
	}
	
	file->m_implicit = 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 ()->resetAngles ();
	
	History::clear ();
	
	// Add it to the recent files list.
	addRecentFile (path);
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
bool LDOpenFile::save (str path) {
	if (!~path)
		path = m_filename;
	
	FILE* fp = fopen (path, "w");
	
	if (!fp) {
		lastError = errno;
		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 (m_implicit == false && m_objs.size () >= 2 && object (1)->getType () == LDObject::Comment) {
		fpathComment = static_cast<LDComment*> (object (1));
		
		if (fpathComment->text.substr (0, 6) == "Name: ") {
			str newfname;
			str dir = basename (dirname (path));
			
			if (dir == "s" || dir == "48")
				newfname = fmt ("%s\\", dir.c ());
			
			newfname += basename (path);
			fpathComment->text = fmt ("Name: %s", newfname.c ());
			g_win->buildObjList ();
		}
	}
	
	// Write all entries now
	for (LDObject* obj : m_objs) {
		// LDraw requires files to have DOS line endings
		str line = fmt ("%s\r\n", obj->getContents ().chars ());
		fwrite (line.chars(), 1, line.len (), fp);
	}
	
	fclose (fp);
	
	// We have successfully saved, update the save position now.
	savePos = History::pos ();
	m_filename = path;
	
	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 #%u was `%s`, expected a number", \
				(i + 1), tokens[i].chars()));

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
static vertex parseVertex (vector<str>& 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] = atof (s[n + ax]);
	
	return v;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
LDObject* parseLine (str line) {
	vector<str> tokens = line.split (" ");
	
	if (!tokens.size ()) {
		// Line was empty, or only consisted of whitespace
		return new LDEmpty;
	}
	
	if (~tokens[0] != 1)
		return new LDGibberish (line, "Illogical line code");
	
	const char c = tokens[0][0];
	switch (c - '0') {
	case 0:
		{
			// Comment
			str comm;
			for (uint 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 %s", 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->color = atol (tokens[3]);
					
					for (const Axis ax : g_Axes)
						obj->pos[ax] = atof (tokens[4 + ax]); // 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)).upper ().strip (' ') == tokens[3]) {
							eType = (LDRadial::Type) i;
							break;
						}
					}
					
					if (eType == LDRadial::NumTypes)
						return new LDGibberish (line, fmt ("Unknown radial type %s", tokens[3].chars ()));
					
					LDRadial* obj = new LDRadial;
					
					obj->radType = eType;			// 3
					obj->color = atol (tokens[4]);		// 4
					obj->segs = atol (tokens[5]);	// 5
					obj->divs = atol (tokens[6]);	// 6
					obj->ringNum = atol (tokens[7]);	// 7
					
					obj->pos = parseVertex (tokens, 8); // 8 - 10
					
					for (short i = 0; i < 9; ++i)
						obj->transform[i] = atof (tokens[i + 11]); // 11 - 19
					
					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
			LDOpenFile* pFile = loadSubfile (tokens[14]);
			
			// If we cannot open the file, mark it an error
			if (!pFile)
				return new LDGibberish (line, "Could not open referred file");
			
			LDSubfile* obj = new LDSubfile;
			obj->color = atol (tokens[1]);
			obj->pos = parseVertex (tokens, 2); // 2 - 4
			
			for (short i = 0; i < 9; ++i)
				obj->transform[i] = atof (tokens[i + 5]); // 5 - 13
			
			obj->fileName = tokens[14];
			obj->fileInfo = pFile;
			return obj;
		}
	
	case 2:
		{
			CHECK_TOKEN_COUNT (8)
			CHECK_TOKEN_NUMBERS (1, 7)
			
			// Line
			LDLine* obj = new LDLine;
			obj->color = atol (tokens[1]);
			for (short i = 0; i < 2; ++i)
				obj->coords[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->color = atol (tokens[1]);
			
			for (short i = 0; i < 3; ++i)
				obj->coords[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->color = atol (tokens[1]);
			
			for (short i = 0; i < 4; ++i)
				obj->coords[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->color = atol (tokens[1]);
			
			for (short i = 0; i < 4; ++i)
				obj->coords[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* loadSubfile (str zFile) {
	// Try open the file
	LDOpenFile* pFile = findLoadedFile (zFile);
	if (!pFile)
		pFile = openDATFile (zFile, true, false);
	
	return pFile;
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void reloadAllSubfiles () {
	if (!g_curfile)
		return;
	
	// First, close all but the current open file.
	for (LDOpenFile* file : g_loadedFiles)
		if (file != g_curfile)
			delete file;
	
	g_loadedFiles.clear ();
	g_loadedFiles.push_back (g_curfile);
	
	// Go through all objects in the current file and reload the subfiles
	for (LDObject* obj : g_curfile->m_objs) {
		if (obj->getType() == LDObject::Subfile) {
			// Note: ref->pFile is invalid right now since all subfiles were closed.
			LDSubfile* ref = static_cast<LDSubfile*> (obj);
			LDOpenFile* pFile = loadSubfile (ref->fileName);
			
			if (pFile)
				ref->fileInfo = pFile;
			else {
				// Couldn't load the file, mark it an error
				ref->replace (new LDGibberish (ref->getContents (), "Could not open referred file"));
			}
		}
		
		// Reparse gibberish files. It could be that they are invalid because
		// the file could not be opened. Circumstances may be different now.
		if (obj->getType() == LDObject::Gibberish)
			obj->replace (parseLine (static_cast<LDGibberish*> (obj)->contents));
	}
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
ulong LDOpenFile::addObject (LDObject* obj) {
	m_objs.push_back (obj);
	
	if (this == g_curfile)
		g_BBox.calcObject (obj);
	
	return m_objs.size() - 1;
}

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

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
void LDOpenFile::forgetObject (LDObject* obj) {
	// Find the index for the given object
	ulong ulIndex;
	for (ulIndex = 0; ulIndex < (ulong)m_objs.size(); ++ulIndex)
		if (m_objs[ulIndex] == obj)
			break; // found it
	
	if (ulIndex >= m_objs.size ())
		return; // was not found
	
	// Erase it from memory
	m_objs.erase (m_objs.begin() + ulIndex);
	
	// Update the bounding box
	if (this == g_curfile)
		g_BBox.calculate ();
}

// =============================================================================
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// =============================================================================
std::vector<partListEntry> g_PartList;

void initPartList () {
	logf ("%s: initializing parts.lst\n", __func__);
	
	FILE* fp = openLDrawFile ("parts.lst", false);
	
	if (!fp)
		return;
	
	char sLine[1024];
	while (fgets (sLine, sizeof sLine, fp)) {
		// Locate the first whitespace
		char* cpWhite = strstr (sLine, " ");
		
		char sName[65];
		size_t uLength = (cpWhite - sLine);
		
		if (uLength >= 64)
			continue; // too long
		
		strncpy (sName, sLine, uLength);
		sName[uLength] = '\0';
		
		// Surf through the whitespace sea!
		while (*cpWhite == ' ')
			cpWhite++;
		
		// Get the end point
		char* cpEnd = strstr (sLine, "\r");
		
		if (cpEnd == null) {
			// must not be DOS-formatted
			cpEnd = strstr (sLine, "\n");
		}
		
		assert (cpEnd != null);
		
		// Make the file title now
		char sTitle[81];
		uLength = (cpEnd - cpWhite);
		strncpy (sTitle, cpWhite, uLength);
		sTitle[uLength] = '\0';
		
		// Add it to the array.
		partListEntry entry;
		strcpy (entry.sName, sName);
		strcpy (entry.sTitle, sTitle);
		g_PartList.push_back (entry);
	}
}

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

mercurial