Begin rework to add support for multiple libraries

Fri, 23 Mar 2018 12:51:18 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Fri, 23 Mar 2018 12:51:18 +0200
changeset 1308
dcc8c02530c2
parent 1307
adb9d32a1426
child 1309
c72e7e09bda8

Begin rework to add support for multiple libraries

CMakeLists.txt file | annotate | diff | comparison | revisions
src/basics.cpp file | annotate | diff | comparison | revisions
src/basics.h file | annotate | diff | comparison | revisions
src/colors.cpp file | annotate | diff | comparison | revisions
src/colors.h file | annotate | diff | comparison | revisions
src/configurationoptions.txt file | annotate | diff | comparison | revisions
src/dialogs/configdialog.cpp file | annotate | diff | comparison | revisions
src/dialogs/configdialog.h file | annotate | diff | comparison | revisions
src/dialogs/configdialog.ui file | annotate | diff | comparison | revisions
src/documentmanager.cpp file | annotate | diff | comparison | revisions
src/documentmanager.h file | annotate | diff | comparison | revisions
src/lddocument.cpp file | annotate | diff | comparison | revisions
src/lddocument.h file | annotate | diff | comparison | revisions
src/ldpaths.cpp file | annotate | diff | comparison | revisions
src/ldpaths.h file | annotate | diff | comparison | revisions
src/librariesmodel.cpp file | annotate | diff | comparison | revisions
src/librariesmodel.h file | annotate | diff | comparison | revisions
src/main.cpp file | annotate | diff | comparison | revisions
src/toolsets/filetoolset.cpp file | annotate | diff | comparison | revisions
src/toolsets/filetoolset.h file | annotate | diff | comparison | revisions
tools/configcollector.py file | annotate | diff | comparison | revisions
--- a/CMakeLists.txt	Tue Mar 20 12:25:52 2018 +0200
+++ b/CMakeLists.txt	Fri Mar 23 12:51:18 2018 +0200
@@ -43,6 +43,7 @@
 	src/hierarchyelement.cpp
 	src/lddocument.cpp
 	src/ldpaths.cpp
+	src/librariesmodel.cpp
 	src/main.cpp
 	src/mainwindow.cpp
 	src/mathfunctions.cpp
@@ -114,6 +115,7 @@
 	src/lddocument.h
 	src/ldobjectiterator.h
 	src/ldpaths.h
+	src/librariesmodel.h
 	src/macros.h
 	src/main.h
 	src/mainwindow.h
--- a/src/basics.cpp	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/basics.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -378,3 +378,13 @@
 	one = one ^ other;
 	return one;
 }
+
+QDataStream& operator<<(QDataStream& out, const Library& library)
+{
+	return out << library.path << library.role;
+}
+
+QDataStream& operator>>(QDataStream &in, Library& library)
+{
+	return in >> library.path >> enum_cast<>(library.role);
+}
--- a/src/basics.h	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/basics.h	Fri Mar 23 12:51:18 2018 +0200
@@ -27,6 +27,7 @@
 #include <QFile>
 #include <QMatrix4x4>
 #include <functional>
+#include <type_traits>
 #include <math.h>
 #include "macros.h"
 #include "transform.h"
@@ -265,6 +266,15 @@
 };
 
 /*
+ * Casts a reference to an enum into a reference to its underlying integer type.
+ */
+template<typename T>
+typename std::underlying_type<T>::type& enum_cast(T& enu)
+{
+	return *reinterpret_cast<typename std::underlying_type<T>::type*>(&enu);
+}
+
+/*
  * Convenience function for RingAdapter so that the template parameter does not have to be provided. The ring amount is assumed
  * to be the amount of elements in the collection.
  */
@@ -283,6 +293,28 @@
 	return RingAdapter<T> {collection, count};
 }
 
