WSL/SLF GitLab Repository

MainWindow.cc 50.5 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
22
23
/*
 * The main window that is displayed on startup.
 * 2019-10
 */

24
#include "MainWindow.h"
Michael Reisecker's avatar
Michael Reisecker committed
25
#include "src/gui/AboutWindow.h"
26
#include "src/gui_elements/Group.h" //to exclude Groups from panel search
27
#include "src/main/colors.h"
28
#include "src/main/common.h"
29
30
31
#include "src/main/Error.h"
#include "src/main/dimensions.h"
#include "src/main/INIParser.h"
32
#include "src/main/inishell.h"
33
#include "src/main/settings.h"
34
#include "src/main/XMLReader.h"
35
36

#include <QApplication>
37
#include <QCheckBox>
38
#include <QDesktopServices>
39
#include <QDir>
40
#include <QFileDialog>
41
#include <QFileInfo>
Michael Reisecker's avatar
Michael Reisecker committed
42
#include <QGroupBox>
43
#include <QMenuBar>
44
#include <QMessageBox>
45
#include <QRegularExpression>
46
#include <QSpacerItem>
47
48
49
50
#include <QStatusBar>
#include <QSysInfo>
#include <QToolBar>

51
52
#include <QMovie>

53
54
55
#ifdef DEBUG
	#include <iostream>
#endif
56

57
58
59
60
61
62
63
/**
 * @class MouseEventFilter
 * @brief Mouse event listener for the main program window.
 * @param[in] object Object the event stems from.
 * @param[in] event The type of event.
 * @return True if the event was accepted.
 */
64
65
66
bool MouseEventFilter::eventFilter(QObject *object, QEvent *event)
{
	if (event->type() == QEvent::MouseButtonPress) {
67
		if (object->property("mouseclick") == "open_log") {
68
			getMainWindow()->viewLogger();
69
70
71
72
		} else if (object->property("mouseclick") == "open_ini") {
			if (!getMainWindow()->ini_filename_->text().isEmpty())
				QDesktopServices::openUrl("file:" + getMainWindow()->ini_filename_->text());
		}
73
	}
74
	return QObject::eventFilter(object, event); //pass to actual event of the object
75
76
}

77
78
79
/**
 * @class MainWindow
 * @brief Constructor for the main window.
80
81
 * @param[in] settings_location Path to the INIshell settings file.
 * @param[in] errors List of errors that have occurred before starting the GUI.
82
83
 * @param[in] parent Unused since the main window is kept in scope by the entry point function.
 */
84
85
MainWindow::MainWindow(QString &settings_location, const QStringList &errors, QMainWindow *parent)
    : QMainWindow(parent), logger_(this), xml_settings_filename_(settings_location)
86
{
87
88
89
	/*
	 * Program flowchart sketch:
	 *
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
	 *   Set application style and properties  <----- main.cc ----->  cmd line options,
	 *                  |                                             translations
	 *         Load INIshell settings  <----- main.cc ----->  store in global XML node
	 *                  |
	 *             Launch GUI  <----- MainWindow.cc's constructor
	 *                  |
	 *  User opens:     v
	 *      application or simulation                   INI file  <----- INIParser::read()
	 *      -------------------------                   --------
	 *                  |                      (Only possible if an application is loaded)
	 *                  |                                   |
	 *                  |    Find panels by matching their IDs to INIs key <--- set file values
	 *                  |                                   |
	 *                  |                                   v      (ready for user interaction)
	 *                  |                   ________________________________________
	 *                  |                   |INI file contents are displayed in GUI|
	 *                  v                   ----------------------------------------
107
	 *   _________________________________
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
	 *   |  Build GUI elements from XML  |  <----- Scan file system for application XMLs
	 *   ---------------------------------           (read and check: XMLParser class)
	 *                  |                                  ^
	 *         Find panels in XML  <----- inishell.cc      L WorkflowPanel::scanFoldersForApps()
	 *                  |
	 *              Build panels  <----- gui_elements.cc ----->  The panels' constructors
	 *                  |
	 *        Set panels' properties  <----- the panels' setOptions() member functions
	 *                  |
	 * Give panels IDs to interact with INI changes  <----- Atomic::setPrimaryWidget()
	 *                  |                                     ^
	 *                  V                                     |
	 *            _______________             (Base class for all panels, uniform access to
	 *            |GUI is built!|             styling, section/key; used to find all panels)
	 *            ---------------
123
	 *                  |
124
125
126
	 *                  v
	 *          Set INI values from:
	 *          --------------------
127
	 *
128
	 *         XML              INI  <----- INIParser class
129
130
131
132
133
	 *   (default values)  (file system)                      VALIDATION
	 *          |                |                           (set styles)
	 *          |                |                                ^
	 *          v                v                               /
	 *    _____________________________________                 /
134
135
	 *    |       Panel's onPropertySet       |  -----> CHECK VALUE
	 *    -------------------------------------           |
136
	 *          ^                ^        (Property "no_ini" not set? Value not empty?)
137
138
139
140
141
142
143
	 *          |                |                        |
	 *          |                |                        v    MainPanel::setIniValuesFromGui()
	 *         GUI              CODE                    OUTPUT            |
	 * (user interaction)  (reactive changes)        (on request)         |
	 *          ^                                         |               v
	 *          |           Run through panels by IDs (= INI keys) and fill INIParser
	 *   Detect changes                                   |
144
	 *   (when closing)  <----- INIParser::operator==     v
145
	 *                                               Output to file  <----- INIParser class
146
	 */
147

148
	status_timer_ = new QTimer(this); //for temporary status messages
149
150
151
	status_timer_->setSingleShot(true);
	connect(status_timer_, &QTimer::timeout, this, &MainWindow::clearStatus);

152
	logger_.logSystemInfo();
153
	for (auto &err : errors) //errors from main.cc before the Logger existed
154
		logger_.log(err, "error");
155

156
	/* retrieve and set main window geometry */
Michael Reisecker's avatar
Michael Reisecker committed
157
158
	dim::setDimensions(this, dim::MAIN_WINDOW);
	dim::setDimensions(&logger_, dim::LOGGER);
159

160
	/* create basic GUI items */
161
	this->setUnifiedTitleAndToolBarOnMac(true);
162
	this->setWindowTitle(QCoreApplication::applicationName());
163
164
	createMenu();
	createToolbar();
165
	createStatusbar();
166

167
	preview_ = new PreviewWindow(this);
168
	/* create the dynamic GUI area */
169
	control_panel_ = new MainPanel(this);
170
	this->setCentralWidget(control_panel_);
171
	ini_.setLogger(&logger_);
172
173
174
175
	if (errors.isEmpty())
		setStatus(tr("Ready."), "info");
	else
		setStatus(tr("Errors occurred on startup"), "error");
176
}
177

