--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/document.cc Fri Dec 13 20:01:49 2013 +0200 @@ -0,0 +1,1180 @@ +/* + * 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/>. + * ===================================================================== + * + * file.cpp: File I/O and management. + * - File loading, parsing, manipulation, saving, closing. + * - LDraw path verification. + */ + +#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 "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 = 5; +static bool g_aborted = false; +static LDDocument* g_logoedStud = null; +static LDDocument* g_logoedStud2 = null; + +LDDocument* LDDocument::m_curdoc = null; + +// ============================================================================= +// ----------------------------------------------------------------------------- +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; + } +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +LDDocument::LDDocument() +{ setImplicit (true); + setSavePosition (-1); + setListItem (null); + setHistory (new History); + m_History->setFile (this); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +LDDocument::~LDDocument() +{ // Clear everything from the model + for (LDObject* obj : getObjects()) + delete obj; + + // Clear the cache as well + for (LDObject* obj : getCache()) + delete obj; + + delete m_History; + + // Remove this file from the list of files + g_loadedFiles.removeOne (this); + + // 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(); + } + + g_win->updateDocumentList(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +LDDocument* findDocument (str name) +{ for (LDDocument * file : g_loadedFiles) + if (!file->getName().isEmpty() && file->getShortName() == 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) +{ log ("Opening %1...\n", relpath); + File* f = new File; + str 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 + + if (getCurrentDocument()) + { // First, try find the file in the current model's file path. We want a file + // in the immediate vicinity of the current model to override stock LDraw stuff. + str partpath = fmt ("%1" DIRSLASH "%2", dirname (getCurrentDocument()->getName()), relpath); + + if (f->open (partpath, File::Read)) + return f; + } + + if (f->open (relpath, File::Read)) + return f; + + // Try with just the LDraw path first + fullPath = fmt ("%1" DIRSLASH "%2", io_ldpath, relpath); + + if (f->open (fullPath, File::Read)) + return f; + + 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 str& topdir : initlist<str> ({ io_ldpath, net_downloadpath })) + { for (const str& subdir : initlist<str> ({ "parts", "p" })) + { fullPath = fmt ("%1" DIRSLASH "%2" DIRSLASH "%3", topdir, subdir, relpath); + + if (f->open (fullPath, File::Read)) + return f; + } + } + } + + // Did not find the file. + log ("Could not find %1.\n", relpath); + delete f; + 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) + delete obj; + + 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) + { str line = getLines()[i]; + + // Trim the trailing newline + QChar c; + + while (!line.isEmpty() && ((c = line[line.length() - 1]) == '\n' || c == '\r')) + line.chop (1); + + LDObject* obj = parseLine (line); + + // Check for parse errors and warn about tthem + if (obj->getType() == LDObject::Error) + { 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 + 1); + } +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void LDFileLoader::abort() +{ setAborted (true); + + if (isOnForeground()) + g_aborted = true; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +QList<LDObject*> loadFileContents (File* f, int* numWarnings, bool* ok) +{ QList<str> lines; + QList<LDObject*> objs; + + if (numWarnings) + *numWarnings = 0; + + // Read in the lines + for (str line : *f) + lines << line; + + 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 (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; + return null; + } + } + + if (!f) + return null; + + LDDocument* load = new LDDocument; + load->setName (path); + + int numWarnings; + bool ok; + QList<LDObject*> objs = loadFileContents (f, &numWarnings, &ok); + + if (!ok) + return null; + + for (LDObject* obj : objs) + load->addObject (obj); + + delete f; + g_loadedFiles << load; + + if (g_loadingMainFile) + { LDDocument::setCurrent (load); + g_win->R()->setFile (load); + log (QObject::tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings); + } + + 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()) + { str message = fmt ("There are unsaved changes to %1. Should it be saved?", + (getName().length() > 0) ? getName() : "<anonymous>"); + + int button = msgbox::question (g_win, "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) + { str newpath = QFileDialog::getSaveFileName (g_win, "Save As", + getCurrentDocument()->getName(), "LDraw files (*.dat *.ldr)"); + + if (newpath.length() == 0) + return false; + + setName (newpath); + } + + if (!save()) + { message = fmt (QObject::tr ("Failed to save %1: %2\nDo you still want to close?"), + getName(), strerror (errno)); + + if (msgbox::critical (g_win, "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 (str path) +{ alias rfiles = io_recentfiles.value; + 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 (str path) +{ g_loadingMainFile = true; + 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); + + // 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 (str savepath) +{ if (!savepath.length()) + savepath = getName(); + + 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; + LDObject* first = getObject (1); + + if (!isImplicit() && first != null && first->getType() == LDObject::Comment) + { fpathComment = static_cast<LDComment*> (first); + + if (fpathComment->text.left (6) == "Name: ") + { str newname; + str dir = basename (dirname (savepath)); + + if (dir == "s" || dir == "48") + newname = dir + "\\"; + + newname += basename (savepath); + fpathComment->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"); + + // File is saved, now clean up. + f.close(); + + // We have successfully saved, update the save position now. + setSavePosition (getHistory()->getPosition()); + setName (savepath); + + g_win->updateDocumentListItem (this); + g_win->updateTitle(); + return true; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +#define CHECK_TOKEN_COUNT(N) \ + if (tokens.size() != N) \ + return new LDError (line, "Bad amount of tokens"); + +#define CHECK_TOKEN_NUMBERS(MIN, MAX) \ + for (int i = MIN; i <= MAX; ++i) \ + if (!numeric (tokens[i])) \ + return new LDError (line, fmt ("Token #%1 was `%2`, expected a number", (i + 1), tokens[i])); + +// ============================================================================= +// ----------------------------------------------------------------------------- +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 (str line) +{ QStringList tokens = line.split (" ", str::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) + return new LDError (line, "Illogical line code"); + + int num = tokens[0][0].digitValue(); + + switch (num) + { case 0: + { // Comment + str 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 + { str 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) + CHECK_TOKEN_COUNT (7) + CHECK_TOKEN_NUMBERS (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") + { CHECK_TOKEN_COUNT (9); + CHECK_TOKEN_NUMBERS (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 + 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; + LDDocument* load = getDocument (tokens[14]); + g_loadingMainFile = tmp; + + // If we cannot open the file, mark it an error + 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: + { CHECK_TOKEN_COUNT (8) + CHECK_TOKEN_NUMBERS (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: + { CHECK_TOKEN_COUNT (11) + CHECK_TOKEN_NUMBERS (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: + { CHECK_TOKEN_COUNT (14) + CHECK_TOKEN_NUMBERS (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 + return new LDError (line, "Unknown line code number"); + } +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +LDDocument* getDocument (str 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::Subfile) + { LDSubfile* ref = static_cast<LDSubfile*> (obj); + LDDocument* fileInfo = getDocument (ref->getFileInfo()->getName()); + + if (fileInfo) + ref->setFileInfo (fileInfo); + else + ref->replace (new LDError (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::Error) + obj->replace (parseLine (static_cast<LDError*> (obj)->contents)); + } + + // Close all files left unused + LDDocument::closeUnused(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +int LDDocument::addObject (LDObject* obj) +{ getHistory()->add (new AddHistory (getObjects().size(), obj)); + m_Objects << obj; + + if (obj->getType() == LDObject::Vertex) + m_Vertices << obj; + + obj->setFile (this); + return getObjectCount() - 1; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void LDDocument::addObjects (const QList<LDObject*> 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); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void LDDocument::forgetObject (LDObject* obj) +{ int idx = obj->getIndex(); + 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 < m_Objects.size()); + + // Mark this change to history + str oldcode = getObject (idx)->raw(); + str newcode = obj->raw(); + *m_History << new EditHistory (idx, oldcode, newcode); + + obj->setFile (this); + m_Objects[idx] = obj; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +static QList<LDDocument*> getFilesUsed (LDDocument* node) +{ QList<LDDocument*> filesUsed; + + for (LDObject* obj : node->getObjects()) + { if (obj->getType() != LDObject::Subfile) + continue; + + LDSubfile* ref = static_cast<LDSubfile*> (obj); + filesUsed << ref->getFileInfo(); + filesUsed << getFilesUsed (ref->getFileInfo()); + } + + return filesUsed; +} + +// ============================================================================= +// Find out which files are unused and close them. +// ----------------------------------------------------------------------------- +void LDDocument::closeUnused() +{ QList<LDDocument*> filesUsed = getFilesUsed (getCurrentDocument()); + + // Anything that's explicitly opened must not be closed + for (LDDocument* file : g_loadedFiles) + if (!file->isImplicit()) + filesUsed << file; + + // Remove duplicated entries + removeDuplicates (filesUsed); + + // Close all open files that aren't in filesUsed + for (LDDocument* file : g_loadedFiles) + { bool isused = false; + + for (LDDocument* usedFile : filesUsed) + { if (file == usedFile) + { isused = true; + break; + } + } + + if (!isused) + delete file; + } + + g_loadedFiles.clear(); + g_loadedFiles << filesUsed; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +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(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +str LDDocument::getShortName() +{ if (!getName().isEmpty()) + return basename (getName()); + + if (!getDefaultName().isEmpty()) + return getDefaultName(); + + return tr ("<anonymous>"); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +QList<LDObject*> 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)) + { if (getName() == "stud.dat" && g_logoedStud) + return g_logoedStud->inlineContents (flags); + + elif (getName() == "stud2.dat" && g_logoedStud2) + return g_logoedStud2->inlineContents (flags); + } + + QList<LDObject*> objs, objcache; + + bool deep = flags & LDSubfile::DeepInline, + doCache = flags & LDSubfile::CacheInline; + + // If we have this cached, just clone that + if (deep && getCache().size()) + { for (LDObject* obj : getCache()) + objs << obj->clone(); + } + 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::Subfile) + { LDSubfile* ref = static_cast<LDSubfile*> (obj); + + // We only want to cache immediate subfiles, so shed the caching + // flag when recursing deeper in hierarchy. + QList<LDObject*> otherobjs = ref->inlineContents (flags & ~ (LDSubfile::CacheInline)); + + for (LDObject * otherobj : otherobjs) + { // Cache this object, if desired + if (doCache) + objcache << otherobj->clone(); + + objs << otherobj; + } + } + else + { if (doCache) + objcache << obj->clone(); + + objs << obj->clone(); + } + } + + 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. ;) +// +// FIXME: 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()->resetAllAngles(); + g_win->R()->repaint(); + + log ("Changed file to %1", f->getShortName()); + } +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +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() == "" && + !g_loadedFiles[0]->hasUnsavedChanges() + ) + delete g_loadedFiles[0]; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void loadLogoedStuds() +{ log ("Loading logoed studs...\n"); + + delete g_logoedStud; + delete g_logoedStud2; + + g_logoedStud = openDocument ("stud-logo.dat", true); + g_logoedStud2 = openDocument ("stud2-logo.dat", true); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +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 QList<LDObject*>& LDDocument::getSelection() const +{ return m_sel; +} \ No newline at end of file