added automated configuration collection

Sun, 02 Feb 2020 00:30:48 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Sun, 02 Feb 2020 00:30:48 +0200
changeset 41
0abada2a9802
parent 40
30cb5e836736
child 42
1d03dc1173cd

added automated configuration collection

CMakeLists.txt file | annotate | diff | comparison | revisions
locale/fi.ts file | annotate | diff | comparison | revisions
src/configurationoptions.txt file | annotate | diff | comparison | revisions
src/gl/common.h file | annotate | diff | comparison | revisions
src/libraries.cpp file | annotate | diff | comparison | revisions
src/libraries.h file | annotate | diff | comparison | revisions
src/mainwindow.cpp file | annotate | diff | comparison | revisions
src/mainwindow.h file | annotate | diff | comparison | revisions
src/settingseditor/librarieseditor.cpp file | annotate | diff | comparison | revisions
src/settingseditor/librarieseditor.h file | annotate | diff | comparison | revisions
src/settingseditor/settingseditor.cpp file | annotate | diff | comparison | revisions
src/settingseditor/settingseditor.h file | annotate | diff | comparison | revisions
tools/caseconversions.py file | annotate | diff | comparison | revisions
tools/configcollector.py file | annotate | diff | comparison | revisions
--- a/CMakeLists.txt	Sat Feb 01 17:20:10 2020 +0200
+++ b/CMakeLists.txt	Sun Feb 02 00:30:48 2020 +0200
@@ -140,6 +140,7 @@
 	${LDFORGE_QM_RC_FILE}
 	${QM_FILES}
 	${LDFORGE_FORMS_HEADERS}
+	${CMAKE_BINARY_DIR}/configuration.cpp
 )
 
 set_source_files_properties(${LDFORGE_HEADERS} PROPERTIES HEADER_FILE_ONLY TRUE)
@@ -150,6 +151,18 @@
 add_dependencies(ldforge resources)
 cotire(ldforge)
 