178
179
180
181
/**
 * @brief Destructor for the main window.
 * @details This function is called after all safety checks have already been performed.
 */
182
MainWindow::~MainWindow()
183
{ //safety checks are performed in closeEvent()
184
	setWindowSizeSettings(); //put the main window sizes into the settings XML
185
	saveSettings();
186
187
	delete mouse_events_toolbar_;
	delete mouse_events_statusbar_;
188
189
}

190
191
192
193
194
195
196
/**
 * @brief Build the dynamic GUI.
 * This function initiates the recursive GUI building with an XML document that was
 * parsed beforehand.
 * @param[in] xml XML to build the gui from.
 */
void MainWindow::buildGui(const QDomDocument &xml)
197
{
198
	setUpdatesEnabled(false); //disable painting until done
199
	QDomNode root = xml.firstChild();
200
201
202
203
204
205
206
207
	while (!root.isNull()) {
		if (root.isElement()) { //skip over comments
			//give no parent group - tabs will be created for top level:
			recursiveBuild(root, nullptr, QString());
			break;
		}
		root = root.nextSibling();
	}
208
	setUpdatesEnabled(true);
209
210
}

211
212
213
214
215
/**
 * @brief Retrieve all panels that handle a certain INI key.
 * @param[in] ini_key The INI key to find.
 * @return A list of all panels that handle the INI key.
 */
216
217
QList<Atomic *> MainWindow::getPanelsForKey(const QString &ini_key)
{
218
	QList<Atomic *> panel_list( control_panel_->findChildren<Atomic *>(Atomic::getQtKey(ini_key)) );
219
	for (auto it = panel_list.begin(); it != panel_list.end(); ++it) {
220
221
		//groups don't count towards finding INI keys (for this reason, they additionally
		//have the "no_ini" key set):
222
		if (qobject_cast<Group *>(*it))
223
			panel_list.erase(it);
224
225
226
227
	}
	return panel_list;
}

228
229
230
231
232
233
/**
 * @brief Save the current GUI to an INI file.
 * @details This function calls the underlying function that does this and displays a message if
 * the user has neglected to set some mandatory INI values.
 * @param[in] filename The file to save to. If not given, the current INI file will be used.
 */
234
void MainWindow::saveIni(const QString &filename)
235
{
236
237
238
239
240
241
	/*
	 * We keep the original INIParser as-is, i. e. we always keep the INI file as it was loaded
	 * originally. This is solemnly to be able to check if anything has changed through user
	 * interaction, and if yes, warn before closing.
	 */
	INIParser gui_ini = ini_; //an INIParser is safe to copy around - this is a deep copy
242
	const QString missing( control_panel_->setIniValuesFromGui(&gui_ini) );
243

244
	if (!missing.isEmpty()) {
245
246
247
		QMessageBox msgMissing;
		msgMissing.setWindowTitle("Warning ~ " + QCoreApplication::applicationName());
		msgMissing.setText(tr("<b>Missing mandatory INI values.</b>"));
248
249
		msgMissing.setInformativeText(tr(
		    "Some non-optional INI keys are not set.\nSee details for a list or go back to the GUI and set all highlighted fields."));
250
		msgMissing.setDetailedText(tr("Missing INI keys:\n") + missing);
251
252
253
254
255
256
		msgMissing.setIcon(QMessageBox::Warning);
		msgMissing.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel);
		msgMissing.setDefaultButton(QMessageBox::Cancel);
		int clicked = msgMissing.exec();
		if (clicked == QMessageBox::Cancel)
			return;
257
	}
258
	//if no file is specified we save to the currently open INI file (save vs. save as):
259
260
261
	gui_ini.writeIni(filename.isEmpty()? gui_ini.getFilename() : filename);
}

262
263
264
/**
 * @brief Save the currently set values to a new INI file.
 */
265
266
void MainWindow::saveIniAs()
{
267
268
269
	QString start_path( getSetting("auto::history::last_ini", "path") );
	if (start_path.isEmpty())
		start_path = getSetting("auto::history::last_preview_write", "path");
270
271
272
273
274
	if (start_path.isEmpty())
		start_path = QDir::currentPath();

	const QString filename = QFileDialog::getSaveFileName(this, tr("Save INI file"),
	    start_path + "/" + ini_filename_->text(),
275
	    "INI files (*.ini *.INI);;All files (*)", nullptr, QFileDialog::DontUseNativeDialog);
276
277
278
	if (filename.isNull()) //cancelled
		return;
	saveIni(filename);
279
	ini_.setFilename(filename); //the new file is the new current file like in all programs
280
	ini_filename_->setText(filename);
281
	autoload_->setVisible(true); //if started from an empty GUI, this could still be disabled
282
283
	toolbar_save_ini_->setEnabled(true); //toolbar entry
	file_save_ini_->setEnabled(true); //menu entry
284
285
286

	const QFileInfo finfo(filename);
	setSetting("auto::history::last_ini", "path", finfo.absoluteDir().path());
287
288
}

