WSL/SLF GitLab Repository

main.cc 15.1 KB
Newer Older
1
2
3
4
5
/*****************************************************************************/
/*  Copyright 2019 WSL Institute for Snow and Avalanche Research  SLF-DAVOS  */
/*****************************************************************************/
/* This file is part of INIshell.
   INIshell is free software: you can redistribute it and/or modify
6
   it under the terms of the GNU General Public License as published by
7
8
9
10
11
12
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   INIshell 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
13
   GNU General Public License for more details.
14

15
16
   You should have received a copy of the GNU General Public License
   along with INIshell.  If not, see <http://www.gnu.org/licenses/>.
17
18
*/

19
/*
20
21
 * INIshell - A dynamic graphical interface to generate INI files
 * INIshell v2: Michael Reisecker, 2019
22
23
 * Inspired by INIshell v1: Mathias Bavay, Thomas Egger & Daniela Korhammer, 2011
 *
24
25
26
27
 * This is the main program starting the event loop.
 * 2019-10
 */

28
#include "colors.h"
29
#include "common.h"
30
#include "Error.h"
31
#include "XMLReader.h"
32
#include "src/gui/MainWindow.h"
33
#include "src/main/settings.h"
34
35

#include <QApplication>
36
#include <QCommandLineParser>
37
#include <QDir>
38
#include <QFile>
39
#include <QFileInfo>
40
#include <QList>
41
#include <QStandardPaths>
42
#include <QStringList>
43
#include <QStyleFactory>
44
#include <QTranslator>
45

46
47
#include <iostream>

48
49
50
51
/**
 * @brief Set meta data for the application.
 * @details This is used in different places INIshell writes and installs to depending on the OS.
 */
52
void setAppMetadata()
53
{
54
55
56
57
	QApplication::setApplicationName("INIshell");
	QApplication::setOrganizationName("SLF");
	QApplication::setOrganizationDomain("slf.ch");
	QApplication::setApplicationVersion(APP_VERSION_STR);
Michael Reisecker's avatar
Michael Reisecker committed
58
	QApplication::setWindowIcon(QIcon(":/icons/inishell_192.ico"));
59
60
}

61
62
63
64
65
/**
 * @brief Prepare the command line parser options.
 * @param[in] parser Instance of the command line parser.
 * @param[in] cmd_args Command line args given to the programs.
 */
66
void prepareCommandline(QCommandLineParser &parser, command_line_args &cmd_args)
67
{
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
	QList<QCommandLineOption> cmd_options;
	cmd_options << QCommandLineOption({"e", "exit"}, "Exit after command line operations (surpass GUI)");
	cmd_options << QCommandLineOption({"i", "inifile"}, "INI file to import on startup\nUse syntax SECTION::KEY=\"value\" as additional arguments to modifiy INI keys", "inifile");
	cmd_options << QCommandLineOption({"s", "settingsfile"}, "INIshell settings file", "settingsfile");
	cmd_options << QCommandLineOption({"o", "outinifile"}, "INI file to write out", "outinifile");
	cmd_options << QCommandLineOption("dump_resources", "Dump internal resource files to current directory");
	cmd_options << QCommandLineOption("dump_help", "Dump user's guide and developer's help to current directory");
	cmd_options << QCommandLineOption("print_search_dirs", "Print list of directories INIshell searches");
	cmd_options << QCommandLineOption("print_settings_location", "Print location of the settings file");
	cmd_options << QCommandLineOption({"c", "clear"}, "Clear settings file");
	cmd_options << QCommandLineOption("print_styles", "Print available Qt styles");
	cmd_options << QCommandLineOption("set_style", "Set the program style", "style");
	cmd_options << QCommandLineOption("info", "Display program info");

	parser.addOptions(cmd_options);
83
84
	parser.addHelpOption();
	parser.addVersionOption();
85

86
	parser.process(QCoreApplication::arguments());
87
88
	cmd_args.startup_ini_file = parser.value("inifile");
	cmd_args.settings_file = parser.value("settingsfile");
89
	cmd_args.out_ini_file = parser.value("outinifile");
90
	cmd_args.program_style = parser.value("set_style");
91
92
}

93
94
95
96
97
98
99
/**
 * @brief Work through command line arguments.
 * @details This function processes command line options on program start.
 * @return True if "exit" was parsed and the user wants to immediately quit after
 * the command line tools.
 */
