WSL/SLF GitLab Repository

main.cc 14.3 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 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.
 */
156
void setAppStylesheet(QApplication &app, const command_line_args &cmd_args)
157
{
158
159
160
161
162
163
164
165
166
167
168
	/*
	 * 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.
169
170
	 * 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,
171
172
	 * the only alternative is to style each and every widget we use manually.
	 */
173

174
	if (!cmd_args.program_style.isEmpty())
175
		QApplication::setStyle(cmd_args.program_style);
176
	else
177
#if defined Q_OS_WIN
178
179
180
181
182
		QApplication::setStyle("WindowsVista");
#else
		if (QStyleFactory::keys().contains("Fusion"))
			QApplication::setStyle("Fusion");
#endif
183
184
	/*
	 * Set the global stylesheet:
185
186
187
188
	 * 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
189
	 * of our design elements trying to take into account current OS color scheme settings.
190
	 */
191
	app.setStyleSheet(" \
Michael Reisecker's avatar
Michael Reisecker committed
192
	    * [mandatory=\"true\"] {font: bold; color: " + colors::getQColor("mandatory").name() + "} \
193
	    * [shows_default=\"true\"] {font-style: italic; color: " + colors::getQColor("default_values").name() + "} \
Michael Reisecker's avatar
Michael Reisecker committed
194
195
	    * [invalid=\"true\"] {color:" + colors::getQColor("invalid_values").name() + ";} \
	    * [highlight=\"true\"] {background-color: " + colors::getQColor("sl_yellow").name() + "} \
196
197
198
	    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
199
	    QScrollBar:horizontal {height: 15px;} \
200
	    Group {background-color: " + colors::getQColor("app_bg").name() + "} \
201
	");
202
203
}

204
/**
205
206
207
208
209
210
 * @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.
211
 */
212
void perform_cmd_ini_operations(const QCommandLineParser &parser, const command_line_args &cmd_args, QStringList &errors)
213
{
214
215
	const QString in_inifile( cmd_args.startup_ini_file );
	const QString out_inifile( cmd_args.out_ini_file );
216
	if (!out_inifile.isEmpty() && in_inifile.isEmpty()) {
217
218
		const QString err_msg(
		    QApplication::tr(R"(To output a file with "-o" you need to specify the input file with "-i")"));
219
220
221
222
223
224
225
226
		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;
227
		} else {
228
			INIParser cmd_ini;
229
			cmd_ini.parseFile(in_inifile);
230

231
232
			/* modify INI keys */
			for (auto &pos : parser.positionalArguments()) {
Mathias Bavay's avatar
Mathias Bavay committed
233
				const QStringList mod_ini_list( pos.split("=") );
234
235
236
237
238
239
				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());
240
				}
241
			}
242

243
244
245
246
			QFile ini_output(out_inifile);
			if (ini_output.open(QIODevice::WriteOnly)) {
				QTextStream iniss(&ini_output);
				cmd_ini.outputIni(iniss);
247
			} else {
248
249
				const QString err_msg(QApplication::tr(R"(Unable to open output INI file "%1": %2)").arg(
				    QDir::toNativeSeparators(out_inifile), ini_output.errorString()));
250
251
				errors.push_back(err_msg);
				std::cerr << "[E] " << err_msg.toStdString() << std::endl;
252
			}
253
254
		} //endif out_inifile.isEmpty()
	} //endif in/outfile.isEmpty()
255
256
257
258
259
260
261
262
263
264
265
}

/**
 * @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[])
{
266
	QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
267
	QStringList errors; //errors that happen before a logger is available
268
269
270
271
272
273
274
275
276
277
278
279
280
	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;
281
282
	loadSettings(xml_settings_filename, errors); //load global settings file
	checkSettings(); //make sure valid settings can be read throughout this program run
283

284
285
286
287
	//NOTE: Due to the issues outlined in setAppStylesheet() font inheritance is stopped as soon
	//as a custom font, stylesheet, or style is set (e. g. for macOS styling).
	//We would need to set the font manually for each label, widget, ... and always when we
	//apply a stylesheet...
288
	QFont global_font(QApplication::font());
289
	global_font.setPointSize(getSetting("user::appearance::fontsize", "value").toInt());
290
291
292
293
	QApplication::setFont(global_font);

	/* command line INI file manipulation */
	perform_cmd_ini_operations(parser, cmd_args, errors);
294
	if (exit_early) //exit after command line tools if user gives "--exit"
295
		return 0;
296
297

	/* GUI mode when reaching this */
298
	const QString language( getSetting("user::appearance::language", "value") );
299
300
301
302
303
	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);
304
		} else { //should not happen since it's a resource and the build process would complain
305
306
307
308
309
310
311
			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

312
	setAppStylesheet(app, cmd_args);
313
	//open MainWindow with the XML settings and their path, and errors that occurred so far:
314
	MainWindow main_window(xml_settings_filename, errors);
315
	main_window.show(); //start INIshell's GUI
316
	errors.clear(); //save a bit of RAM - the messages have been processed
317
	return QApplication::exec();
318
}