289
290
291
/**
 * @brief Select a path for an INI file to be opened, and then open it.
 */
292
void MainWindow::openIni()
293
{
294
	QString start_path( getSetting("auto::history::last_ini", "path") );
295
296
297
298
	if (start_path.isEmpty())
		start_path = QDir::currentPath();

	const QString path = QFileDialog::getOpenFileName(this, tr("Open INI file"), start_path,
299
300
	    "INI files (*.ini);;All files (*)", nullptr,
	    QFileDialog::DontUseNativeDialog | QFileDialog::DontConfirmOverwrite);
301
	if (path.isNull()) //cancelled
302
303
		return;
	openIni(path);
304
305
306

	const QFileInfo finfo(path);
	setSetting("auto::history::last_ini", "path", finfo.absoluteDir().path());
307
308
}

309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
/**
 * @brief Set the loaded panels' values from an INI file.
 * @param[in] ini The INI file in form of an INIParser (usually the main one).
 * @return True if all INI keys are known to the loaded XML.
 */
bool MainWindow::setGuiFromIni(const INIParser &ini)
{
	bool all_ok = true;
	bool first_error_message = true;
	for (auto &sec : ini.getSectionsCopy()) { //run through sections in INI file
		ScrollPanel *tab_scroll = getControlPanel()->getSectionScrollarea(sec.getName(),
		    QString(), QString(), true); //get the corresponding tab of our GUI
		if (tab_scroll != nullptr) { //section exists in GUI
			const auto kv_list( sec.getKeyValueList() );
			for (size_t ii = 0; ii < kv_list.size(); ++ii) {
				//find the corresponding panel, and try to create it for dynamic panels
				//(e. g. Selector, Replicator):
				QWidgetList widgets( findPanel(tab_scroll, sec, *sec[ii]) );
				if (!widgets.isEmpty()) {
					for (int jj = 0; jj < widgets.size(); ++jj) //multiple panels can share the same key
						widgets.at(jj)->setProperty("ini_value", sec[ii]->getValue());
				} else {
					writeGuiFromIniHeader(first_error_message, ini);
					logger_.log(tr("%1 does not know INI key \"").arg(current_application_) +
					    sec.getName() + Cst::sep + sec[ii]->getKey() + "\"", "warning");
					all_ok = false;
				}
			} //endfor kv
		} else { //section does not exist
			writeGuiFromIniHeader(first_error_message, ini);
			log(tr(R"(%1 does not know INI section "[%2]")").arg(
			    current_application_, sec.getName()), "warning");
			all_ok = false;
		} //endif section exists
	} //endfor sec
	return all_ok;
}

347
348
349
350
351
/**
 * @brief Open an INI file and set the GUI to it's values.
 * @param[in] path The INI file to open.
 * @param[in] is_autoopen Is the INI being loaded through the autoopen mechanism?
 */
352
void MainWindow::openIni(const QString &path, const bool &is_autoopen, const bool &fresh)
353
{
354
	this->getControlPanel()->getWorkflowPanel()->setEnabled(false); //hint at INIshell processing...
355
	setStatus(tr("Reading INI file..."), "info", true);
356
	refreshStatus(); //necessary if heavy operations follow
357
358
	if (fresh)
		clearGui();
359
	const bool success = ini_.parseFile(path); //load the file into the main INI parser
360
361
362
363
	if (!setGuiFromIni(ini_)) //set the GUI to the INI file's values
		setStatus(tr("INI file read with unknown keys"), "warning");
	else
		setStatus(tr("INI file read ") + (success? tr("successfully") : tr("with warnings")),
364
		    (success? "info" : "warning")); //ill-formatted lines in INI file
365
	toolbar_save_ini_->setEnabled(true);
366
	file_save_ini_->setEnabled(true);
367
	autoload_->setVisible(true);
368
	ini_filename_->setText(path);
369
	autoload_box_->setText(tr("autoload this INI for ") + current_application_);
370
	if (!is_autoopen) //when a user clicks an INI file to open it we ask anew whether to autoopen
371
		autoload_box_->setCheckState(Qt::Unchecked);
372

373
	this->getControlPanel()->getWorkflowPanel()->setEnabled(true);
Mathias Bavay's avatar
Mathias Bavay committed
374
	QApplication::alert( this ); //notify the user that the task is finished
375
376
}

377
378
379
380
381
382
/**
 * @brief Close the currently opened INI file.
 * @details This function checks whether the user has made changes to the INI file and if yes,
 * allows to save first, discard changes, or cancel the operation.
 * @return True if the INI file was closed, false if the user has cancelled.
 */