bool workCommandlineArguments(QCommandLineParser *parser)
100
{
101
	if (parser->isSet("clear")) {
102
		const QString xml_settings_filename(getSettingsFileName());
103
		std::cout << "Deleting " << xml_settings_filename.toStdString() << "..." << std::endl;
104
		QFile sfile( getSettingsFileName() );
105
106
107
108
		sfile.remove();
		if (sfile.error())
			std::cerr << "[E] Can't delete settings file: " << sfile.errorString().toStdString() << std::endl;
	}
109
	if (parser->isSet("dump_resources")) { //note that resource files are write-protected when copying out
110
111
		std::cout << "Dumping config_schema.xsd..." << std::endl;
		QFile::copy(":config_schema.xsd", QDir::currentPath() + "/config_schema.xsd");
112
113
		std::cout << "Dumping inishell_settings_minimal.xml..." << std::endl;
		QFile::copy(":inishell_settings_minimal.xml", QDir::currentPath() + "/inishell_settings_minimal.xml");
114
115
116
117
118
119
120
	}
	if (parser->isSet("dump_help")) {
		std::cout << "Dumping help.xml and help_dev.xml..." << std::endl;
		QFile::copy(":doc/help.xml", QDir::currentPath() + "/help.xml");
		QFile::copy(":doc/help_dev.xml", QDir::currentPath() + "/help_dev.xml");
	}
	if (parser->isSet("print_search_dirs")) {
121
		const QStringList search_dirs( getSearchDirs(false) ); //without user dirs, settings not ready yet
122
123
124
125
126
		std::cout << "Searching the following directories:" << std::endl;
		for (auto &dir: search_dirs)
			std::cout << dir.toStdString() << std::endl;
	}
	if (parser->isSet("print_settings_location")) {
127
		std::cout << "Location of settings file: " << getSettingsFileName().toStdString() << std::endl;
128
129
130
	}
	if (parser->isSet("print_styles")) {
		std::cout << "The following styles are available:" << std::endl;
Michael Reisecker's avatar
Michael Reisecker committed
131
		for (auto &style : QStyleFactory::keys())
132
133
			std::cout << style.toStdString() << std::endl;
	}
134
135
136
137
138
139
140
	if (parser->isSet("info")) {
		std::cout << QApplication::applicationName().toStdString() << " " <<
		    QApplication::applicationVersion().toStdString() << std::endl;
		std::cout << "(c) 2019 " << QApplication::organizationName().toStdString() << ", " <<
		    QApplication::organizationDomain().toStdString() << std::endl;
		std::cout << "INIshell is a dynamic graphical user interface to manipulate INI files." << std::endl;
		std::cout << "Visit https://models.slf.ch/p/inishell-ng/ for more information." << std::endl;
141
		std::cout << "License: GNU General Public License" << std::endl;
142
143
144
		std::cout << "Run ./inishell --help to view all command line options." << std::endl;
		return true; //don't enter GUI
	}
145

Michael Reisecker's avatar
Michael Reisecker committed
146
	return (parser->isSet("exit"));
147
148
}

149
150
151
152
153
154
155
/**
 * @brief Load settings from INIshell's XML settings file.
 * @details This has nothing to do with the XMLs that are parsed to build the GUI,
 * stored here are INIshell's own static GUI settings (like the language etc.).
 * @param cmd_args
 * @return
 */
156
void loadSettings(QString &settings_file, QStringList &errors)
157
{
158
	if (!QFileInfo( settings_file ).exists()) { //quietly create file after first program start
159
160
161
		global_xml_settings = QDomDocument( );
		return;
	}
162

163
	XMLReader xml_settings_reader;
164
	QString xml_error = QString();
165
	xml_settings_reader.read(settings_file, xml_error, true);
166
	if (!xml_error.isNull()) {
167
168
169
		errors.push_back(QApplication::tr("Could not read settings file. Unable to load \"") +
		    QDir::toNativeSeparators(settings_file) + "\"\n" + xml_error +
		    QApplication::tr("If possible, the settings file will be recreated for the next program start (check INIshell's write access to the directory).\nIf not, INIshell will function normally but will not be able to save any settings."));
170
	}
171
	global_xml_settings = xml_settings_reader.getXml();
172
}
173
174
175
176
177
178
179
180

