Sort out versions more, add about page

Mon, 27 Jun 2022 15:46:12 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Mon, 27 Jun 2022 15:46:12 +0300
changeset 272
9d52b119b3f5
parent 271
416e8c497829
child 273
57952c5ca59b

Sort out versions more, add about page

CMakeLists.txt file | annotate | diff | comparison | revisions
src/main.cpp file | annotate | diff | comparison | revisions
src/mainwindow.ui file | annotate | diff | comparison | revisions
src/version.cpp file | annotate | diff | comparison | revisions
src/version.h file | annotate | diff | comparison | revisions
tools/caseconversions.py file | annotate | diff | comparison | revisions
tools/outputfile.py file | annotate | diff | comparison | revisions
tools/updaterevision.py file | annotate | diff | comparison | revisions
--- a/CMakeLists.txt	Mon Jun 27 02:01:52 2022 +0300
+++ b/CMakeLists.txt	Mon Jun 27 15:46:12 2022 +0300
@@ -3,17 +3,16 @@
 set(VERSION_MAJOR 1)
 set(VERSION_MINOR 0)
 set(VERSION_PATCH 0)
+set(COPYRIGHT "Copyright (C) 2013 - 2022 Teemu Piippo")
 
-set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}")
-if (NOT ${VERSION_PATCH} EQUAL 0)
-	set(VERSION_STRING "${VERSION_STRING}.${VERSION_PATCH}")
-endif()
+set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
 string(TOLOWER ${PROJECT_NAME} TARGET_NAME)
 add_definitions(-DVERSION_MAJOR=${VERSION_MAJOR})
 add_definitions(-DVERSION_MINOR=${VERSION_MINOR})
 add_definitions(-DVERSION_PATCH=${VERSION_PATCH})
 add_definitions(-DAPPNAME="${PROJECT_NAME}")
 add_definitions(-DVERSION_STRING="${VERSION_STRING}")
+add_definitions(-DCOPYRIGHT="${COPYRIGHT}")
 
 set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMake")
 set(OpenGL_GL_PREFERENCE GLVND)
@@ -112,6 +111,7 @@
 	src/widgets/colorselectdialog.h
 )
 set(FORM_FILES
+	src/about.ui
 	src/mainwindow.ui
 	src/settingseditor/librarieseditor.ui
 	src/settingseditor/settingseditor.ui
--- a/src/main.cpp	Mon Jun 27 02:01:52 2022 +0300
+++ b/src/main.cpp	Mon Jun 27 15:46:12 2022 +0300
@@ -6,6 +6,7 @@
 #include <QScrollBar>
 #include <QStackedWidget>
 #include <QTranslator>
+#include <ui_about.h>
 #include <ui_mainwindow.h>
 #include "src/gl/partrenderer.h"
 #include "src/layers/axeslayer.h"
@@ -69,7 +70,7 @@
 
 static void doQtRegistrations()
 {
-	QCoreApplication::setApplicationName(::appName);
+	QCoreApplication::setApplicationName(QStringLiteral(APPNAME));
 	QCoreApplication::setOrganizationName("hecknology.net");
 	QCoreApplication::setOrganizationDomain("hecknology.net");
 	qRegisterMetaType<Message>();
@@ -213,16 +214,9 @@
 	}
 }
 
-/**
- * @brief Updates the title of the main window so to contain the app's name
- * and version as well as the open document name.
- */
 static QString title()
 {
-	QString title = ::appName;
-	title += " ";
-	title += fullVersionString();
-	return title;
+	return fullVersionString();
 }
 
 static ColorTable loadColors(const LibrariesModel* libraries)
@@ -364,6 +358,23 @@
 	return result;
 }
 
+static void about(QWidget* parent)
+{
+	QDialog dialog{parent};
+	Ui_About ui;
+	ui.setupUi(&dialog);
+	ui.textBrowser->setHtml(
+		ui.textBrowser->toHtml()
+		.replace("%APPNAME%", APPNAME)
+		.replace("%COPYRIGHT%", COPYRIGHT)
+		.replace("%VERSION%", detailedVersionString())
+		.replace("%REVDATE%", revisionDateString())
+		.replace("%QTVERSION%", qVersion())
+	);
+	dialog.setWindowTitle(QObject::tr("About %1").arg(APPNAME));
+	dialog.exec();
+}
+
 int main(int argc, char *argv[])
 {
 	doQtRegistrations();
@@ -584,7 +595,7 @@
 				addRecentlyOpenedFile(*pathPtr);
 			}
 		}
-	};
+	};;
 	const auto actionSaveAs = [&]{
 		const std::optional<ModelId> modelId = findCurrentModelId(&ui);
 		if (modelId.has_value())
@@ -709,6 +720,11 @@
 				}
 			}
 		});