383
bool MainWindow::closeIni()
384
{
385
	if (!help_loaded_ && //unless user currently has the help opened which we always allow to close
386
	    getSetting("user::inireader::warn_unsaved_ini", "value") == "TRUE") {
387
388
389
390
391
392
393
		/*
		 * We leave the original INIParser - the one that holds the values like they were
		 * originally read from an INI file - intact and make a copy of it. The currently
		 * set values from the GUI are loaded into the copy, and then the two are compared
		 * for changes. This way we don't display a "settings may be lost" warning if in
		 * fact nothing has changed, resp. the changes cancelled out.
		 */
394
395
396
397
398
399
		INIParser gui_ini = ini_;
		(void) control_panel_->setIniValuesFromGui(&gui_ini);
		if (ini_ != gui_ini) {
			QMessageBox msgNotSaved;
			msgNotSaved.setWindowTitle("Warning ~ " + QCoreApplication::applicationName());
			msgNotSaved.setText(tr("<b>INI settings will be lost.</b>"));
400
			msgNotSaved.setInformativeText(tr(
401
			    "Some INI keys will be lost if you don't save the current INI file."));
402
			msgNotSaved.setDetailedText(ini_.getEqualityCheckMsg());
403
404
405
			msgNotSaved.setIcon(QMessageBox::Warning);
			msgNotSaved.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel | QMessageBox::Discard);
			msgNotSaved.setDefaultButton(QMessageBox::Cancel);
406

407
			auto *show_msg_again = new QCheckBox(tr("Don't show this warning again"));
408
409
410
411
412
413
414
415
			show_msg_again->setToolTip(tr("The warning can be re-enabled in the settings"));
			show_msg_again->setStyleSheet("QCheckBox {color: " + colors::getQColor("info").name() + "}");
			msgNotSaved.setCheckBox(show_msg_again);
			connect(show_msg_again, &QCheckBox::stateChanged, [](int state) {
				const bool checked = (state == Qt::CheckState::Checked);
				setSetting("user::inireader::warn_unsaved_ini", "value", !checked? "TRUE" : "FALSE");
			});

416
417
418
			int clicked = msgNotSaved.exec();
			if (clicked == QMessageBox::Cancel)
				return false;
Michael Reisecker's avatar
Michael Reisecker committed
419
			if (clicked == QMessageBox::Save)
420
				saveIni();
421
			delete show_msg_again;
422
423
		}
	} //endif help_loaded
424

425
	ini_.clear();
426
	toolbar_save_ini_->setEnabled(false);
427
	file_save_ini_->setEnabled(false);
428
	ini_filename_->setText(QString());
429
	autoload_->setVisible(false);
430
	return true;
431
432
}

433
434
435
436
437
/**
 * @brief Reset the GUI to default values.
 * @details This function checks whether there were any changes to the INI values
 * (if yes, the user can save first or discard or cancel), then clears the whole GUI.
 */
438
void MainWindow::clearGui(const bool &set_default)
439
{
440
	const bool perform_close = closeIni();
441
	if (!perform_close) //user clicked 'cancel'
442
		return;
443
	getControlPanel()->clearGui(set_default);
444
	ini_filename_->setText(QString());
445
	autoload_->setVisible(false);
446
447
}

448
449
450
451
452
/**
 * @brief Store the main window sizes in the settings XML.
 */
void MainWindow::setWindowSizeSettings()
{
453
	setSplitterSizeSettings();
454
455
456
457
458
459
460
461
462
463
464
465
466

	setSetting("auto::sizes::window_" + QString::number(dim::window_type::PREVIEW),
	    "width", QString::number(preview_->width()));
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::PREVIEW),
	    "height", QString::number(preview_->height()));
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::LOGGER),
	    "width", QString::number(logger_.width()));
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::LOGGER),
	    "height", QString::number(logger_.height()));
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::MAIN_WINDOW),
	    "width", QString::number(this->width()));
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::MAIN_WINDOW),
	    "height", QString::number(this->height()));
467
468
469

	//remember the toolbar position:
	setSetting("auto::position::toolbar", "position", QString::number(this->toolBarArea(toolbar_)));
470
471
}

472
473
474
475
476
477
478
479
480
481
/**
 * @brief Remember the main splitter's space distribution.
 */
void MainWindow::setSplitterSizeSettings()
{
	QList<int> splitter_sizes(control_panel_->getSplitterSizes());
	setSetting("auto::sizes::splitter_workflow", "size", QString::number(splitter_sizes.at(0)));
	setSetting("auto::sizes::splitter_mainpanel", "size", QString::number(splitter_sizes.at(1)));
}

482
483
484
485
486
487
/**
 * @brief Create a context menu for the toolbar.
 */
void MainWindow::createToolbarContextMenu()
{
	/* allow user to enable toolbar dragging */
488
	auto *fix_toolbar_position = new QAction(tr("Fix toolbar position"), this);
489
490
491
492
493
	fix_toolbar_position->setCheckable(true);
	fix_toolbar_position->setChecked(getSetting("user::appearance::fix_toolbar_pos", "value") == "TRUE");
	toolbar_context_menu_.addAction(fix_toolbar_position);
}

494
495
496
497
498
499
500
501
502
503
504
505
/**
 * @brief Event handler for the "View::Preview" menu: open the preview window.
 * @details This is public so that the Preview can be shown from other windows
 * (e. g. the Logger).
 */
void MainWindow::viewPreview()
{
	preview_->addIniTab();
	preview_->show();
	preview_->raise();
}

506
507
508
509
510
511
/**
 * @brief Open an XML file containing an application/simulation.
 * @param[in] path Path to the file to open.
 * @param[in] app_name When looking for applications, the application name was parsed and put
 * into a list. This is the app_name so we don't have to parse again.
 * @param[in] fresh If true, reset the GUI before opening a new application.
512
 * @param[in] is_settings_dialog Is it the settings that are being loaded?
513
 */
514
515
void MainWindow::openXml(const QString &path, const QString &app_name, const bool &fresh,
    const bool &is_settings_dialog)