/**
 * @brief Set global stylesheets for panels/widgets.
 * @details Global styling is done here, including the styles of properties that may or may not
 * be set, such as how a mandatory field that is not filled in should look. Further styling is
 * done locally.
 * @param[in] app The main app instance.
 */
181
void setAppStylesheet(QApplication &app, const command_line_args &cmd_args)
182
{
183
184
185
186
187
188
189
190
191
192
193
	/*
	 * Unfortunately, it is technically not possible in Qt to style a widget in any way while
	 * keeping the native OS style, at least it is not guaranteed. Same for Palettes, which
	 * may or may not be respected. Hence, it is impossible to color for example a button and
	 * keep the macOS style, which also leads to surprising differences in the sizes.
	 * See the following links:
	 *   https://doc.qt.io/qt-5/stylesheet.html (last paragraph)
	 *   https://stackoverflow.com/questions/28839907/how-to-override-just-one-propertyvalue-pair-in-qt-stylesheet
	 * Furthermore, there are bugs in many of the styles that concern our look, for example
	 * a frame's caption may be striked through by the border, and a frame coloring may extend
	 * outside the frame.
194
195
	 * For these reasons we try to set a fixed style that is widely available. "Fusion" should be
	 * built by default; make sure on deployment that the plugin is included. The way I see it,
196
197
	 * the only alternative is to style each and every widget we use manually.
	 */
198

199
	if (!cmd_args.program_style.isEmpty())
200
		QApplication::setStyle(cmd_args.program_style);
201
	else
202
#if defined Q_OS_WIN
203
204
205
206
207
		QApplication::setStyle("WindowsVista");
#else
		if (QStyleFactory::keys().contains("Fusion"))
			QApplication::setStyle("Fusion");
#endif
208
209
	/*
	 * Set the global stylesheet:
210
211
212
213
	 * Here we define some properties that can be set for all panels (without casting to their type),
	 * and the style that will be applied if the property is set (e. g. default values, faulty
	 * expressions, ...).
	 * We also try to avoid gaps and borders of different colors, hence we set backgrounds for some
214
	 * of our design elements trying to take into account current OS color scheme settings.
215
	 */
216
	app.setStyleSheet(" \
217
	    * [mandatory=\"true\"] {background-color: " + colors::getQColor("mandatory").name() + "; color: " + colors::getQColor("normal").name() + "} \
218
	    * [shows_default=\"true\"] {font-style: italic; color: " + colors::getQColor("default_values").name() + "} \
Mathias Bavay's avatar
Mathias Bavay committed
219
220
	    * [faulty=\"true\"] {color: " + colors::getQColor("faulty_values").name() + "} \
	    * [valid=\"true\"] {color: " + colors::getQColor("valid_values").name() + "} \
221
222
223
	    QTabWidget {padding: 0px; font-weight: bold; background-color: " + colors::getQColor("app_bg").name() + "} \
	    QTabWidget:pane {background-color: " + colors::getQColor("app_bg").name() + "} \
	    QScrollArea {background-color: " + colors::getQColor("app_bg").name() + "} \
Mathias Bavay's avatar
Mathias Bavay committed
224
	    QScrollBar:horizontal {height: 15px;} \
225
	    Group {background-color: " + colors::getQColor("app_bg").name() + "} \
226
	");
227
228
}

229
/**
230
231
232
233
234
235
 * @brief Perform INI operations in command line mode.
 * @details INIshell will still start the GUI for most command line operations, unless explicitly
 * asked to quit via -e.
 * @param[in] parser Command line parser object.
 * @param[in] cmd_args Container for the command line arguments.
 * @param[in] errors Error messages to add on to if necessary.
236
 */
237
void perform_cmd_ini_operations(const QCommandLineParser &parser, const command_line_args &cmd_args, QStringList &errors)
238
{
239
240
	const QString in_inifile( cmd_args.startup_ini_file );
	const QString out_inifile( cmd_args.out_ini_file );
241
	if (!out_inifile.isEmpty() && in_inifile.isEmpty()) {
242
243
		const QString err_msg(
		    QApplication::tr(R"(To output a file with "-o" you need to specify the input file with "-i")"));
244
245
246
247
248
249
250
251
		errors.push_back(err_msg);
		std::cerr << "[E] " << err_msg.toStdString() << std::endl;
	} else if (!in_inifile.isEmpty()) {
		if (out_inifile.isEmpty()) {
			const QString err_msg(QApplication::tr(
			    R"(To input a file with "-i" you need to specify the output file with "-o")"));
			errors.push_back(err_msg);
			std::cerr << "[E] " << err_msg.toStdString() << std::endl;
252
		} else {
253
			INIParser cmd_ini;
254
			cmd_ini.parseFile(in_inifile);
255

256
257
258
259
260
261
262
263
264
			/* modify INI keys */
			for (auto &pos : parser.positionalArguments()) {
				const QStringList mod_ini_list(pos.split("="));
				if (mod_ini_list.size() == 2) {
					const QStringList param_list(mod_ini_list.at(0).trimmed().split(
					    Cst::sep, QString::SkipEmptyParts));
					if (param_list.size() == 2) //silently skip wrong formats
						cmd_ini.set(param_list.at(0), param_list.at(1),
						    mod_ini_list.at(1).trimmed());
265
				}
266
			}
267

268
269
270
271
			QFile ini_output(out_inifile);
			if (ini_output.open(QIODevice::WriteOnly)) {
				QTextStream iniss(&ini_output);
				cmd_ini.outputIni(iniss);
272
			} else {
273
274
				const QString err_msg(QApplication::tr(R"(Unable to open output INI file "%1": %2)").arg(
				    QDir::toNativeSeparators(out_inifile), ini_output.errorString()));
275
276
				errors.push_back(err_msg);
				std::cerr << "[E] " << err_msg.toStdString() << std::endl;
277
			}
278
279
		} //endif out_inifile.isEmpty()
	} //endif in/outfile.isEmpty()
280
281
282
283
284
285
286
287
288
289
290
}

/**
 * @brief Entry point of the main program.
 * @details This function starts the main event loop.
 * @param[in] argc Command line arguments count.
 * @param[in] argv Command line arguments.
 * @return Exit code.
 */
int main(int argc, char *argv[])
{
291
	QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
292
	QStringList errors; //errors that happen before a logger is available
293
294
295
296
297
298
299
300
301
302
303
304
305
	QApplication app(argc, argv);
	setAppMetadata();

	/* parse the command line */
	QCommandLineParser parser;
	command_line_args cmd_args;
	prepareCommandline(parser, cmd_args); //will also be used in MainWindow() where more info is available
	const bool exit_early = workCommandlineArguments(&parser);

	/* load and apply settings for the static part of the GUI */
	QString xml_settings_filename(getSettingsFileName()); //e. g. ".config/SLF/INIshell/inishell_settings.xml" on GNU/Linux
	if (!cmd_args.settings_file.isEmpty()) //file given in command line
		xml_settings_filename = cmd_args.settings_file;
306
307
	loadSettings(xml_settings_filename, errors); //load global settings file
	checkSettings(); //make sure valid settings can be read throughout this program run
308
309

	QFont global_font(QApplication::font());
310
	global_font.setPointSize(getSetting("user::appearance::fontsize", "value").toInt());
311
312
313
314
	QApplication::setFont(global_font);

	/* command line INI file manipulation */
	perform_cmd_ini_operations(parser, cmd_args, errors);
315
	if (exit_early) //exit after command line tools if user gives "--exit"
316
		return 0;
317
318

	/* GUI mode when reaching this */
319
	const QString language( getSetting("user::appearance::language", "value") );
320
321
322
323
324
	QTranslator translator; //can't go out of scope
	if (!language.isEmpty() && language != "en") { //texts that are not found will remain in English
		const QString language_file(":/languages/inishell_" + language);
		if (translator.load(language_file)) {
			QApplication::installTranslator(&translator);
325
		} else { //should not happen since it's a resource and the build process would complain
326
327
328
329
330
331
332
			Error("Language file not found", "File \"" + language_file +
			    "\" is not a valid language file.");
			errors.push_back("Language file not found ~ File \"" + language_file +
			    "\" is not a valid language file.");
		}
	} //endif language

333
	setAppStylesheet(app, cmd_args);
334
	//open MainWindow with the XML settings and their path, and errors that occurred so far:
335
	MainWindow main_window(xml_settings_filename, errors);
336
	main_window.show(); //start INIshell's GUI
337
	errors.clear(); //save a bit of RAM - the messages have been processed
338
	return QApplication::exec();
339
}