+add_custom_target (config_collection ALL
+	COMMAND python3
+	    "${CMAKE_SOURCE_DIR}/tools/configcollector.py"
+		--header ${CMAKE_BINARY_DIR}/configuration.h
+		--source ${CMAKE_BINARY_DIR}/configuration.cpp
+		--sourcedir ${CMAKE_SOURCE_DIR}/src
+		${CMAKE_SOURCE_DIR}/src/configurationoptions.txt
+	WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
+add_dependencies(ldforge config_collection)
+set_source_files_properties (${CMAKE_BINARY_DIR}/configuration.cpp PROPERTIES GENERATED TRUE)
+set_property(SOURCE configuration.cpp PROPERTY SKIP_AUTOGEN ON)
+
 # Collect the current hg revision into hginfo.h
 add_custom_target(revision_check ALL
 	COMMAND python3 "${CMAKE_SOURCE_DIR}/tools/updaterevision.py" --cwd "$(CMAKE_SOURCE_DIR)" hginfo.h
--- a/locale/fi.ts	Sat Feb 01 17:20:10 2020 +0200
+++ b/locale/fi.ts	Sun Feb 02 00:30:48 2020 +0200
@@ -181,27 +181,27 @@
         <translation type="unfinished">Satunnaiset värit</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="91"/>
+        <location filename="../src/mainwindow.cpp" line="97"/>
         <source>Open model</source>
         <translation>Avaa malli</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="93"/>
+        <location filename="../src/mainwindow.cpp" line="99"/>
         <source>LDraw models (*.ldr *.dat)</source>
         <translation>LDraw-mallit (*.ldr *.dat)</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="112"/>
+        <location filename="../src/mainwindow.cpp" line="118"/>
         <source>Problem loading references</source>
         <translation type="unfinished">Ongelma viitteiden lataamisessa</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="122"/>
+        <location filename="../src/mainwindow.cpp" line="128"/>
         <source>Problem opening file</source>
         <translation>Ongelma tiedoston avaamisessa</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="124"/>
+        <location filename="../src/mainwindow.cpp" line="130"/>
         <source>Could not open %1: %2</source>
         <translation>Ei voitu avata %1: %2</translation>
     </message>
@@ -209,7 +209,7 @@
 <context>
     <name>PartRenderer</name>
     <message>
-        <location filename="../src/gl/partrenderer.cpp" line="179"/>
+        <location filename="../src/gl/partrenderer.cpp" line="184"/>
         <source>Rendering error</source>
         <translation type="unfinished"></translation>
     </message>
@@ -238,7 +238,7 @@
     </message>
     <message>
         <location filename="../src/settingseditor/settingseditor.ui" line="46"/>
-        <location filename="../src/settingseditor/settingseditor.cpp" line="55"/>
+        <location filename="../src/settingseditor/settingseditor.cpp" line="57"/>
         <source>System language</source>
         <translation>Järjestelmän kieli</translation>
     </message>
@@ -254,16 +254,21 @@
     </message>
     <message>
         <location filename="../src/settingseditor/settingseditor.ui" line="83"/>
-        <source>Main color:</source>
+        <source>Main colour:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../src/settingseditor/settingseditor.ui" line="131"/>
+        <location filename="../src/settingseditor/settingseditor.ui" line="114"/>
+        <source>Background colour:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/settingseditor/settingseditor.ui" line="175"/>
         <source>LDraw parts libraries</source>
         <translation>LDraw-osakirjastot</translation>
     </message>
     <message>
-        <location filename="../src/settingseditor/settingseditor.ui" line="136"/>
+        <location filename="../src/settingseditor/settingseditor.ui" line="180"/>
         <source>Keyboard shortcuts</source>
         <translation>Näppäinyhdistelmät</translation>
     </message>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/configurationoptions.txt	Sun Feb 02 00:30:48 2020 +0200
@@ -0,0 +1,23 @@
+#
+# LDForge configuration option definitions
+#
+# Syntax:
+#    option OptionName = value
+#    option OptionName = type {value}
+#    # comment
+#
+
+# Rendering options
+option Locale = "system"
+option BackgroundColor = QColor{48, 48, 48}
+option MainColor = QColor{255, 255, 64}
+option LineThickness = 2.0f
+option RenderStyle = 0
+option DrawWireframe = false
+option AntiAliasedLines = true
+option MainWindowGeometry = QByteArray{}
+option MainSplitterState = QByteArray{}
+option RecentFiles = QStringList{}
+
+# File management options
+option Libraries = QVector<Library>{}
--- a/src/gl/common.h	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/gl/common.h	Sun Feb 02 00:30:48 2020 +0200
@@ -132,7 +132,6 @@
 
 	constexpr ArrayClass ARRAY_CLASSES[] = {ArrayClass::Lines, ArrayClass::Triangles, ArrayClass::Quads, ArrayClass::ConditionalLines};
 	constexpr int NUM_ARRAY_CLASSES = countof(ARRAY_CLASSES);
-	constexpr int FLOATS_PER_VERTEX = 7;
 
 	enum class RenderStyle
 	{
--- a/src/libraries.cpp	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/libraries.cpp	Sun Feb 02 00:30:48 2020 +0200
@@ -18,6 +18,7 @@
 
 #include <QSettings>
 #include "libraries.h"
+#include "configuration.h"
 
 /**
  * @brief Constructs a new library manager
@@ -33,7 +34,7 @@
  * @param settings Settings to construct from
  * @param parent Parent object
  */
-LibraryManager::LibraryManager(QSettings* settings, QObject* parent) :
+LibraryManager::LibraryManager(Configuration* settings, QObject* parent) :
 	QAbstractTableModel{parent}
 {
 	this->restoreFromSettings(settings);
@@ -160,19 +161,18 @@
  * changes are lost.
  * @param settings Settings object to restore from.
  */
-void LibraryManager::restoreFromSettings(QSettings* settings)
+void LibraryManager::restoreFromSettings(Configuration* settings)
 {
-	this->libraries = settings->value("libraries").value<Libraries>();
+	this->libraries = settings->libraries();
 }
 
 /**
  * @brief Saves the libraries to the specified settings object.
  * @param settings Settings object to modify.
  */
-void LibraryManager::storeToSettings(QSettings* settings)
+void LibraryManager::storeToSettings(Configuration* settings)
 {
-	QVariant librariesValue = QVariant::fromValue(this->libraries);
-	settings->setValue("libraries", librariesValue);
+	settings->setLibraries(this->libraries);
 }
 
 /**
@@ -380,3 +380,8 @@
 	library.role = static_cast<Library::Role>(role);
 	return result;
 }
+
+bool operator==(const Library& one, const Library& other)
+{
+	return one.role == other.role and one.path == other.path;
+}
--- a/src/libraries.h	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/libraries.h	Sun Feb 02 00:30:48 2020 +0200
@@ -38,6 +38,8 @@
 	constexpr static const Role allRoles[] = {OfficialLibrary, UnofficialLibrary, WorkingLibrary};
 };
 
+bool operator==(const Library& one, const Library& other);
+
 Q_DECLARE_METATYPE(Library)
 Q_DECLARE_METATYPE(Library::Role)
 QDataStream &operator<<(QDataStream&, const Library&);
@@ -46,12 +48,14 @@
 using Libraries = QVector<Library>;
 Q_DECLARE_METATYPE(Libraries)
 
+class Configuration;
+
 class LibraryManager : public QAbstractTableModel
 {
 	Q_OBJECT
 public:
 	LibraryManager(QObject* parent = nullptr);
-	LibraryManager(QSettings* settings, QObject* parent = nullptr);
+	LibraryManager(Configuration* settings, QObject* parent = nullptr);
 	QVector<Library>::const_iterator begin() const;
 	QVector<Library>::const_iterator end() const;
 	QString findFile(QString fileName) const;
@@ -60,8 +64,8 @@
 	const Library& library(int libraryIndex) const;
 	void setLibraryPath(int libraryIndex, const QDir& path);
 	void setLibraryRole(int libraryIndex, const Library::Role role);
-	void restoreFromSettings(QSettings* settings);
-	void storeToSettings(QSettings* settings);
+	void restoreFromSettings(Configuration* settings);
+	void storeToSettings(Configuration* settings);
 	int count() const;
 	void moveLibrary(const int libraryFromIndex, const int libraryToIndex);
 	// Definitions for QAbstractTableModel
--- a/src/mainwindow.cpp	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/mainwindow.cpp	Sun Feb 02 00:30:48 2020 +0200
@@ -45,6 +45,11 @@
 	{ offsetof(Ui_MainWindow, actionRenderStyleRandom), gl::RenderStyle::RandomColors },
 };
 
+class A : public QSettings
+{
+	using QSettings::QSettings;
+};
+
 MainWindow::MainWindow(QWidget *parent) :
 	QMainWindow{parent},
 	ui{std::make_unique<Ui_MainWindow>()},
@@ -197,7 +202,7 @@
 				document->restoreSplitterState(this->documentSplitterState);
 			}
 		}
-		this->settings.setValue("MainWindow/DocumentSplitterState", this->documentSplitterState);
+		this->settings.setMainSplitterState(this->documentSplitterState);
 	}
 }
 
@@ -290,16 +295,16 @@
  */
 void MainWindow::saveSettings()
 {
-	this->settings.setValue("MainWindow/Geometry", this->saveGeometry());
-	this->settings.setValue("MainWindow/RecentlyOpened", this->recentlyOpenedFiles);
-	this->settings.setValue("MainWindow/DocumentSplitterState", this->documentSplitterState);
-	this->settings.setValue("MainWindow/RenderStyle", static_cast<int>(this->renderPreferences.style));
+	this->settings.setMainWindowGeometry(this->saveGeometry());
+	this->settings.setRecentFiles(this->recentlyOpenedFiles);
+	this->settings.setMainSplitterState(this->documentSplitterState);
+	this->settings.setRenderStyle(static_cast<int>(this->renderPreferences.style));
 	this->libraries.storeToSettings(&this->settings);
 }
 
 void MainWindow::restoreStartupSettings()
 {
-	this->restoreGeometry(this->settings.value("MainWindow/Geometry").toByteArray());
+	this->restoreGeometry(this->settings.mainWindowGeometry());
 }
 
 /**
@@ -307,18 +312,14 @@
  */
 void MainWindow::restoreSettings()
 {
-	this->recentlyOpenedFiles = this->settings.value("MainWindow/RecentlyOpened").toStringList();
-	this->documentSplitterState = this->settings.value("MainWindow/DocumentSplitterState").toByteArray();
-	this->renderPreferences.style = static_cast<gl::RenderStyle>(this->settings.value("Render/Style").toInt());
-	this->renderPreferences.mainColor = this->settings.value(
-		"Render/MainColor",
-		gl::RenderPreferences{}.mainColor).toString();
-	this->renderPreferences.backgroundColor = this->settings.value(
-		"Render/BackgroundColor",
-		gl::RenderPreferences{}.backgroundColor).toString();
+	this->recentlyOpenedFiles = this->settings.recentFiles();
+	this->documentSplitterState = this->settings.mainSplitterState();
+	this->renderPreferences.style = static_cast<gl::RenderStyle>(this->settings.renderStyle());
+	this->renderPreferences.mainColor = this->settings.mainColor();
+	this->renderPreferences.backgroundColor = this->settings.backgroundColor();
 	const QString systemLocale = QLocale::system().name();
-	const QVariant defaultLocale = this->settings.value("locale", systemLocale);
-	changeLanguage(defaultLocale.toString());
+	const QVariant defaultLocale = this->settings.locale();
+	this->changeLanguage(defaultLocale.toString());
 	this->libraries.restoreFromSettings(&this->settings);
 	this->updateRecentlyOpenedDocumentsMenu();
 	this->loadColors();
--- a/src/mainwindow.h	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/mainwindow.h	Sun Feb 02 00:30:48 2020 +0200
@@ -21,6 +21,7 @@
 #include <QTranslator>
 #include <QSettings>
 #include <QKeySequence>
+#include "configuration.h"
 #include "documentmanager.h"
 #include "libraries.h"
 #include "uiutilities.h"
@@ -49,7 +50,7 @@
 	QMap<Model*, QWidget*> modelWidgets;
 	QString currentLanguage = "en";
 	QTranslator translator;
-	QSettings settings;
+	Configuration settings;
 	LibraryManager libraries;
 	QByteArray documentSplitterState;
 	uiutilities::KeySequenceMap defaultKeyboardShortcuts;
--- a/src/settingseditor/librarieseditor.cpp	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/settingseditor/librarieseditor.cpp	Sun Feb 02 00:30:48 2020 +0200
@@ -4,7 +4,7 @@
 #include "librarieseditor.h"
 #include "ui_librarieseditor.h"
 
-LibrariesEditor::LibrariesEditor(QSettings* settings, QWidget* parent) :
+LibrariesEditor::LibrariesEditor(Configuration* settings, QWidget* parent) :
 	QWidget{parent},
 	libraries{settings, this},
 	ui{*new Ui_LibrariesEditor}
@@ -145,7 +145,7 @@
 	return this->ui.librariesTable->selectionModel()->currentIndex().row();
 }
 
-void LibrariesEditor::saveSettings(QSettings* settings)
+void LibrariesEditor::saveSettings(Configuration* settings)
 {
 	this->libraries.storeToSettings(settings);
 }
--- a/src/settingseditor/librarieseditor.h	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/settingseditor/librarieseditor.h	Sun Feb 02 00:30:48 2020 +0200
@@ -7,9 +7,9 @@
 {
 	Q_OBJECT
 public:
-	LibrariesEditor(QSettings* settings, QWidget* parent = nullptr);
+	LibrariesEditor(Configuration* settings, QWidget* parent = nullptr);
 	~LibrariesEditor();
-	void saveSettings(QSettings* settings);
+	void saveSettings(Configuration* settings);
 private slots:
 	void searchPathForNewLibrary();
 	void addNewLibrary();
--- a/src/settingseditor/settingseditor.cpp	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/settingseditor/settingseditor.cpp	Sun Feb 02 00:30:48 2020 +0200
@@ -5,7 +5,7 @@
 #include "ui_settingseditor.h"
 
 SettingsEditor::SettingsEditor(
-	QSettings* settings,
+	Configuration* settings,
 	const uiutilities::KeySequenceMap& defaultKeyboardShortcuts,
 	QWidget* parent
 ) :
@@ -37,9 +37,9 @@
 
 void SettingsEditor::handleAccepted()
 {
-	this->settings->setValue("locale", this->ui.language->currentData().toString());
-	this->settings->setValue("Render/MainColor", this->ui.mainColorButton->selectedColor().name());
-	this->settings->setValue("Render/BackgroundColor", this->ui.backgroundColorButton->selectedColor().name());
+	this->settings->setLocale(this->ui.language->currentData().toString());
+	this->settings->setMainColor(this->ui.mainColorButton->selectedColor());
+	this->settings->setBackgroundColor(this->ui.backgroundColorButton->selectedColor());
 	this->librariesEditor.saveSettings(this->settings);
 }
 
@@ -67,13 +67,9 @@
 
 void SettingsEditor::setDefaults()
 {
-	this->setCurrentLanguage(this->settings->value("locale", QLocale::system().name()).toString());
-	this->ui.mainColorButton->setSelectedColor(this->settings->value(
-		"Render/MainColor",
-		gl::RenderPreferences{}.mainColor).toString());
-	this->ui.backgroundColorButton->setSelectedColor(this->settings->value(
-		"Render/BackgroundColor",
-		gl::RenderPreferences{}.backgroundColor).toString());
+	this->setCurrentLanguage(this->settings->locale());
+	this->ui.mainColorButton->setSelectedColor(this->settings->mainColor());
+	this->ui.backgroundColorButton->setSelectedColor(this->settings->backgroundColor());
 }
 
 void SettingsEditor::setCurrentLanguage(const QString& localeCode)
--- a/src/settingseditor/settingseditor.h	Sat Feb 01 17:20:10 2020 +0200
+++ b/src/settingseditor/settingseditor.h	Sun Feb 02 00:30:48 2020 +0200
@@ -4,12 +4,13 @@
 #include "librarieseditor.h"
 #include "libraries.h"
 #include "uiutilities.h"
+#include "configuration.h"
 
 class SettingsEditor : public QDialog
 {
 	Q_OBJECT
 public:
-	SettingsEditor(QSettings* settings,
+	SettingsEditor(Configuration* settings,
 		const uiutilities::KeySequenceMap& defaultKeyboardShortcuts = {},
 		QWidget* parent = nullptr);
 	~SettingsEditor();
@@ -17,7 +18,7 @@
 	void handleAccepted();
 private:
 	class Ui_SettingsEditor& ui;
-	QSettings* const settings;
+	Configuration* const settings;
 	LibraryManager libraries;
 	LibrariesEditor librariesEditor;
 	const uiutilities::KeySequenceMap defaultKeyboardShortcuts;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/caseconversions.py	Sun Feb 02 00:30:48 2020 +0200
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+'''
+Provides facilities to converting identifier cases.
+'''
+
+#
+#	Copyright 2015 Teemu Piippo
+#	All rights reserved.
+#
+#	Redistribution and use in source and binary forms, with or without
+#	modification, are permitted provided that the following conditions
+#	are met:
+#
+#	1. Redistributions of source code must retain the above copyright
+#	   notice, this list of conditions and the following disclaimer.
+#	2. Redistributions in binary form must reproduce the above copyright
+#	   notice, this list of conditions and the following disclaimer in the
+#	   documentation and/or other materials provided with the distribution.
+#	3. Neither the name of the copyright holder nor the names of its
+#	   contributors may be used to endorse or promote products derived from
+#	   this software without specific prior written permission.
+#
+#	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+#	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+#	TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+#	PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
+#	OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+#	EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+#	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+#	PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+#	LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+#	NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+#	SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+import string
+import re
+
+def to_one_case (name, tolower):
+	'''
+	Convers name to either all uppercase or all lowercase (depending on the truth value of tolower), using underscores
+	as delimiters.
+	'''
+	result = ""
+	targetSet = (string.ascii_lowercase if tolower else string.ascii_uppercase) + string.digits
+	inverseSet = (string.ascii_uppercase if tolower else string.ascii_lowercase)
+	isUnderscorable = lambda ch: ch in string.ascii_uppercase \
+		or ch in string.whitespace or ch in string.punctuation
+	previousWasUnderscorable = isUnderscorable (name[0])
+
+	for ch in name:
+		if isUnderscorable (ch) and result != "" and not previousWasUnderscorable:
+			result += "_"
+
+		if ch in inverseSet:
+			result += (ch.lower() if tolower else ch.upper())
+		elif ch in targetSet:
+			result += ch
+		previousWasUnderscorable = isUnderscorable (ch)
+
+	return result
+
+def to_camel_case (name, java = False):
+	'''
+	Converts name to camelcase. If java is False, the first letter will be lowercase, otherwise it will be uppercase.
+	'''
+	result = ""
+	wantUpperCase = False
+
+	# If it's all uppercase, make it all lowercase so that this algorithm can digest it
+	if name == name.upper():
+		name = name.lower()
+
+	for ch in name:
+		if ch == '_':
+			wantUpperCase = True
+			continue
+
+		if wantUpperCase:
+			if ch in string.ascii_lowercase:
+				ch = ch.upper()
+			wantUpperCase = False
+
+		result += ch
+
+	if java:
+		match = re.match (r'^([A-Z]+)([A-Z].+)$', result)
+		if match:
+			result = match.group (1).lower() + match.group (2)
+		else:
+			result = result[0].lower() + result[1:]
+	else:
+		result = result[0].upper() + result[1:]
+	return result
+
+case_styles = {
+	'lower': lambda name: to_one_case (name, tolower=True),
+	'upper': lambda name: to_one_case (name, tolower=False),
+	'camel': lambda name: to_camel_case (name, java=False),
+	'java': lambda name: to_camel_case (name, java=True),
+}
+
+def convert_case (name, style):
+	'''
+	Converts name to the given style. Style may be one of:
+		- 'lower': 'foo_barBaz' -> 'foo_bar_baz'
+		- 'upper': 'foo_barBaz' -> 'FOO_BAR_BAZ'
+		- 'camel': 'foo_barBaz' -> 'FooBarBaz'
+		- 'java': 'foo_barBaz' -> 'fooBarBaz'
+	'''
+	try:
+		stylefunc = case_styles[style]
+	except KeyError:
+		validstyles = ', '.join (sorted (case_styles.keys()))
+		raise ValueError ('Unknown style "%s", should be one of: %s' % (style, validstyles))
+	else:
+		return stylefunc (name)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/configcollector.py	Sun Feb 02 00:30:48 2020 +0200
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+# coding: utf-8
+#
+#	Copyright 2015 - 2017 Teemu Piippo
+#	All rights reserved.
+#
+#	Redistribution and use in source and binary forms, with or without
+#	modification, are permitted provided that the following conditions
+#	are met:
+#
+#	1. Redistributions of source code must retain the above copyright
+#	   notice, this list of conditions and the following disclaimer.
+#	2. Redistributions in binary form must reproduce the above copyright
+#	   notice, this list of conditions and the following disclaimer in the
+#	   documentation and/or other materials provided with the distribution.
+#	3. Neither the name of the copyright holder nor the names of its
+#	   contributors may be used to endorse or promote products derived from
+#	   this software without specific prior written permission.
+#
+#	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+#	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+#	TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+#	PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
+#	OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+#	EXEMPLARY, OR CONSEQUENTIAL DAMAGES(INCLUDING, BUT NOT LIMITED TO,
+#	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+#	PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+#	LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT(INCLUDING
+#	NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+#	SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+from argparse import ArgumentParser
+from collections import OrderedDict
+import caseconversions
+import outputfile
+
+# These types are passed by value
+passbyvalue = {'int', 'bool', 'float', 'double', 'qreal'}
+
+def deduce_type(value):
+	'''
+		Try to determine the type of value from the value itself.
+	'''
+	if value in('true', 'false'):
+		return 'bool'
+	elif value.startswith(('"', 'R"')) and value.endswith('"'):
+		return 'QString'
+
+	try:
+		int(value)
+		return 'int'
+	except:
+		pass
+
+	try:
+		float(value)
+		return 'double'
+	except:
+		pass
+
+	if value.endswith('f'):
+		try:
+			float(value[:-1])
+			return 'float'
+		except:
+			pass
+
+	raise ValueError('unable to deduce type of %r' % value)
+
+class ConfigCollector:
+	def __init__(self, args):
+		self.declarations = OrderedDict()
+		self.qtTypes = set()
+		self.args = args
+
+	def collect(self, filename):
+		with open(filename) as file:
+			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 = match.group(1)
+						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
+		for declaration in self.declarations.values():
+			declaration['readgate'] = caseconversions.convert_case(declaration['name'], style='java')
+			declaration['writegate'] = 'set' + caseconversions.convert_case(declaration['name'], style='camel')
+			declaration['togglefunction'] = 'toggle' + caseconversions.convert_case(declaration['name'], style='camel')
+			if declaration['type'] in passbyvalue:
+				declaration['typereference'] = declaration['type']
+			else:
+				declaration['typereference'] = 'const %s&' % declaration['type']
+
+	def declare(self, name, typename, default):
+		from re import findall
+		if name in self.declarations:
+			raise ValueError('Attempted to redeclare %r' % name)
+		self.declarations[name] = {
+			'name': name,
+			'type': typename,
+			'default': default
+		}
+		# Keep a file of any Qt types, we'll need to #include them.
+		self.qtTypes.update(findall(r'Q\w+', typename))
+
+	def writeHeader(self, device):
+		device.write('#pragma once\n')
+		device.write('#include <QMap>\n')
+		device.write('#include <QSettings>\n')
+		device.write('#include <glm/glm.hpp>\n')
+		for qtType in sorted(self.qtTypes):
+			device.write('#include <%s>\n' % qtType)
+		device.write('#include "libraries.h"\n')
+		device.write('\n')
+		formatargs = {}
+		write = lambda value: device.write(value)
+		write('class Configuration : private QSettings\n')
+		write('{\n')
+		write('public:\n')
+		write('\tusing QSettings::QSettings;\n');
+		write('\tbool exists(const QString& name);\n')
+		write('\tQVariant value(const QString& name);\n')
+		write('\tQVariant setValue(const QString& name);\n')
+		for declaration in self.declarations.values():
+			write('\t{type} {readgate}();\n'.format(**declaration))
+		for declaration in self.declarations.values():
+			write('\tvoid {writegate}({typereference} value);\n'.format(**declaration))
+		for declaration in filter(lambda declaration: declaration['type'] == 'bool', self.declarations.values()):
+			write('\tvoid {togglefunction}();\n'.format(**declaration))
+		write('private:\n')
+		write('\tusing QSettings::value;\n')
+		write('\tusing QSettings::setValue;\n')
+		write('\tstatic const QMap<QString, QVariant>& defaults();\n')
+		write('};\n')
+	
+	def writeSource(self, device, headername):
+		device.write('#include <QSet>\n')
+		device.write('#include <QSettings>\n')
+		device.write('#include <QVariant>\n')
+		device.write('#include "%s/mainwindow.h"\n' % (self.args.sourcedir))
+		device.write('#include "%s"\n' % headername)
+		device.write(
+			'const QMap<QString, QVariant>& Configuration::defaults()\n'
+			'{\n'
+			'\tstatic const QMap<QString, QVariant> defaults {\n'
+		)
+		for declaration in self.declarations.values():
+			device.write('\t\t{{"{name}", QVariant::fromValue<{type}>({default})}},\n'.format(**declaration))
+		device.write('\t};\n'
+			'\treturn defaults;\n'
+			'}\n'
+			'\n')
+		device.write('bool Configuration::exists(const QString& name)\n')
+		device.write('{\n')
+		device.write('\treturn defaults().contains(name);\n')
+		device.write('}\n')
+		device.write('\n')
+		device.write('QVariant Configuration::value(const QString& name)\n'
+			'{\n'
+			'\treturn this->value(name, Configuration::defaults().value(name));\n'
+			'}\n')
+		device.write('\n')
+		for declaration in self.declarations.values():
+			device.write('{type} Configuration::{readgate}()\n'.format(**declaration))
+			device.write('{\n')
+			device.write('\tstatic const QVariant defaultvalue = QVariant::fromValue<{type}>({default});\n'.format(**declaration))
+			device.write('\treturn this->value("{name}", defaultvalue).value<{type}>();\n'.format(**declaration))
+			device.write('}\n')
+			device.write('\n')
+		for declaration in self.declarations.values():
+			device.write('void Configuration::{writegate}({typereference} value)\n'.format(**declaration))
+			device.write('{\n')
+			device.write('\tif (value != {default})\n'.format(**declaration))
+			device.write('\t\tthis->setValue("{name}", QVariant::fromValue<{type}>(value));\n'.format(**declaration))
+			device.write('\telse\n')
+			device.write('\t\tthis->remove("{name}");\n'.format(**declaration))
+			device.write('}\n')
+			device.write('\n')
+		for declaration in filter(lambda declaration: declaration['type'] == 'bool', self.declarations.values()):
+			device.write('void Configuration::{togglefunction}()\n'.format(**declaration))
+			device.write('{\n')
+			device.write('\t{writegate}(not {readgate}());\n'.format(**declaration))
+			device.write('}\n')
+			device.write('\n')
+
+def main():
+	parser = ArgumentParser(description='Collects a list of configuration objects')
+	parser.add_argument('input')
+	parser.add_argument('--header', required=True)
+	parser.add_argument('--source', required=True)
+	parser.add_argument('--sourcedir', required=True)
+	args = parser.parse_args()
+	collector = ConfigCollector(args)
+	collector.collect(args.input)
+	from outputfile import OutputFile
+	header = OutputFile(args.header)
+	source = OutputFile(args.source)
+	collector.writeSource(source, headername=args.header)
+	collector.writeHeader(header)
+	header.save(verbose = True)
+	source.save(verbose = True)
+
+if __name__ == '__main__':
+	main()

mercurial