516
{
517
	if (fresh) {
518
		const bool perform_close = closeIni();
519
		if (!perform_close)
520
			return;
521
		control_panel_->closeSettingsTab();
522
		control_panel_->clearGuiElements();
523
		help_loaded_ = false;
524
	}
525
	current_application_ = app_name;
526
527

	if (QFile::exists(path)) {
528
		setStatus(tr("Reading application XML..."), "info", true);
529
		refreshStatus();
530
531
		XMLReader xml;
		QString xml_error = QString();
532
		QString autoload_ini( xml.read(path, xml_error) );
533
534
535
		if (!xml_error.isNull()) {
			xml_error.chop(1); //trailing \n
			Error(tr("Errors occured when parsing the XML configuration file"),
536
			    tr("File: \"") + QDir::toNativeSeparators(path) + "\"", xml_error);
537
		}
538
		setStatus("Building GUI...", "info", true);
539
		buildGui(xml.getXml());
540
		setStatus("Ready.", "info", false);
541
		control_panel_->getWorkflowPanel()->buildWorkflowPanel(xml.getXml());
542
		if (!autoload_ini.isEmpty()) {
543
			if (QFile::exists(autoload_ini))
544
				openIni(autoload_ini);
545
			else
546
547
548
				log(QString("Can not load INI file \"%1\" automatically because it does not exist.").arg(
				    QDir::toNativeSeparators(autoload_ini)), "error");
		}
549
	} else {
550
		topLog(tr("An application or simulation file that has previously been found is now missing. Right-click the list to refresh."),
551
		     "error");
552
		setStatus(tr("File has been removed"), "error");
553
		return;
554
	}
555

556
	//run through all INIs that were saved to be autoloaded and check if it's the application we are opening:
557
	for (auto ini_node = global_xml_settings.firstChildElement().firstChildElement("user").firstChildElement(
558
	    "autoload").firstChildElement("ini");
559
560
561
562
	    !ini_node.isNull(); ini_node = ini_node.nextSiblingElement("ini")) {
		if (ini_node.attribute("application").toLower() == app_name.toLower()) {
			autoload_box_->blockSignals(true); //don't re-save the setting we just fetched
			autoload_box_->setCheckState(Qt::Checked);
563
			autoload_box_->setText(tr("autoload this INI for ") + current_application_);
564
			autoload_box_->blockSignals(false);
565
			openIni(ini_node.text(), true); //TODO: delete if non-existent
566
567
568
			break;
		}
	}
569
570
571
572
573
574
575
576

	toolbar_clear_gui_->setEnabled(true);
	gui_reset_->setEnabled(true);
	gui_clear_->setEnabled(true);
	if (is_settings_dialog) //no real XML - don't enable XML options
		return;
	toolbar_save_ini_as_->setEnabled(true);
	file_save_ini_as_->setEnabled(true);
577
578
	toolbar_open_ini_->setEnabled(true); //toolbar entry
	file_open_ini_->setEnabled(true); //menu entry
579
580
	view_preview_->setEnabled(true);

581
582
583
584
	auto *file_view = getControlPanel()->getWorkflowPanel()->getFilesystemView();
	file_view->setEnabled(true);
	auto *path_label = file_view->getInfoLabel();
	path_label->setText(file_view->getInfoLabel()->property("path").toString());
Mathias Bavay's avatar
Mathias Bavay committed
585
586
	path_label->setWordWrap(true);
	path_label->setStyleSheet("QLabel {font-style: italic}");
Mathias Bavay's avatar
Mathias Bavay committed
587
	QApplication::alert( this ); //notify the user that the task is finished
588
589
}

590
591
592
593
594
595
596
597
598
599
/**
 * @brief Find the panels that control a certain key/value pair.
 * @details This function includes panels that could be created by dynamic panels (Selector etc.).
 * @param[in] parent The parent panel or window to search, since usually we have some information
 * about where to find it. This function is used when iterating through INIParser elements
 * to load the GUI with values.
 * @param[in] section The section to find.
 * @param[in] keyval The key/value pair to find.
 * @return A list of all panels controlling the key/value pair.
 */
600
QWidgetList MainWindow::findPanel(QWidget *parent, const Section &section, const KeyValue &keyval)
601
{
602
	QWidgetList panels;
603
	//first, check if there is a panel for it loaded and available:
604
	panels = findSimplePanel(parent, section, keyval);
605
	int panel_count = parent->findChildren<Atomic *>().count();
606
	//if not found, check if one of our Replicators can create it:
607
	if (panels.isEmpty())
608
		panels = prepareReplicator(parent, section, keyval);
609
610
611
612
613
614
	//if still not found, check if one of our Selectors can create it:
	if (panels.isEmpty())
		panels = prepareSelector(parent, section, keyval);
	//TODO: For the current MeteoIO XML the order is important here so that a TIME filter
	//does not show up twice (because both the normal and time filter's panel fit the pattern).
	//Cleaning up here would also increase performance.
615
	if (parent->findChildren<Atomic *>().count() != panel_count) //new elements were created
616
		return findPanel(parent, section, keyval); //recursion through Replicators that create Selectors that...
Michael Reisecker's avatar
Michael Reisecker committed
617
	return panels; //no more suitable dynamic panels found
618
619
}

620
621
622
623
624
625
626
627
628
/**
 * @brief Find the panels that control a certain key/value pair.
 * @details This function does not include dynamic panels (Selector etc.) and searches only
 * what has already been created.
 * @param[in] parent The parent panel or window to search, because usually we have some information.
 * @param[in] section Section to find.
 * @param[in] keyval Key-value pair to find.
 * @return A list of all panels controlling the key/value pair.
 */