+	QObject::connect(
+		ui.actionAbout,
+		&QAction::triggered,
+		[&mainWindow]{about(&mainWindow);}
+	);
 	mainWindow.tabifyDockWidget(ui.messageLogDock, ui.toolOptionsDock);
 	mainWindow.restoreGeometry(setting<Setting::MainWindowGeometry>());
 	mainWindow.restoreState(setting<Setting::MainWindowState>());
@@ -720,6 +736,7 @@
 	restoreSettings();
 	updateRenderPreferences(&ui, &renderPreferences, &documents);
 	mainWindow.setWindowTitle(title());
+	ui.actionAbout->setText(ui.actionAbout->text().arg(APPNAME));
 	mainWindow.show();
 	const int result = app.exec();
 	saveSettings();
--- a/src/mainwindow.ui	Mon Jun 27 02:01:52 2022 +0300
+++ b/src/mainwindow.ui	Mon Jun 27 15:46:12 2022 +0300
@@ -39,7 +39,7 @@
      <x>0</x>
      <y>0</y>
      <width>729</width>
-     <height>38</height>
+     <height>41</height>
     </rect>
    </property>
    <widget class="QMenu" name="menuFile">
@@ -92,6 +92,7 @@
     <property name="title">
      <string>Help</string>
     </property>
+    <addaction name="actionAbout"/>
     <addaction name="actionAboutQt"/>
    </widget>
    <addaction name="menuFile"/>
@@ -474,6 +475,11 @@
     <string>Make unofficial</string>
    </property>
   </action>
+  <action name="actionAbout">
+   <property name="text">
+    <string>About %1</string>
+   </property>
+  </action>
  </widget>
  <resources>
   <include location="../resources.qrc"/>
--- a/src/version.cpp	Mon Jun 27 02:01:52 2022 +0300
+++ b/src/version.cpp	Mon Jun 27 15:46:12 2022 +0300
@@ -17,36 +17,46 @@
  */
 
 #include <QString>
+#include <QDateTime>
 #include <time.h>
 #include <hginfo.h>
 #include "src/version.h"
 
-const char* fullVersionString()
+#ifdef HG_ALL_TAGS
+# define HGTEXT HG_NODE " (" HG_ALL_TAGS ")"
+#else
+# define HGTEXT HG_NODE
+#endif
+
+QString detailedVersionString()
 {
-	if (::BUILD_TYPE != ReleaseBuild) {
-		return VERSION_STRING "-" HG_DATE_VERSION;
-	}
-	else {
-		return VERSION_STRING;
-	}
+	return QStringLiteral(VERSION_STRING "-" HGTEXT)
+		+ " (" + revisionDateString() + ")";
 }
 
-static QString makeCommitTimeString()
+QString versionString()
 {
-	QString result;
-#ifdef HG_DATE_TIME
-	{
-		char buffer[100];
-		constexpr time_t timestamp = HG_DATE_TIME;
-		strftime (buffer, sizeof buffer, "%d %b %Y", localtime (&timestamp));
-		result += buffer;
-	}
+#ifndef HG_VERSION_TAG
+	return detailedVersionString();
+#else
+	return QStringLiteral(HG_VERSION_TAG);
 #endif
-	return result;
 }
 
-const QString &commitTimeString()
+static QString makeFullVersionString()
+{
+	QString versionstring = APPNAME " " + versionString();
+	return versionstring;
+}
+
+const QString& fullVersionString()
 {
-	static QString result = makeCommitTimeString();
-	return result;
+	static const QString cached = makeFullVersionString();
+	return cached;
 }
+
+QString revisionDateString()
+{
+	const QDateTime dt = QDateTime::fromSecsSinceEpoch(HG_DATE_TIME);
+	return dt.toString(QObject::tr("d MMMM yyyy"));
+}
--- a/src/version.h	Mon Jun 27 02:01:52 2022 +0300
+++ b/src/version.h	Mon Jun 27 15:46:12 2022 +0300
@@ -19,17 +19,6 @@
 #pragma once
 #include <QString>
 
-constexpr char appName[] = APPNAME;
-
-constexpr struct {
-	int major = VERSION_MAJOR;
-	int minor = VERSION_MINOR;
-	int patch = VERSION_PATCH;
-} APPVERSION;
-
-enum BuildType {InternalBuild, ReleaseBuild};
-constexpr BuildType BUILD_TYPE = InternalBuild;
-
 #ifdef DEBUG
 # undef RELEASE
 #endif // DEBUG
@@ -38,5 +27,7 @@
 # undef DEBUG
 #endif // RELEASE
 
-const char *fullVersionString();
-const QString& commitTimeString();
+const QString& fullVersionString();
+QString detailedVersionString();
+QString versionString();
+QString revisionDateString();
--- a/tools/caseconversions.py	Mon Jun 27 02:01:52 2022 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-#!/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
--- a/tools/outputfile.py	Mon Jun 27 02:01:52 2022 +0300
+++ b/tools/outputfile.py	Mon Jun 27 15:46:12 2022 +0300
@@ -31,31 +31,32 @@
 #
 
 class OutputFile:
-    def __init__ (self, filename):
+    def __init__(self, filename, *, verbose = False):
         self.filename = filename
+        self.verbose = verbose
+        self.body = ''
+        self.oldsum = ''
+    def __enter__(self):
         try:
-            with open (self.filename, "r") as file:
-                self.oldsum = file.readline()
-                self.oldsum = self.oldsum.replace ('// ', '').strip()
+            with open(self.filename, "r") as file:
+                self.oldsum = file.readline().strip().removeprefix('// ')
         except IOError:
-            self.oldsum = ''
-        self.body = ''
-
+            pass
+        return self
     def write(self, text):
         self.body += text
-
-    def save(self, verbose = False):
+    def __exit__(self, *args):
         from hashlib import sha256
         checksum = sha256(self.body.encode('utf-8')).hexdigest()
         if checksum == self.oldsum:
-            if verbose:
-                print ('%s is up to date' % self.filename)
+            if self.verbose:
+                print(f'{self.filename} is up to date')
             pass
         else:
-            with open (self.filename, "w") as file:
+            with open(self.filename, "w") as file:
                 file.write('// %s\n' % checksum)
                 file.write('// This file has been automatically generated. Do not edit by hand\n')
                 file.write('\n')
                 file.write(self.body)
-                if verbose:
-                    print('%s written' % self.filename)
+                if self.verbose:
+                    print(f'{self.filename} written ({checksum})')
--- a/tools/updaterevision.py	Mon Jun 27 02:01:52 2022 +0300
+++ b/tools/updaterevision.py	Mon Jun 27 15:46:12 2022 +0300
@@ -33,36 +33,47 @@
 import argparse
 import outputfile
 
-def main():
+if __name__ == '__main__':
 	import subprocess
 	from datetime import datetime
 	parser = argparse.ArgumentParser(description='Writes a header file with Hg commit information')
 	parser.add_argument('--cwd', default = '.')
 	parser.add_argument('output')
 	args = parser.parse_args()
-	f = outputfile.OutputFile(args.output)
 	data = subprocess.check_output(['hg', 'log', '--cwd', args.cwd, '-r.', '--template',
-		'{node|short} {branch} {date|hgdate}']).decode().replace('\n', '').split(' ')
-
+		'{node|short} {branch} {date|hgdate} {tags}']).decode().replace('\n', '').split()
 	rev = data[0]
 	branch = data[1]
 	timestamp = int(data[2])
+	all_tags = set(data[4:])
+	try:
+		version_tag = [tag for tag in all_tags if tag.startswith('v')][0]
+	except IndexError:
+		version_tag = None
 	date = datetime.utcfromtimestamp(timestamp)
 	datestring = date.strftime('%y%m%d-%H%M') if date.year >= 2000 else '000000-0000'
-
 	if len(rev) > 7:
-		rev = rev[0:7]
-
-	if subprocess.check_output(['hg', 'id', '--cwd', args.cwd, '-n']).decode().replace('\n', '').endswith('+'):
+		rev = rev[:7]
+	modified = subprocess.check_output(['hg', 'id', '--cwd', args.cwd, '-n']).decode().strip().endswith('+')
+	if modified:
 		rev += '+'
-
-	f.write('#define HG_NODE "%s"\n' % rev)
-	f.write('#define HG_BRANCH "%s"\n' % branch)
-	f.write('#define HG_DATE_VERSION "%s"\n' % datestring)
-	f.write('#define HG_DATE_STRING "%s"\n' % date.strftime('%d %b %Y'))
-	f.write('#define HG_DATE_TIME %d\n' % int(timestamp))
-	if f.save():
-		print('%s updated to %s' %(f.filename, rev))
-
-if __name__ == '__main__':
-	main()
+		# if the source tree is modified, it's not the released version
+		version_tag = None
+	if version_tag is not None:
+		# tags should be the same in released versions whether or not it's tip
+		# (though released versions shouldn't be tip because hgtags is modified
+		# to create the tag 🤔, but let's do this anyway)
+		all_tags -= {'tip'}
+	with outputfile.OutputFile(args.output, verbose = True) as f:
+		f.write(f'#define HG_NODE "{rev}"\n')
+		if branch != 'default':
+			f.write(f'#define HG_BRANCH "{branch}"\n')
+		f.write(f'#define HG_DATE_VERSION "{datestring}"\n')
+		f.write(f'#define HG_DATE_TIME {int(timestamp)}\n')
+		if all_tags:
+			f.write(f'#define HG_ALL_TAGS "{" ".join(sorted(all_tags))}"\n')
+		if version_tag:
+			f.write(f'#define HG_VERSION_TAG "{version_tag[1:]}"\n')
+		if 'tip' in all_tags:
+			f.write('#define HG_TIP\n')
+	print(f'Current Hg revision: {rev}')

mercurial