+struct Library
+{
+	QString path;
+	enum
+	{
+		ReadOnlyStorage, // for official files, etc
+		UnofficialFiles, // put downloaded files here
+		WorkingDirectory, // for editable documents
+	} role = ReadOnlyStorage;
+
+	bool operator==(const Library& other) const
+	{
+		return (this->path == other.path) and (this->role == other.role);
+	}
+};
+
+Q_DECLARE_METATYPE(Library)
+using Libraries = QVector<Library>;
+
+QDataStream& operator<<(QDataStream& out, const Library& library);
+QDataStream& operator>>(QDataStream& in, Library& library);
+
 //
 // Get the amount of elements in something.
 //
--- a/src/colors.cpp	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/colors.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -16,6 +16,7 @@
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <QDir>
 #include <QMessageBox>
 #include "colors.h"
 #include "ldpaths.h"
@@ -29,6 +30,7 @@
 void LDColor::initColors()
 {
 	static ColorData colors;
+	colors.loadFromLdconfig();
 	LDColor::colorData = &colors;
 }
 
@@ -216,9 +218,6 @@
 	m_data[EdgeColor].faceColor = Qt::black;
 	m_data[EdgeColor].edgeColor = Qt::black;
 	m_data[EdgeColor].name = "Edge color";
-
-	// Load the rest from LDConfig.ldr.
-	loadFromLdconfig();
 }
 
 /*
@@ -247,19 +246,41 @@
  */
 void ColorData::loadFromLdconfig()
 {
-	QString path = LDPaths::ldConfigPath();
-	QFile file {path};
+	*this = {};
 
-	if (not file.open(QIODevice::ReadOnly))
+	for (const Library& library : ::config->libraries())
 	{
-		QMessageBox::critical(nullptr, "Error", "Unable to open LDConfig.ldr for parsing: " + file.errorString());
-		return;
-	}
+		QDir dir {library.path};
+
+		if (dir.exists("LDConfig.ldr"))
+		{
+			QFile file {dir.filePath("LDConfig.ldr")};
 
+			if (file.open(QIODevice::ReadOnly))
+			{
+				this->loadFromFile(file);
+			}
+			else
+			{
+				QMessageBox::critical(
+					nullptr,
+					QObject::tr("Error"),
+					format(
+						QObject::tr("Unable to open LDConfig.ldr for parsing: %1"),
+						file.errorString()
+					)
+				);
+			}
+		}
+	}
+}
+
+void ColorData::loadFromFile(QIODevice& device)
+{
 	// TODO: maybe LDConfig can be loaded as a Document? Or would that be overkill?
-	while (not file.atEnd())
+	while (not device.atEnd())
 	{
-		QString line = QString::fromUtf8(file.readLine());
+		QString line = QString::fromUtf8(device.readLine());
 
 		if (line.isEmpty() or line[0] != '0')
 			continue; // empty or illogical
--- a/src/colors.h	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/colors.h	Fri Mar 23 12:51:18 2018 +0200
@@ -37,6 +37,7 @@
 
 	ColorData();
 	void loadFromLdconfig();
+	void loadFromFile(QIODevice& device);
 	bool contains(int code) const;
 	const Entry& get(int code) const;
 
--- a/src/configurationoptions.txt	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/configurationoptions.txt	Fri Mar 23 12:51:18 2018 +0200
@@ -71,10 +71,10 @@
 option DrawAngles = false
 
 # File management options
+option Libraries = QVector<Library> {}
 option DownloadFilePath = ""
 option GuessDownloadPaths = true
 option AutoCloseDownloadDialog = true
-option LDrawPath = ""
 
 # Other options
 option FirstStart = true
--- a/src/dialogs/configdialog.cpp	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/dialogs/configdialog.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -33,6 +33,7 @@
 #include <QPushButton>
 #include "../main.h"
 #include "../lddocument.h"
+#include "../librariesmodel.h"
 #include "../miscallenous.h"
 #include "../canvas.h"
 #include "../guiutilities.h"
@@ -74,9 +75,12 @@
 	QDialog (parent, f),
 	HierarchyElement (parent),
 	ui (*new Ui_ConfigDialog),
-    m_settings (MainWindow::makeSettings (this))
+	m_settings (MainWindow::makeSettings (this)),
+	libraries {::config->libraries()},
+	librariesModel {new LibrariesModel {this->libraries, this}}
 {
 	ui.setupUi (this);
+	ui.librariesView->setModel(this->librariesModel);
 
 	// Set defaults
 	applyToWidgetOptions([&](QWidget* widget, QString confname)
@@ -146,10 +150,52 @@
 		this, SLOT (buttonClicked (QAbstractButton*)));
 	connect (ui.m_pages, SIGNAL (currentChanged (int)), this, SLOT (selectPage (int)));
 	connect (ui.m_pagelist, SIGNAL (currentRowChanged (int)), this, SLOT (selectPage (int)));
+	connect(
+		this->ui.addLibrary,
+		&QPushButton::clicked,
+		[&]()
+		{
+			this->librariesModel->insertRow(this->librariesModel->rowCount());
+		}
+	);
+	connect(
+		this->ui.removeLibrary,
+		&QPushButton::clicked,
+		[&]()
+		{
+			QModelIndex index = this->ui.librariesView->currentIndex();
+
+			if (index.isValid())
+				this->librariesModel->removeRow(index.row());
+		}
+	);
+	connect(
+		this->ui.moveLibraryUp,
+		&QPushButton::clicked,
+		[&]()
+		{
+			QModelIndex index = this->ui.librariesView->currentIndex();
+
+			if (index.isValid())
+				this->librariesModel->moveRows({}, index.row(), 1, {}, index.row() - 1);
+		}
+	);
+	connect(
+		this->ui.moveLibraryDown,
+		&QPushButton::clicked,
+		[&]()
+		{
+			QModelIndex index = this->ui.librariesView->currentIndex();
+
+			if (index.isValid())
+				this->librariesModel->moveRows({}, index.row(), 1, {}, index.row() + 2);
+		}
+	);
 }
 
 ConfigDialog::~ConfigDialog()
 {
+	delete this->librariesModel;
 	delete &ui;
 }
 
@@ -277,6 +323,7 @@
 	// Rebuild the quick color toolbar
 	m_window->setQuickColors (quickColors);
 	m_config->setQuickColorToolbar (quickColorString());
+	::config->setLibraries(this->libraries);
 
 	// Ext program settings
 	for (int i = 0; i < NumExternalPrograms; ++i)
--- a/src/dialogs/configdialog.h	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/dialogs/configdialog.h	Fri Mar 23 12:51:18 2018 +0200
@@ -74,6 +74,8 @@
 	ExternalProgramWidgets m_externalProgramWidgets[NumExternalPrograms];
 	class QSettings* m_settings;
 	QVector<ColorToolbarItem> quickColors;
+	class LibrariesModel* librariesModel;
+	Libraries libraries;
 
 	void applySettings();
 	void addShortcut (QAction* act);
--- a/src/dialogs/configdialog.ui	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/dialogs/configdialog.ui	Fri Mar 23 12:51:18 2018 +0200
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>792</width>
-    <height>408</height>
+    <width>808</width>
+    <height>499</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -41,6 +41,11 @@
        </item>
        <item>
         <property name="text">
+         <string>Libraries</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
          <string>Editing tools</string>
         </property>
        </item>
@@ -351,6 +356,71 @@
          </item>
         </layout>
        </widget>
+       <widget class="QWidget" name="page_2">
+        <layout class="QVBoxLayout" name="verticalLayout_11">
+         <item>
+          <widget class="QGroupBox" name="groupBox_8">
+           <property name="title">
+            <string>Libraries</string>
+           </property>
+           <layout class="QHBoxLayout" name="horizontalLayout_3">
+            <item>
+             <widget class="QTableView" name="librariesView">
+              <attribute name="horizontalHeaderStretchLastSection">
+               <bool>true</bool>
+              </attribute>
+             </widget>
+            </item>
+            <item>
+             <layout class="QVBoxLayout" name="verticalLayout_12">
+              <item>
+               <widget class="QPushButton" name="addLibrary">
+                <property name="text">
+                 <string>Add</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QPushButton" name="moveLibraryUp">
+                <property name="text">
+                 <string>Move up</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QPushButton" name="moveLibraryDown">
+                <property name="text">
+                 <string>Move down</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QPushButton" name="removeLibrary">
+                <property name="text">
+                 <string>Remove</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <spacer name="verticalSpacer_8">
+                <property name="orientation">
+                 <enum>Qt::Vertical</enum>
+                </property>
+                <property name="sizeHint" stdset="0">
+                 <size>
+                  <width>20</width>
+                  <height>40</height>
+                 </size>
+                </property>
+               </spacer>
+              </item>
+             </layout>
+            </item>
+           </layout>
+          </widget>
+         </item>
+        </layout>
+       </widget>
        <widget class="QWidget" name="page">
         <layout class="QVBoxLayout" name="verticalLayout_6">
          <item>
@@ -1102,6 +1172,7 @@
  </widget>
  <resources>
   <include location="../../ldforge.qrc"/>
+  <include location="../../ldforge.qrc"/>
  </resources>
  <connections/>
 </ui>
--- a/src/documentmanager.cpp	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/documentmanager.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -17,6 +17,7 @@
  */
 
 #include <QApplication>
+#include <QDir>
 #include <QFileInfo>
 #include <QMessageBox>
 #include "documentmanager.h"
@@ -197,85 +198,22 @@
 	return path;
 }
 
-QString DocumentManager::findDocumentPath (QString relativePath, bool subdirs)
+QString DocumentManager::findDocument(QString name) const
 {
-	// LDraw models use backslashes as path separators. Replace those into forward slashes for Qt.
-	relativePath.replace ("\\", "/");
-
-	// Try find it relative to other currently open documents. We want a file in the immediate vicinity of a current
-	// part model to override stock LDraw stuff.
-	QString relativeTopDir = Basename (Dirname (relativePath));
-
-	for (LDDocument* document : m_documents)
-	{
-		QString partpath = format ("%1/%2", Dirname (document->fullPath()), relativePath);
-		QFileInfo fileinfo (partpath);
-
-		if (fileinfo.exists())
-		{
-			// Ensure we don't mix subfiles and 48-primitives with non-subfiles and non-48
-			QString partTopDir = Basename (Dirname (partpath));
-
-			for (QString subdir : specialSubdirectories)
-			{
-				if ((partTopDir == subdir) != (relativeTopDir == subdir))
-					goto skipthis;
-			}
+	name = name.replace("\\", "/");
 
-			return partpath;
-		}
-skipthis:
-		continue;
-	}
-
-	if (QFileInfo::exists (relativePath))
-		return relativePath;
-
-	// Try with just the LDraw path first
-	QString fullPath = format ("%1" DIRSLASH "%2", m_config->lDrawPath(), relativePath);
-
-	if (QFileInfo::exists (fullPath))
-		return fullPath;
+	for (const Library& library : ::config->libraries())
+	{
+		for (const QString& subdirectory : {"parts", "p"})
+		{
+			QDir dir {library.path + "/" + subdirectory};
 
-	if (subdirs)
-	{
-		// Look in sub-directories: parts and p. Also look in the download path, since that's where we download parts
-		// from the PT to.
-		QStringList dirs = { m_config->lDrawPath(), m_config->downloadFilePath() };
-		for (const QString& topdir : dirs)
-		{
-			for (const QString& subdir : QStringList ({ "parts", "p" }))
-			{
-				fullPath = format ("%1" DIRSLASH "%2" DIRSLASH "%3", topdir, subdir, relativePath);
-
-				if (QFile::exists (fullPath))
-					return fullPath;
-			}
+			if (dir.exists(name))
+				return QDir::cleanPath(dir.filePath(name));
 		}
 	}
 
-	// Did not find the file.
-	return "";
-}
-
-QFile* DocumentManager::openLDrawFile (QString relpath, bool subdirs, QString* pathpointer)
-{
-	print ("Opening %1...\n", relpath);
-	QString path = findDocumentPath (relpath, subdirs);
-
-	if (pathpointer)
-		*pathpointer = path;
-
-	if (path.isEmpty())
-		return nullptr;
-
-	QFile* fp = new QFile (path);
-
-	if (fp->open (QIODevice::ReadOnly))
-		return fp;
-
-	fp->deleteLater();
-	return nullptr;
+	return {};
 }
 
 void DocumentManager::printParseErrorMessage(QString message)
@@ -283,65 +221,63 @@
 	print(message);
 }
 
-LDDocument* DocumentManager::openDocument (QString path, bool search, bool implicit, LDDocument* 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;
+LDDocument* DocumentManager::openDocument(
+	QString path,
+	bool search,
+	bool implicit,
+	LDDocument* fileToOverride
+) {
+	if (search and not QFileInfo {path}.exists())
+	{
+		// 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.
+		path = this->findDocument(path.toLower());
+	}
+
+	QFile file {path};
+
+	if (file.open(QIODevice::ReadOnly))
+	{
+		LDDocument* load = fileToOverride;
+
+		if (fileToOverride == nullptr)
+			load = m_window->newDocument(implicit);
+
+		load->setFullPath(path);
+		load->setName(LDDocument::shortenName(path));
 
-	if (search)
-	{
-		fp = openLDrawFile (path.toLower(), true, &fullpath);
+		// Loading the file shouldn't count as actual edits to the document.
+		load->history()->setIgnoring (true);
+
+		Parser parser {file};
+		Winding winding = NoWinding;
+		load->header = parser.parseHeader(winding);
+		load->setWinding(winding);
+		parser.parseBody(*load);
+		file.close();
+
+		if (m_loadingMainFile)
+		{
+			int numWarnings = 0;
+
+			for (LDObject* object : load->objects())
+			{
+				if (object->type() == LDObjectType::Error)
+					numWarnings += 1;
+			}
+
+			m_window->changeDocument(load);
+			print(tr("File %1 opened successfully (%2 errors)."), load->name(), numWarnings);
+		}
+
+		load->history()->setIgnoring (false);
+		return load;
 	}
 	else
 	{
-		fp = new QFile (path);
-		fullpath = path;
-
-		if (not fp->open (QIODevice::ReadOnly))
-		{
-			delete fp;
-			return nullptr;
-		}
+		return nullptr;
 	}
-
-	if (not fp)
-		return nullptr;
-
-	LDDocument* load = (fileToOverride ? fileToOverride : m_window->newDocument (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;
-	Parser parser {*fp};
-	Winding winding = NoWinding;
-	load->header = parser.parseHeader(winding);
-	load->setWinding(winding);
-	parser.parseBody(*load);
-	fp->close();
-	fp->deleteLater();
-
-	if (m_loadingMainFile)
-	{
-		int numWarnings = 0;
-
-		for (LDObject* object : load->objects())
-		{
-			if (object->type() == LDObjectType::Error)
-				numWarnings += 1;
-		}
-
-		m_window->changeDocument (load);
-		print (tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings);
-	}
-
-	load->history()->setIgnoring (false);
-	return load;
 }
 
 void DocumentManager::addRecentFile (QString path)
--- a/src/documentmanager.h	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/documentmanager.h	Fri Mar 23 12:51:18 2018 +0200
@@ -37,13 +37,17 @@
 	const Documents& allDocuments() const { return m_documents; }
 	void clear();
 	LDDocument* createNew();
+	QString findDocument(QString name) const;
 	LDDocument* findDocumentByName (QString name);
-	QString findDocumentPath (QString relpath, bool subdirs);
 	LDDocument* getDocumentByName (QString filename);
 	bool isSafeToCloseAll();
 	void loadLogoedStuds();
-	LDDocument* openDocument (QString path, bool search, bool implicit, LDDocument* fileToOverride = nullptr);
-	QFile* openLDrawFile (QString relpath, bool subdirs, QString* pathpointer);
+	LDDocument* openDocument(
+		QString path,
+		bool search,
+		bool implicit,
+		LDDocument* fileToOverride = nullptr
+	);
 	void openMainModel (QString path);
 	bool preInline (LDDocument* doc, Model& model, bool deep, bool renderinline);
 
--- a/src/lddocument.cpp	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/lddocument.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -67,12 +67,12 @@
 
 QString LDDocument::name() const
 {
-	return m_name;
+	return this->header.name;
 }
 
 void LDDocument::setName (QString value)
 {
-	m_name = value;
+	this->header.name = value;
 }
 
 EditHistory* LDDocument::history() const
--- a/src/lddocument.h	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/lddocument.h	Fri Mar 23 12:51:18 2018 +0200
@@ -130,7 +130,6 @@
 	LDObject* withdrawAt(int position);
 
 private:
-	QString m_name;
 	QString m_fullPath;
 	QString m_defaultName;
 	EditHistory* m_history;
--- a/src/ldpaths.cpp	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/ldpaths.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -26,7 +26,7 @@
 	m_config(config),
 	m_dialog(nullptr) {}
 
-
+/*
 void LDPaths::checkPaths()
 {
 	QString pathconfig = m_config->lDrawPath();
@@ -42,7 +42,7 @@
 			m_config->setLDrawPath(m_dialog->path());
 	}
 }
-
+*/
 
 bool LDPaths::isValid (const QDir& dir) const
 {
--- a/src/ldpaths.h	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/ldpaths.h	Fri Mar 23 12:51:18 2018 +0200
@@ -28,7 +28,7 @@
 
 public:
 	LDPaths(Configuration* config, QObject* parent = nullptr);
-	void checkPaths();
+	// void checkPaths();
 	bool isValid (const class QDir& path) const;
 
 	static QDir& baseDir();
@@ -43,4 +43,4 @@
 	Configuration* m_config;
 	mutable QString m_error;
 	class LDrawPathDialog* m_dialog;
-};
\ No newline at end of file
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/librariesmodel.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -0,0 +1,156 @@
+#include "librariesmodel.h"
+#include "generics/migrate.h"
+
+LibrariesModel::LibrariesModel(Libraries& libraries, QObject* parent) :
+	QAbstractTableModel {parent},
+	libraries {libraries} {}
+
+QString libraryRoleName(decltype(Library::role) role)
+{
+	switch (role)
+	{
+	case Library::ReadOnlyStorage:
+		return QObject::tr("Storage");
+
+	case Library::UnofficialFiles:
+		return QObject::tr("Unofficial files");
+
+	case Library::WorkingDirectory:
+		return QObject::tr("Working directory");
+
+	default:
+		return "";
+	}
+}
+
+int LibrariesModel::rowCount(const QModelIndex&) const
+{
+	return this->libraries.size();
+}
+
+int LibrariesModel::columnCount(const QModelIndex&) const
+{
+	return 2;
+}
+
+QVariant LibrariesModel::data(const QModelIndex& index, int role) const
+{
+	Column column = static_cast<Column>(index.column());
+
+	if (index.row() >= 0 and index.row() < this->rowCount())
+	{
+		const Library& library = this->libraries[index.row()];
+
+		switch (column)
+		{
+		case PathColumn:
+			switch (role)
+			{
+			case Qt::DisplayRole:
+			case Qt::EditRole:
+				return library.path;
+			}
+			break;
+
+		case RoleColumn:
+			switch (role)
+			{
+			case Qt::DisplayRole:
+				return libraryRoleName(library.role);
+
+			case Qt::EditRole:
+				return library.role;
+			}
+			break;
+		}
+	}
+
+	return {};
+}
+
+bool LibrariesModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+	if (index.row() >= 0 and index.row() < this->rowCount({}) and role == Qt::EditRole)
+	{
+		Library& library = this->libraries[index.row()];
+		Column column = static_cast<Column>(index.column());
+
+		switch (column)
+		{
+		case PathColumn:
+			library.path = value.toString();
+			return true;
+
+		case RoleColumn:
+			{
+				int intValue = value.toInt();
+
+				if (intValue >= 0 and intValue < 3)
+				{
+					library.role = static_cast<decltype(Library::role)>(intValue);
+					return true;
+				}
+			}
+			break;
+		}
+	}
+
+	return false;
+}
+
+Qt::ItemFlags LibrariesModel::flags(const QModelIndex& index) const
+{
+	Qt::ItemFlags flags = QAbstractTableModel::flags(index);
+
+	if (index.isValid())
+		flags |= Qt::ItemIsEditable;
+
+	return flags;
+}
+
+bool LibrariesModel::moveRows(
+	const QModelIndex&,
+	int sourceRow,
+	int count,
+	const QModelIndex&,
+	int destinationRow
+) {
+	int sourceRowLast = sourceRow + count - 1;
+	this->beginMoveRows({}, sourceRow, sourceRowLast, {}, destinationRow);
+	::migrate(this->libraries, sourceRow, sourceRowLast, destinationRow);
+	this->endMoveRows();
+	return true;
+}
+
+bool LibrariesModel::removeRows(int row, int count, const QModelIndex&)
+{
+	if (row >= 0 and row + count - 1 < this->rowCount())
+	{
+		this->beginRemoveRows({}, row, row + count - 1);
+		this->libraries.remove(row, count);
+		this->endRemoveRows();
+		return true;
+	}
+	else
+	{
+		return false;
+	}
+}
+
+bool LibrariesModel::insertRows(int startRow, int count, const QModelIndex&)
+{
+	if (startRow >= 0 and startRow <= this->rowCount())
+	{
+		this->beginInsertRows({}, startRow, startRow + count - 1);
+
+		for (int row : range(startRow, startRow + 1, startRow + count - 1))
+			this->libraries.insert(row, {});
+
+		this->endInsertRows();
+		return true;
+	}
+	else
+	{
+		return false;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/librariesmodel.h	Fri Mar 23 12:51:18 2018 +0200
@@ -0,0 +1,29 @@
+#pragma once
+#include <QAbstractTableModel>
+#include "main.h"
+
+class LibrariesModel : public QAbstractTableModel
+{
+public:
+	enum Column { RoleColumn, PathColumn };
+
+	LibrariesModel(Libraries& libraries, QObject* parent);
+
+	QVariant data(const QModelIndex& index, int role) const override;
+	int rowCount(const QModelIndex& parent = {}) const override;
+	int columnCount(const QModelIndex& parent = {}) const override;
+	bool setData(const QModelIndex& index, const QVariant& value, int role) override;
+	Qt::ItemFlags flags(const QModelIndex& index) const override;
+	bool moveRows(
+		const QModelIndex&,
+		int sourceRow,
+		int count,
+		const QModelIndex&,
+		int destinationRow
+	) override;
+	bool removeRows(int row, int count, const QModelIndex&) override;
+	bool insertRows(int startRow, int count, const QModelIndex&) override;
+
+private:
+	Libraries& libraries;
+};
--- a/src/main.cpp	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/main.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -30,12 +30,18 @@
 	QApplication app (argc, argv);
 	app.setOrganizationName (APPNAME);
 	app.setApplicationName (APPNAME);
+	qRegisterMetaType<Library>("Library");
+	qRegisterMetaType<Libraries>("Libraries");
+	qRegisterMetaTypeStreamOperators<Library>("Library");
+	qRegisterMetaTypeStreamOperators<Libraries>("Libraries");
 
 	static Configuration configObject;
 	config = &configObject;
+	/*
 	LDPaths* paths = new LDPaths(&configObject);
 	paths->checkPaths();
 	paths->deleteLater();
+	*/
 
 	initializeCrashHandler();
 	LDColor::initColors();
--- a/src/toolsets/filetoolset.cpp	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/toolsets/filetoolset.cpp	Fri Mar 23 12:51:18 2018 +0200
@@ -93,14 +93,6 @@
 	(new ConfigDialog {m_window})->exec();
 }
 
-void FileToolset::setLDrawPath()
-{
-	LDrawPathDialog* dialog = new LDrawPathDialog {m_config->lDrawPath(), true};
-
-	if (dialog->exec())
-		m_config->setLDrawPath (dialog->path());
-}
-
 void FileToolset::exit()
 {
 	::exit(EXIT_SUCCESS);
@@ -155,10 +147,8 @@
 	{
 		for (LDObject* obj : selectedObjects())
 		{
-			QString contents = obj->asText();
-			QByteArray data = contents.toUtf8();
-			file.write(data, countof(data));
-			file.write("\r\n", 2);
+			file.write(obj->asText().toUtf8());
+			file.write("\r\n");
 		}
 	}
 	else
--- a/src/toolsets/filetoolset.h	Tue Mar 20 12:25:52 2018 +0200
+++ b/src/toolsets/filetoolset.h	Fri Mar 23 12:51:18 2018 +0200
@@ -44,6 +44,5 @@
 	Q_INVOKABLE void saveAll();
 	Q_INVOKABLE void saveAs();
 	Q_INVOKABLE void scanPrimitives();
-	Q_INVOKABLE void setLDrawPath();
 	Q_INVOKABLE void settings();
 };
--- a/tools/configcollector.py	Tue Mar 20 12:25:52 2018 +0200
+++ b/tools/configcollector.py	Fri Mar 23 12:51:18 2018 +0200
@@ -59,7 +59,7 @@
 	except:
 		pass
 
-	if endswith(value, 'f'):
+	if value.endswith('f'):
 		try:
 			float(value[:-1])
 			return 'float'
@@ -76,22 +76,32 @@
 
 	def collect(self, filename):
 		with open(filename) as file:
-			for line in file:
-				line = line.strip()
-				if line and not line.startswith('#'):
-					from re import search
-					match = search('^option (\w+) = (.+)$', line)
-					if not match:
-						raise ValueError('unable to parse: %r' % line)
-					name, value = match.groups()
-					match = search(r'^(\w+)\s*\{(.*)\}$', value)
-					try:
-						typename, value = match.groups()
-						if not value:
-							value = typename + ' {}'
-					except:
-						typename = deduce_type(value)
-					self.declare(name, typename, value)
+			for linenumber, line in enumerate(file, 1):
+				try:
+					line = line.strip()
+					if line and not line.startswith('#'):
+						from re import search
+						match = search('^option (\w+) = (.+)$', line)
+						if not match:
+							raise ValueError('unable to parse: %r' % line)
+						name, value = match.groups()
+						match = search(r'^([a-zA-Z0-9_<>]+)\s*\{(.*)\}$', value)
+						try:
+							typename, value = match.groups()
+							if not value:
+								value = typename + ' {}'
+						except:
+							typename = deduce_type(value)
+						self.declare(name, typename, value)
+				except ValueError as error:
+					from sys import stderr, exit
+					print(str.format(
+						'{file}:{line}: {error}',
+						file = filename,
+						line = linenumber,
+						error = str(error),
+					), file = stderr)
+					exit(1)
 		# Sort the declarations in alphabetical order
 		self.declarations = OrderedDict(sorted(self.declarations.items(), key = lambda t: t[1]['name']))
 		# Fill in additional information

mercurial