629
QWidgetList MainWindow::findSimplePanel(QWidget *parent, const Section &section, const KeyValue &keyval)
630
631
632
{
	QString id(section.getName() + Cst::sep + keyval.getKey());
	/* simple, not nested, keys */
Michael Reisecker's avatar
Michael Reisecker committed
633
	return parent->findChildren<QWidget *>(Atomic::getQtKey(id));
634
635
}

636
637
638
639
640
641
642
643
644
645
/**
 * @brief Find a Selector in our GUI that could handle a given INI key.
 * @details This function looks for Selectors handling keys like "%::COPY". If one is found,
 * it requests the Selector to create a new panel for the key which will then be available
 * to the parent function that is currently looking for a suitable panel for the INI key.
 * @param[in] parent The parent panel or window to search.
 * @param[in] section Section to find.
 * @param[in] keyval Key/value pair to find.
 * @return A list of panels for the INI key including the possibly newly created ones.
 */
646
QWidgetList MainWindow::prepareSelector(QWidget *parent, const Section &section, const KeyValue &keyval)
647
{
648
	QString id(section.getName() + Cst::sep + keyval.getKey());
649
	//INI key as it would be given in the file, i. e. with the parameters in place instead of the Selector's %:
650
	const QString regex_selector("^" + section.getName() + Cst::sep + R"(([\w\.]+)()" + Cst::sep + R"()(\w+?)([0-9]*$))");
651
652
653
	const QRegularExpression rex(regex_selector);
	const QRegularExpressionMatch matches = rex.match(id);

654
	if (matches.captured(0) == id) { //it could be from a selector with template children
655
656
657
658
659
660
		static const size_t idx_parameter = 1;
		static const size_t idx_keyname = 3;
		static const size_t idx_optional_number = 4;
		const QString parameter(matches.captured(idx_parameter));
		const QString key_name(matches.captured(idx_keyname));
		const QString number(matches.captured(idx_optional_number));
661
		//this would be the ID given in an XML, i. e. with a "%" staing for a parameter:
662
		const QString gui_id = section.getName() + Cst::sep + "%" + Cst::sep + key_name + (number.isEmpty()? "" : "#");
663

664
		//try to find Selector:
Michael Reisecker's avatar
Michael Reisecker committed
665
		QList<Selector *> selector_list = parent->findChildren<Selector *>(Atomic::getQtKey(gui_id));
666
		for (int ii = 0; ii < selector_list.size(); ++ii)
667
			selector_list.at(ii)->setProperty("ini_value", parameter); //cf. notes in prepareReplicator
668
		//now all the right child widgets should exist and we can try to find again:
669
		const QString key_id = section.getName() + Cst::sep + keyval.getKey(); //the one we were originally looking for
670

Michael Reisecker's avatar
Michael Reisecker committed
671
		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
672
	}
673
	return QWidgetList();
674
675
}

676
677
678
679
680
681
682
683
684
685
/**
 * @brief Find a Replicator in our GUI that could handle a given INI key.
 * @details This function looks for Replicators handling keys like "STATION#". If one is found,
 * it requests the Replicator to create a new panel for the key which will then be available
 * to the parent function that is currently looking for a suitable panel for the INI key.
 * @param[in] parent The parent panel or window to search.
 * @param[in] section Section to find.
 * @param[in] keyval Key/value pair to find.
 * @return A list of panels for the INI key including the possibly newly created ones.
 */
686
QWidgetList MainWindow::prepareReplicator(QWidget *parent, const Section &section, const KeyValue &keyval)
687
688
{
	QString id(section.getName() + Cst::sep + keyval.getKey());
689
	//the key how it would look in an INI file:
690
	const QString regex_selector("^" + section.getName() + Cst::sep +
691
	    R"(([\w\.]+)" + Cst::sep + R"()*(\w*?)(?=\d)(\d*)$)");
692
693
694
	const QRegularExpression rex(regex_selector);
	const QRegularExpressionMatch matches = rex.match(id);

695
696
697
698
699
700
701
702
703
704
705
	if (matches.captured(0) == id) { //it's from a replicator with template children
		/*
		 * If we arrive here, we try to find the panel for an INI key that could belong
		 * to the child of a Replicator, e. g. "INPUT::STATION1" or "FILTERS::TA::FILTER1".
		 * We extract the name the Replicator would have (number to "#") and try finding it.
		 */
		static const size_t idx_parameter = 1;
		static const size_t idx_key = 2;
		static const size_t idx_number = 3;
		QString parameter(matches.captured(idx_parameter)); //has trailing "::" if existent
		const QString key(matches.captured(idx_key));
706
		const QString number(matches.captured(idx_number));
707
		//the key how it would look in an XML:
708
709
710
711
712
713
714
715
716
717
718
		QString gui_id = section.getName() + Cst::sep + parameter + key + "#"; //Replicator's name

		/*
		 * A replicator can't normally be accessed via INI keys, because there is no
		 * standalone XML code for it. It always occurs together with child panels,
		 * and those are the ones that will be sought by the INI parser to set values.
		 * Therefore we can use the "ini_value" property listener for a Replicator to
		 * tell it to replicate its panel, thus creating the necessary child panels.
		 */
		QList<Replicator *> replicator_list = parent-> //try to find replicator:
		    findChildren<Replicator *>(Atomic::getQtKey(gui_id));
719
720
		for (int ii = 0; ii < replicator_list.count(); ++ii)
			replicator_list.at(ii)->setProperty("ini_value", number);
721
722
		//now all the right child panels should exist and we can try to find again:
		const QString key_id(section.getName() + Cst::sep + keyval.getKey()); //the one we were originally looking for
Michael Reisecker's avatar
Michael Reisecker committed
723
		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
724
	} //endif has match
725
726

	return QWidgetList(); //no suitable Replicator found
727
728
}

