--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ldDocument.cpp Tue Mar 03 16:55:36 2015 +0200 @@ -0,0 +1,1540 @@ +/* + * LDForge: LDraw parts authoring CAD + * Copyright (C) 2013 - 2015 Teemu 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 <QTime> +#include <QApplication> + +#include "main.h" +#include "configuration.h" +#include "ldDocument.h" +#include "miscallenous.h" +#include "mainWindow.h" +#include "editHistory.h" +#include "dialogs.h" +#include "glRenderer.h" +#include "glCompiler.h" +#include "partDownloader.h" + +CFGENTRY (String, LDrawPath, "") +CFGENTRY (List, RecentFiles, {}) +CFGENTRY (Bool, TryDownloadMissingFiles, false) +EXTERN_CFGENTRY (String, DownloadFilePath) +EXTERN_CFGENTRY (Bool, UseLogoStuds) + +static bool g_loadingMainFile = false; +static const int g_maxRecentFiles = 10; +static bool g_aborted = false; +static LDDocumentPtr g_logoedStud; +static LDDocumentPtr g_logoedStud2; +static QList<LDDocumentWeakPtr> g_allDocuments; +static QList<LDDocumentPtr> g_explicitDocuments; +static LDDocumentPtr g_currentDocument; +static bool g_loadingLogoedStuds = false; + +const QStringList g_specialSubdirectories ({ "s", "48", "8" }); + +// ============================================================================= +// +namespace LDPaths +{ + static QString pathError; + + struct + { + QString LDConfigPath; + QString partsPath, primsPath; + } pathInfo; + + void initPaths() + { + if (not tryConfigure (cfg::LDrawPath)) + { + LDrawPathDialog dlg (false); + + if (not dlg.exec()) + Exit(); + + cfg::LDrawPath = dlg.filename(); + } + } + + bool tryConfigure (QString path) + { + QDir dir; + + if (not 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 = format ("%1" DIRSLASH "parts", path); + pathInfo.LDConfigPath = format ("%1" DIRSLASH "LDConfig.ldr", path); + pathInfo.primsPath = format ("%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 (LDDocumentPtr* selfptr) : + m_isImplicit (true), + m_flags (0), + m_verticesOutdated (true), + m_needVertexMerge (true), + m_gldata (new LDGLData) +{ + *selfptr = LDDocumentPtr (this); + setSelf (*selfptr); + setSavePosition (-1); + setTabIndex (-1); + setHistory (new History); + history()->setDocument (*selfptr); + m_needsReCache = true; + g_allDocuments << *selfptr; +} + +// ============================================================================= +// +LDDocumentPtr LDDocument::createNew() +{ + LDDocumentPtr ptr; + new LDDocument (&ptr); + return ptr; +} + +// ============================================================================= +// +LDDocument::~LDDocument() +{ + // Don't bother during program termination + if (IsExiting()) + return; + + g_allDocuments.removeOne (self()); + m_flags |= DOCF_IsBeingDestroyed; + delete m_history; + delete m_gldata; +} + +// ============================================================================= +// +void LDDocument::setImplicit (bool const& a) +{ + if (m_isImplicit != a) + { + m_isImplicit = a; + + if (a == false) + { + g_explicitDocuments << self().toStrongRef(); + print ("Opened %1", name()); + + // Implicit files are not compiled by the GL renderer. Now that this + // part is no longer implicit, it needs to be compiled. + if (g_win != null) + g_win->R()->compiler()->compileDocument (self()); + } + else + { + g_explicitDocuments.removeOne (self().toStrongRef()); + print ("Closed %1", name()); + } + + if (g_win != null) + g_win->updateDocumentList(); + + // If the current document just became implicit (e.g. it was 'closed'), + // we need to get a new current document. + if (current() == self() and isImplicit()) + { + if (explicitDocuments().isEmpty()) + newFile(); + else + setCurrent (explicitDocuments().first()); + } + } +} + +// ============================================================================= +// +QList<LDDocumentPtr> const& LDDocument::explicitDocuments() +{ + return g_explicitDocuments; +} + +// ============================================================================= +// +LDDocumentPtr FindDocument (QString name) +{ + for (LDDocumentWeakPtr weakfile : g_allDocuments) + { + if (weakfile == null) + continue; + + LDDocumentPtr file (weakfile.toStrongRef()); + + if (Eq (name, file->name(), file->defaultName())) + return file; + } + + return LDDocumentPtr(); +} + +// ============================================================================= +// +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 FindDocumentPath (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 (LDDocumentWeakPtr doc : g_allDocuments) + { + if (doc == null) + continue; + + QString partpath = format ("%1/%2", Dirname (doc.toStrongRef()->fullPath()), 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 and reltop != s) or (reltop == s and proptop != s)) + { + bogus = true; + break; + } + } + + if (not bogus) + return partpath; + } + } + + if (QFile::exists (relpath)) + return relpath; + + // Try with just the LDraw path first + fullPath = format ("%1" DIRSLASH "%2", cfg::LDrawPath, 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 : QList<QString> ({ cfg::LDrawPath, cfg::DownloadFilePath })) + { + for (const QString& subdir : QList<QString> ({ "parts", "p" })) + { + fullPath = format ("%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) +{ + print ("Opening %1...\n", relpath); + QString path = FindDocumentPath (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 ldDocument.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 (lines().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 (LDObjectPtr obj : m_objects) + obj->destroy(); + + m_objects.clear(); + setDone (true); + return; + } + + // Parse up to 300 lines per iteration + int max = i + 300; + + for (; i < max and i < (int) lines().size(); ++i) + { + QString line = lines()[i]; + + // Trim the trailing newline + QChar c; + + while (line.endsWith ("\n") or line.endsWith ("\r")) + line.chop (1); + + LDObjectPtr obj = ParseLine (line); + + // Check for parse errors and warn about tthem + if (obj->type() == OBJ_Error) + { + print ("Couldn't parse line #%1: %2", + progress() + 1, obj.staticCast<LDError>()->reason()); + + if (warnings() != null) + (*warnings())++; + } + + 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) lines().size()) - 1) + { + emit workDone(); + setDone (true); + return; + } + + // Otherwise, continue, by recursing back. + if (not 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 (not fp->atEnd()) + 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 (not loader->isDone()) + qApp->processEvents(); + + // If we wanted the success value, supply that now + if (ok) + *ok = not loader->isAborted(); + + objs = loader->objects(); + delete loader; + return objs; +} + +// ============================================================================= +// +LDDocumentPtr OpenDocument (QString path, bool search, bool implicit, LDDocumentPtr fileToOverride) +{ + // Convert the file name to lowercase when searching because some parts contain subfile + // subfile references with uppercase file names. I'll assume here that the library will always + // use lowercase file names for the part files. + QFile* fp; + QString fullpath; + + if (search) + { + fp = OpenLDrawFile (path.toLower(), true, &fullpath); + } + else + { + fp = new QFile (path); + fullpath = path; + + if (not fp->open (QIODevice::ReadOnly)) + { + delete fp; + return LDDocumentPtr(); + } + } + + if (not fp) + return LDDocumentPtr(); + + LDDocumentPtr load = (fileToOverride != null ? fileToOverride : LDDocument::createNew()); + load->setImplicit (implicit); + load->setFullPath (fullpath); + load->setName (LDDocument::shortenName (load->fullPath())); + + // Loading the file shouldn't count as actual edits to the document. + load->history()->setIgnoring (true); + + int numWarnings; + bool ok; + LDObjectList objs = LoadFileContents (fp, &numWarnings, &ok); + fp->close(); + fp->deleteLater(); + + if (not ok) + { + load->dismiss(); + return LDDocumentPtr(); + } + + load->addObjects (objs); + + if (g_loadingMainFile) + { + LDDocument::setCurrent (load); + g_win->R()->setDocument (load); + print (QObject::tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings); + } + + load->history()->setIgnoring (false); + return load; +} + +// ============================================================================= +// +bool LDDocument::isSafeToClose() +{ + using msgbox = QMessageBox; + setlocale (LC_ALL, "C"); + + // If we have unsaved changes, warn and give the option of saving. + if (hasUnsavedChanges()) + { + QString message = format (QObject::tr ("There are unsaved changes to %1. Should it be saved?"), getDisplayName()); + + int button = msgbox::question (g_win, QObject::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 (name().length() == 0) + { + QString newpath = QFileDialog::getSaveFileName (g_win, QObject::tr ("Save As"), + CurrentDocument()->name(), QObject::tr ("LDraw files (*.dat *.ldr)")); + + if (newpath.length() == 0) + return false; + + setName (newpath); + } + + if (not save()) + { + message = format (QObject::tr ("Failed to save %1 (%2)\nDo you still want to close?"), + name(), strerror (errno)); + + if (msgbox::critical (g_win, QObject::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 CloseAllDocuments() +{ + for (LDDocumentPtr file : g_explicitDocuments) + file->dismiss(); +} + +// ============================================================================= +// +void newFile() +{ + // Create a new anonymous file and set it to our current + LDDocumentPtr f = LDDocument::createNew(); + f->setName (""); + f->setImplicit (false); + LDDocument::setCurrent (f); + LDDocument::closeInitialFile(); + g_win->R()->setDocument (f); + g_win->doFullRefresh(); + g_win->updateTitle(); + g_win->updateActions(); +} + +// ============================================================================= +// +void AddRecentFile (QString path) +{ + int idx = cfg::RecentFiles.indexOf (path); + + // If this file already is in the list, pop it out. + if (idx != -1) + { + if (idx == cfg::RecentFiles.size() - 1) + return; // first recent file - abort and do nothing + + cfg::RecentFiles.removeAt (idx); + } + + // If there's too many recent files, drop one out. + while (cfg::RecentFiles.size() > (g_maxRecentFiles - 1)) + cfg::RecentFiles.removeAt (0); + + // Add the file + cfg::RecentFiles << path; + + Config::Save(); + g_win->updateRecentFilesMenu(); +} + +// ============================================================================= +// Open an LDraw file and set it as the main model +// ============================================================================= +void OpenMainModel (QString path) +{ + // If there's already a file with the same name, this file must replace it. + LDDocumentPtr documentToReplace; + LDDocumentPtr file; + QString shortName = LDDocument::shortenName (path); + + for (LDDocumentWeakPtr doc : g_allDocuments) + { + if (doc != null and doc.toStrongRef()->name() == shortName) + { + documentToReplace = doc; + break; + } + } + + // We cannot open this file if the document this would replace is not + // safe to close. + if (documentToReplace != null and not documentToReplace->isSafeToClose()) + return; + + g_loadingMainFile = true; + + // If we're replacing an existing document, clear the document and + // make it ready for being loaded to. + if (documentToReplace != null) + { + file = documentToReplace; + file->clear(); + } + + file = OpenDocument (path, false, false, file); + + if (file == null) + { + if (not g_aborted) + { + // Tell the user loading failed. + setlocale (LC_ALL, "C"); + Critical (format (QObject::tr ("Failed to open %1: %2"), path, strerror (errno))); + } + + g_loadingMainFile = false; + return; + } + + file->setImplicit (false); + + // 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; + + // If there were problems loading subfile references, try see if we can find these + // files on the parts tracker. + QStringList unknowns; + + for (LDObjectPtr obj : file->objects()) + { + if (obj->type() != OBJ_Error or obj.staticCast<LDError>()->fileReferenced().isEmpty()) + continue; + + unknowns << obj.staticCast<LDError>()->fileReferenced(); + } + + if (cfg::TryDownloadMissingFiles and not unknowns.isEmpty()) + { + PartDownloader dl; + + if (dl.checkValidPath()) + { + dl.setSource (PartDownloader::PartsTracker); + dl.setPrimaryFile (file); + + for (QString const& unknown : unknowns) + dl.downloadFromPartsTracker (unknown); + + dl.exec(); + dl.checkIfFinished(); + file->reloadAllSubfiles(); + } + } +} + +// ============================================================================= +// +bool LDDocument::save (QString path, int64* sizeptr) +{ + if (isImplicit()) + return false; + + if (not path.length()) + path = fullPath(); + + // If the second object in the list holds the file name, update that now. + LDObjectPtr nameObject = getObject (1); + + if (nameObject != null and nameObject->type() == OBJ_Comment) + { + LDCommentPtr nameComment = nameObject.staticCast<LDComment>(); + + if (nameComment->text().left (6) == "Name: ") + { + QString newname = shortenName (path); + nameComment->setText (format ("Name: %1", newname)); + g_win->buildObjList(); + } + } + + QByteArray data; + + if (sizeptr != null) + *sizeptr = 0; + + // 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 (LDObjectPtr obj : objects()) + { + QByteArray subdata ((obj->asText() + "\r\n").toUtf8()); + data.append (subdata); + + if (sizeptr != null) + *sizeptr += subdata.size(); + } + + QFile f (path); + + if (not f.open (QIODevice::WriteOnly)) + return false; + + f.write (data); + f.close(); + + // We have successfully saved, update the save position now. + setSavePosition (history()->position()); + setFullPath (path); + setName (shortenName (path)); + + g_win->updateDocumentListItem (self().toStrongRef()); + g_win->updateTitle(); + return true; +} + +// ============================================================================= +// +void LDDocument::clear() +{ + for (LDObjectPtr obj : objects()) + forgetObject (obj); +} + +// ============================================================================= +// +static void CheckTokenCount (const QStringList& tokens, int num) +{ + if (tokens.size() != num) + throw QString (format ("Bad amount of tokens, expected %1, got %2", num, tokens.size())); +} + +// ============================================================================= +// +static void CheckTokenNumbers (const QStringList& tokens, int min, int max) +{ + bool ok; + + QRegExp scient ("\\-?[0-9]+\\.[0-9]+e\\-[0-9]+"); + + for (int i = min; i <= max; ++i) + { + // Check for floating point + tokens[i].toDouble (&ok); + if (ok) + return; + + // Check hex + if (tokens[i].startsWith ("0x")) + { + tokens[i].mid (2).toInt (&ok, 16); + + if (ok) + return; + } + + // Check scientific notation, e.g. 7.99361e-15 + if (scient.exactMatch (tokens[i])) + return; + + throw QString (format ("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; + v.apply ([&] (Axis ax, double& a) { a = s[n + ax].toDouble(); }); + return v; +} + +static int32 StringToNumber (QString a, bool* ok = null) +{ + int base = 10; + + if (a.startsWith ("0x")) + { + a.remove (0, 2); + base = 16; + } + + return a.toLong (ok, base); +} + +// ============================================================================= +// 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. +// ============================================================================= +LDObjectPtr ParseLine (QString line) +{ + try + { + QStringList tokens = line.split (" ", QString::SkipEmptyParts); + + if (tokens.size() <= 0) + { + // Line was empty, or only consisted of whitespace + return LDSpawn<LDEmpty>(); + } + + if (tokens[0].length() != 1 or not tokens[0][0].isDigit()) + throw QString ("Illogical line code"); + + int num = tokens[0][0].digitValue(); + + switch (num) + { + case 0: + { + // Comment + QString commentText (line.mid (line.indexOf ("0") + 2)); + QString commentTextSimplified (commentText.simplified()); + + // Handle BFC statements + if (tokens.size() > 2 and tokens[1] == "BFC") + { + for_enum (BFCStatement, i) + { + if (commentTextSimplified == format ("BFC %1", + LDBFC::StatementStrings[int (i)])) + { + return LDSpawn<LDBFC> (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. + if (commentTextSimplified == "BFC CERTIFY INVERTNEXT") + return LDSpawn<LDBFC> (BFCStatement::InvertNext); + elif (commentTextSimplified == "BFC CERTIFY CLIP") + return LDSpawn<LDBFC> (BFCStatement::Clip); + elif (commentTextSimplified == "BFC CERTIFY NOCLIP") + return LDSpawn<LDBFC> (BFCStatement::NoClip); + } + + if (tokens.size() > 2 and tokens[1] == "!LDFORGE") + { + // Handle LDForge-specific types, they're embedded into comments too + if (tokens[2] == "OVERLAY") + { + CheckTokenCount (tokens, 9); + CheckTokenNumbers (tokens, 5, 8); + + LDOverlayPtr obj = LDSpawn<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: + LDCommentPtr obj = LDSpawn<LDComment>(); + obj->setText (commentText); + return obj; + } + + case 1: + { + // Subfile + CheckTokenCount (tokens, 15); + CheckTokenNumbers (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; + LDDocumentPtr 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 (not load) + { + LDErrorPtr obj = LDSpawn<LDError> (line, format ("Could not open %1", tokens[14])); + obj->setFileReferenced (tokens[14]); + return obj; + } + + LDSubfilePtr obj = LDSpawn<LDSubfile>(); + obj->setColor (LDColor::fromIndex (StringToNumber (tokens[1]))); + 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 (tokens, 8); + CheckTokenNumbers (tokens, 1, 7); + + // Line + LDLinePtr obj (LDSpawn<LDLine>()); + obj->setColor (LDColor::fromIndex (StringToNumber (tokens[1]))); + + for (int i = 0; i < 2; ++i) + obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 7 + + return obj; + } + + case 3: + { + CheckTokenCount (tokens, 11); + CheckTokenNumbers (tokens, 1, 10); + + // Triangle + LDTrianglePtr obj (LDSpawn<LDTriangle>()); + obj->setColor (LDColor::fromIndex (StringToNumber (tokens[1]))); + + for (int i = 0; i < 3; ++i) + obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 10 + + return obj; + } + + case 4: + case 5: + { + CheckTokenCount (tokens, 14); + CheckTokenNumbers (tokens, 1, 13); + + // Quadrilateral / Conditional line + LDObjectPtr obj; + + if (num == 4) + obj = LDSpawn<LDQuad>(); + else + obj = LDSpawn<LDCondLine>(); + + obj->setColor (LDColor::fromIndex (StringToNumber (tokens[1]))); + + for (int i = 0; i < 4; ++i) + obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 13 + + return obj; + } + + default: + throw QString ("Unknown line code number"); + } + } + catch (QString& e) + { + // Strange line we couldn't parse + return LDSpawn<LDError> (line, e); + } +} + +// ============================================================================= +// +LDDocumentPtr GetDocument (QString filename) +{ + // Try find the file in the list of loaded files + LDDocumentPtr doc = FindDocument (filename); + + // If it's not loaded, try open it + if (not doc) + doc = OpenDocument (filename, true, true); + + return doc; +} + +// ============================================================================= +// +void LDDocument::reloadAllSubfiles() +{ + print ("Reloading subfiles of %1", getDisplayName()); + + // Go through all objects in the current file and reload the subfiles + for (LDObjectPtr obj : objects()) + { + if (obj->type() == OBJ_Subfile) + { + LDSubfilePtr ref = obj.staticCast<LDSubfile>(); + LDDocumentPtr fileInfo = GetDocument (ref->fileInfo()->name()); + + if (fileInfo != null) + { + ref->setFileInfo (fileInfo); + } + else + { + ref->replace (LDSpawn<LDError> (ref->asText(), + format ("Could not open %1", ref->fileInfo()->name()))); + } + } + + // Reparse gibberish files. It could be that they are invalid because + // of loading errors. Circumstances may be different now. + if (obj->type() == OBJ_Error) + obj->replace (ParseLine (obj.staticCast<LDError>()->contents())); + } + + m_needsReCache = true; + + if (self() == CurrentDocument()) + g_win->buildObjList(); +} + +// ============================================================================= +// +int LDDocument::addObject (LDObjectPtr obj) +{ + history()->add (new AddHistory (objects().size(), obj)); + m_objects << obj; + addKnownVertices (obj); + obj->setDocument (self()); + g_win->R()->compileObject (obj); + return getObjectCount() - 1; +} + +// ============================================================================= +// +void LDDocument::addObjects (const LDObjectList& objs) +{ + for (LDObjectPtr obj : objs) + { + if (obj != null) + addObject (obj); + } +} + +// ============================================================================= +// +void LDDocument::insertObj (int pos, LDObjectPtr obj) +{ + history()->add (new AddHistory (pos, obj)); + m_objects.insert (pos, obj); + obj->setDocument (self()); + g_win->R()->compileObject (obj); + + +#ifdef DEBUG + if (not isImplicit()) + dprint ("Inserted object #%1 (%2) at %3\n", obj->id(), obj->typeName(), pos); +#endif +} + +// ============================================================================= +// +void LDDocument::addKnownVertices (LDObjectPtr obj) +{ + auto it = m_objectVertices.find (obj); + + if (it == m_objectVertices.end()) + it = m_objectVertices.insert (obj, QVector<Vertex>()); + else + it->clear(); + + obj->getVertices (*it); + needVertexMerge(); +} + +// ============================================================================= +// +void LDDocument::forgetObject (LDObjectPtr obj) +{ + int idx = obj->lineNumber(); + obj->deselect(); + assert (m_objects[idx] == obj); + + if (not isImplicit() and not (flags() & DOCF_IsBeingDestroyed)) + { + history()->add (new DelHistory (idx, obj)); + m_objectVertices.remove (obj); + } + + m_objects.removeAt (idx); + obj->setDocument (LDDocumentPtr()); +} + +// ============================================================================= +// +bool IsSafeToCloseAll() +{ + for (LDDocumentPtr f : LDDocument::explicitDocuments()) + { + if (not f->isSafeToClose()) + return false; + } + + return true; +} + +// ============================================================================= +// +void LDDocument::setObject (int idx, LDObjectPtr obj) +{ + assert (idx >= 0 and idx < m_objects.size()); + + // Mark this change to history + if (not m_history->isIgnoring()) + { + QString oldcode = getObject (idx)->asText(); + QString newcode = obj->asText(); + *m_history << new EditHistory (idx, oldcode, newcode); + } + + m_objectVertices.remove (m_objects[idx]); + m_objects[idx]->deselect(); + m_objects[idx]->setDocument (LDDocumentPtr()); + obj->setDocument (self()); + addKnownVertices (obj); + g_win->R()->compileObject (obj); + m_objects[idx] = obj; + needVertexMerge(); +} + +// ============================================================================= +// +LDObjectPtr LDDocument::getObject (int pos) const +{ + if (m_objects.size() <= pos) + return LDObjectPtr(); + + return m_objects[pos]; +} + +// ============================================================================= +// +int LDDocument::getObjectCount() const +{ + return objects().size(); +} + +// ============================================================================= +// +bool LDDocument::hasUnsavedChanges() const +{ + return not isImplicit() and history()->position() != savePosition(); +} + +// ============================================================================= +// +QString LDDocument::getDisplayName() +{ + if (not name().isEmpty()) + return name(); + + if (not defaultName().isEmpty()) + return "[" + defaultName() + "]"; + + return QObject::tr ("untitled"); +} + +// ============================================================================= +// +void LDDocument::initializeCachedData() +{ + if (m_needsReCache) + { + m_vertices.clear(); + + for (LDObjectPtr obj : inlineContents (true, true)) + { + if (obj->type() == OBJ_Subfile) + { + print ("Warning: unable to inline %1 into %2", + obj.staticCast<LDSubfile>()->fileInfo()->getDisplayName(), + getDisplayName()); + continue; + } + + LDPolygon* data = obj->getPolygon(); + + if (data != null) + { + m_polygonData << *data; + delete data; + } + } + + m_needsReCache = false; + } + + if (m_verticesOutdated) + { + m_objectVertices.clear(); + + for (LDObjectPtr obj : inlineContents (true, false)) + addKnownVertices (obj); + + mergeVertices(); + m_verticesOutdated = false; + } + + if (m_needVertexMerge) + mergeVertices(); +} + +// ============================================================================= +// +void LDDocument::mergeVertices() +{ + m_vertices.clear(); + + for (QVector<Vertex> const& verts : m_objectVertices) + m_vertices << verts; + + RemoveDuplicates (m_vertices); + m_needVertexMerge = false; +} + +// ============================================================================= +// +QList<LDPolygon> LDDocument::inlinePolygons() +{ + initializeCachedData(); + return polygonData(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +LDObjectList LDDocument::inlineContents (bool deep, bool renderinline) +{ + // Possibly substitute with logoed studs: + // stud.dat -> stud-logo.dat + // stud2.dat -> stud-logo2.dat + if (cfg::UseLogoStuds and renderinline) + { + // Ensure logoed studs are loaded first + LoadLogoStuds(); + + if (name() == "stud.dat" and g_logoedStud != null) + return g_logoedStud->inlineContents (deep, renderinline); + elif (name() == "stud2.dat" and g_logoedStud2 != null) + return g_logoedStud2->inlineContents (deep, renderinline); + } + + LDObjectList objs, objcache; + + for (LDObjectPtr obj : objects()) + { + // Skip those without scemantic meaning + if (not obj->isScemantic()) + continue; + + // Got another sub-file reference, inline it if we're deep-inlining. If not, + // just add it into the objects normally. Yay, recursion! + if (deep == true and obj->type() == OBJ_Subfile) + { + for (LDObjectPtr otherobj : obj.staticCast<LDSubfile>()->inlineContents (deep, renderinline)) + objs << otherobj; + } + else + objs << obj->createCopy(); + } + + return objs; +} + +// ============================================================================= +// +LDDocumentPtr LDDocument::current() +{ + return g_currentDocument; +} + +// ============================================================================= +// 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 (LDDocumentPtr f) +{ + // Implicit files were loaded for caching purposes and must never be set + // current. + if (f != null and f->isImplicit()) + return; + + g_currentDocument = f; + + if (g_win and f) + { + // A ton of stuff needs to be updated + g_win->updateDocumentListItem (f); + g_win->buildObjList(); + g_win->updateTitle(); + g_win->R()->setDocument (f); + g_win->R()->compiler()->needMerge(); + print ("Changed file to %1", f->getDisplayName()); + } +} + +// ============================================================================= +// +int LDDocument::countExplicitFiles() +{ + return g_explicitDocuments.size(); +} + +// ============================================================================= +// This little beauty closes the initial file that was open at first when opening +// a new file over it. +// ============================================================================= +void LDDocument::closeInitialFile() +{ + if (g_explicitDocuments.size() == 2 and + g_explicitDocuments[0]->name().isEmpty() and + not g_explicitDocuments[1]->name().isEmpty() and + not g_explicitDocuments[0]->hasUnsavedChanges()) + { + LDDocumentPtr filetoclose = g_explicitDocuments.first(); + filetoclose->dismiss(); + } +} + +// ============================================================================= +// +void LoadLogoStuds() +{ + if (g_loadingLogoedStuds or (g_logoedStud and g_logoedStud2)) + return; + + g_loadingLogoedStuds = true; + g_logoedStud = OpenDocument ("stud-logo.dat", true, true); + g_logoedStud2 = OpenDocument ("stud2-logo.dat", true, true); + print (QObject::tr ("Logoed studs loaded.\n")); + g_loadingLogoedStuds = false; +} + +// ============================================================================= +// +void LDDocument::addToSelection (LDObjectPtr obj) // [protected] +{ + if (obj->isSelected()) + return; + + assert (obj->document() == self()); + m_sel << obj; + g_win->R()->compileObject (obj); + obj->setSelected (true); +} + +// ============================================================================= +// +void LDDocument::removeFromSelection (LDObjectPtr obj) // [protected] +{ + if (not obj->isSelected()) + return; + + assert (obj->document() == self()); + m_sel.removeOne (obj); + g_win->R()->compileObject (obj); + obj->setSelected (false); +} + +// ============================================================================= +// +void LDDocument::clearSelection() +{ + for (LDObjectPtr obj : m_sel) + removeFromSelection (obj); + + assert (m_sel.isEmpty()); +} + +// ============================================================================= +// +const LDObjectList& LDDocument::getSelection() const +{ + return m_sel; +} + +// ============================================================================= +// +void LDDocument::swapObjects (LDObjectPtr one, LDObjectPtr other) +{ + int a = m_objects.indexOf (one); + int b = m_objects.indexOf (other); + assert (a != b and a != -1 and b != -1); + m_objects[b] = one; + m_objects[a] = other; + addToHistory (new SwapHistory (one->id(), other->id())); +} + +// ============================================================================= +// +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; +} + +// ============================================================================= +// +QVector<Vertex> const& LDDocument::inlineVertices() +{ + initializeCachedData(); + return m_vertices; +} + +void LDDocument::redoVertices() +{ + m_verticesOutdated = true; +} + +void LDDocument::needVertexMerge() +{ + m_needVertexMerge = true; +}