729
730
731
/**
 * @brief Build the main window's menu items.
 */
732
void MainWindow::createMenu()
733
{
734
	/* File menu */
735
	QMenu *menu_file = this->menuBar()->addMenu(tr("&File"));
736
	file_open_ini_ = new QAction(getIcon("document-open"), tr("&Open INI file..."), menu_file);
737
	menu_file->addAction(file_open_ini_); //note that this does not take ownership
738
	connect(file_open_ini_, &QAction::triggered, this, [=]{ toolbarClick("open_ini"); });
739
	file_open_ini_->setShortcut(QKeySequence::Open);
Mathias Bavay's avatar
Mathias Bavay committed
740
	//file_open_ini_->setMenuRole(QAction::ApplicationSpecificRole);
741
	file_save_ini_ = new QAction(getIcon("document-save"), tr("&Save INI file"), menu_file);
742
	file_save_ini_->setShortcut(QKeySequence::Save);
Mathias Bavay's avatar
Mathias Bavay committed
743
	//file_save_ini_->setMenuRole(QAction::ApplicationSpecificRole);
744
745
746
	menu_file->addAction(file_save_ini_);
	file_save_ini_->setEnabled(false); //enable after an INI is opened
	connect(file_save_ini_, &QAction::triggered, this, [=]{ toolbarClick("save_ini"); });
747
	file_save_ini_as_ = new QAction(getIcon("document-save-as"), tr("Save INI file &as..."), menu_file);
748
	file_save_ini_as_->setShortcut(QKeySequence::SaveAs);
Mathias Bavay's avatar
Mathias Bavay committed
749
	//file_save_ini_as_->setMenuRole(QAction::ApplicationSpecificRole);
750
751
752
753
	menu_file->addAction(file_save_ini_as_);
	file_save_ini_as_->setEnabled(false);
	connect(file_save_ini_as_, &QAction::triggered, this, [=]{ toolbarClick("save_ini_as"); });
	menu_file->addSeparator();
754
755
756
757
758
	auto *file_quit_ = new QAction(getIcon("application-exit"), tr("&Exit"), menu_file);
	file_quit_->setShortcut(QKeySequence::Quit);
	file_quit_->setMenuRole(QAction::QuitRole);
	menu_file->addAction(file_quit_);
	connect(file_quit_, &QAction::triggered, this, &MainWindow::quitProgram);
759

760
761
	/* GUI menu */
	QMenu *menu_gui = this->menuBar()->addMenu(tr("&GUI"));
762
	gui_reset_ = new QAction(getIcon("document-revert"), tr("&Reset GUI to default values"), menu_gui);
763
764
765
	menu_gui->addAction(gui_reset_);
	gui_reset_->setEnabled(false);
	gui_reset_->setShortcut(Qt::CTRL + Qt::Key_Backspace);
Mathias Bavay's avatar
Mathias Bavay committed
766
	//gui_reset_->setMenuRole(QAction::ApplicationSpecificRole);
767
	connect(gui_reset_, &QAction::triggered, this, [=]{ toolbarClick("reset_gui"); });
768
	gui_clear_ = new QAction(getIcon("edit-delete"), tr("&Clear GUI"), menu_gui);
769
770
	menu_gui->addAction(gui_clear_);
	gui_clear_->setEnabled(false);
771
	gui_clear_->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Backspace);
Mathias Bavay's avatar
Mathias Bavay committed
772
	//gui_clear_->setMenuRole(QAction::ApplicationSpecificRole);
773
	connect(gui_clear_, &QAction::triggered, this, [=]{ toolbarClick("clear_gui"); });
774
	menu_gui->addSeparator();
775
776
777
	gui_close_all_ = new QAction(getIcon("window-close"), tr("Close all content"), menu_gui);
	menu_gui->addAction(gui_close_all_);
	connect(gui_close_all_, &QAction::triggered, this, &MainWindow::resetGui);
Mathias Bavay's avatar
Mathias Bavay committed
778
	//gui_close_all_->setMenuRole(QAction::ApplicationSpecificRole);
779

780
	/* View menu */
781
	QMenu *menu_view = this->menuBar()->addMenu(tr("&View"));
782
783
784
785
	view_preview_ = new QAction(getIcon("document-print-preview"), tr("P&review"), menu_view);
	menu_view->addAction(view_preview_);
	view_preview_->setShortcut(QKeySequence::Print);
	view_preview_->setEnabled(false); //enable with loaded application
Mathias Bavay's avatar
Mathias Bavay committed
786
	//view_preview->setMenuRole(QAction::ApplicationSpecificRole);
787
	connect(view_preview_, &QAction::triggered, this, &MainWindow::viewPreview);
788
	auto *view_log = new QAction(getIcon("utilities-system-monitor"), tr("&Log"), menu_view);
789
	menu_view->addAction(view_log);
790
	connect(view_log, &QAction::triggered, this, &MainWindow::viewLogger);
791
	view_log->setShortcut(Qt::CTRL + Qt::Key_L);
Mathias Bavay's avatar
Mathias Bavay committed
792
	//view_log->setMenuRole(QAction::ApplicationSpecificRole);
793
794
795
796
797
798
	auto *view_refresh = new QAction(tr("&Refresh Applications"), menu_view);
	menu_view->addAction(view_refresh);
	connect(view_refresh, &QAction::triggered, this,
	    [=]{ control_panel_->getWorkflowPanel()->scanFoldersForApps(); });
	view_refresh->setShortcut(QKeySequence::Refresh);

799
	menu_view->addSeparator();
800
	auto *view_settings = new QAction(getIcon("preferences-system"), tr("&Settings"), menu_view);
Mathias Bavay's avatar
Mathias Bavay committed
801
	view_settings->setMenuRole(QAction::PreferencesRole);
802
	menu_view->addAction(view_settings);
803
	connect(view_settings, &QAction::triggered, this, &MainWindow::viewSettings);
804
805
	view_close_settings_ = new QAction(tr("Close settings"), menu_view);
	view_close_settings_->setEnabled(false);
Mathias Bavay's avatar
Mathias Bavay committed
806
	//view_close_settings_->setMenuRole(QAction::ApplicationSpecificRole);
807
808
	menu_view->addAction(view_close_settings_);
	connect(view_close_settings_, &QAction::triggered, this, &MainWindow::closeSettings);
809
810
811
812
813
814
815
#if defined Q_OS_MAC //only Mac has this as of now: https://doc.qt.io/archives/qt-4.8/qkeysequence.html
//equivalent at runtime: if (QKeySequence(QKeySequence::Preferences).toString(QKeySequence::NativeText).isEmpty())
	view_settings->setShortcut(QKeySequence::Preferences);
	view_close_settings_->setShortcut(QKeySequence(
	    "Shift+" + QKeySequence(QKeySequence::Preferences).toString(QKeySequence::NativeText)));
#else
	view_settings->setShortcut(Qt::Key_F3);
816
	view_close_settings_->setShortcut(Qt::SHIFT + Qt::Key_F3);
817
#endif
818
819
	/* Window menu */
	QMenu *menu_window = this->menuBar()->addMenu(tr("&Window"));
820
	auto *window_show_workflow = new QAction(tr("Show wor&kflow"), menu_window);
821
822
	menu_window->addAction(window_show_workflow);
	connect(window_show_workflow, &QAction::triggered, this, &MainWindow::showWorkflow);
823
	window_show_workflow->setShortcut(Qt::CTRL + Qt::Key_K);
Mathias Bavay's avatar
Mathias Bavay committed
824
	//window_show_workflow->setMenuRole(QAction::ApplicationSpecificRole);
825
	auto *window_hide_workflow = new QAction(tr("&Hide workflow"), menu_window);
826
827
	menu_window->addAction(window_hide_workflow);
	connect(window_hide_workflow, &QAction::triggered, this, &MainWindow::hideWorkflow);
828
	window_hide_workflow->setShortcut(Qt::CTRL + Qt::Key_H);
Mathias Bavay's avatar
Mathias Bavay committed
829
	//window_hide_workflow->setMenuRole(QAction::ApplicationSpecificRole);
830

Michael Reisecker's avatar
Michael Reisecker committed
831
832
833
#ifdef DEBUG
	/* Debug menu */
	QMenu *menu_debug = this->menuBar()->addMenu("&Debug");
834
	auto *debug_run = new QAction("&Run action", menu_debug); //to run arbitrary code with a click
Michael Reisecker's avatar
Michael Reisecker committed
835
	menu_debug->addAction(debug_run);
836
	connect(debug_run, &QAction::triggered, this, &MainWindow::z_onDebugRunClick);
Michael Reisecker's avatar
Michael Reisecker committed
837
838
#endif //def DEBUG

Michael Reisecker's avatar
Michael Reisecker committed
839
	/* Help menu */
Michael Reisecker's avatar
Michael Reisecker committed
840
	auto *menu_help_main = new QMenuBar(this->menuBar());
841
	QMenu *menu_help = menu_help_main->addMenu(tr("&Help"));
842
	auto *help = new QAction(getIcon("help-contents"), tr("&Help"), menu_help_main);
843
	help->setShortcut(QKeySequence::HelpContents);
844
	help->setMenuRole(QAction::ApplicationSpecificRole);
845
846
	menu_help->addAction(help);
	connect(help, &QAction::triggered, this, &MainWindow::loadHelp);
847
	auto *help_about = new QAction(getIcon("help-about"), tr("&About"), menu_help_main);
Mathias Bavay's avatar
Mathias Bavay committed
848
	help_about->setMenuRole(QAction::AboutRole);
849
850
	menu_help->addAction(help_about);
	connect(help_about, &QAction::triggered, this, &MainWindow::helpAbout);
851
	help_about->setShortcut(QKeySequence::WhatsThis);
852
	menu_help->addSeparator();
853
	auto *help_dev = new QAction(tr("&Developer's help"), menu_help_main);
854
	help_dev->setShortcut(Qt::Key_F2);
855
	help_dev->setMenuRole(QAction::ApplicationSpecificRole);
856
857
	menu_help->addAction(help_dev);
	connect(help_dev, &QAction::triggered, this, &MainWindow::loadHelpDev);
858
	auto *help_bugreport = new QAction(tr("File &bug report..."), menu_help_main);
859
	help_bugreport->setMenuRole(QAction::ApplicationSpecificRole);
860
861
	menu_help->addAction(help_bugreport);
	connect(help_bugreport, &QAction::triggered,
862
	    []{ QDesktopServices::openUrl(QUrl("https://models.slf.ch/p/inishell-ng/issues/")); });
863

864
#if !defined Q_OS_MAC
865
	this->menuBar()->setCornerWidget(menu_help_main); //push help menu to the right
866
#endif
867
868
}

869
870
871
/**
 * @brief Create the main window's toolbar.
 */
872
void MainWindow::createToolbar()
873
874
{
	/* toolbar */