WSL/SLF GitLab Repository

MainWindow.cc 51.9 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/Atomic.h"
27
#include "src/gui_elements/Group.h" //to exclude Groups from panel search
28
#include "src/main/colors.h"
29
#include "src/main/common.h"
30
#include "src/main/constants.h"
31
32
33
#include "src/main/Error.h"
#include "src/main/dimensions.h"
#include "src/main/INIParser.h"
34
#include "src/main/inishell.h"
35
#include "src/main/settings.h"
36
#include "src/main/XMLReader.h"
37
38

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

54
55
#include <QMovie>

56
57
58
#ifdef DEBUG
	#include <iostream>
#endif
59

60
61
62
63
64
65
66
/**
 * @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.
 */
67
68
69
bool MouseEventFilter::eventFilter(QObject *object, QEvent *event)
{
	if (event->type() == QEvent::MouseButtonPress) {
70
		if (object->property("mouseclick") == "open_log") {
71
			getMainWindow()->viewLogger();
72
73
74
75
		} else if (object->property("mouseclick") == "open_ini") {
			if (!getMainWindow()->ini_filename_->text().isEmpty())
				QDesktopServices::openUrl("file:" + getMainWindow()->ini_filename_->text());
		}
76
	}
77
	return QObject::eventFilter(object, event); //pass to actual event of the object
78
79
}

80
81
82
/**
 * @class MainWindow
 * @brief Constructor for the main window.
83
84
 * @param[in] settings_location Path to the INIshell settings file.
 * @param[in] errors List of errors that have occurred before starting the GUI.
85
86
 * @param[in] parent Unused since the main window is kept in scope by the entry point function.
 */
87
88
MainWindow::MainWindow(QString &settings_location, const QStringList &errors, QMainWindow *parent)
    : QMainWindow(parent), logger_(this), xml_settings_filename_(settings_location)
89
{
90
91
92
	/*
	 * Program flowchart sketch:
	 *
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
	 *   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                   ----------------------------------------
110
	 *   _________________________________
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
	 *   |  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)
	 *            ---------------
126
	 *                  |
127
128
129
	 *                  v
	 *          Set INI values from:
	 *          --------------------
130
	 *
131
	 *         XML              INI  <----- INIParser class
132
133
134
135
136
	 *   (default values)  (file system)                      VALIDATION
	 *          |                |                           (set styles)
	 *          |                |                                ^
	 *          v                v                               /
	 *    _____________________________________                 /
137
138
	 *    |       Panel's onPropertySet       |  -----> CHECK VALUE
	 *    -------------------------------------           |
139
	 *          ^                ^        (Property "no_ini" not set? Value not empty?)
140
141
142
143
144
145
146
	 *          |                |                        |
	 *          |                |                        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                                   |
147
	 *   (when closing)  <----- INIParser::operator==     v
148
	 *                                               Output to file  <----- INIParser class
149
	 */
150

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

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

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

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

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

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

193
194
195
196
197
198
199
/**
 * @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)
200
{
201
	setUpdatesEnabled(false); //disable painting until done
202
	QDomNode root = xml.firstChild();
203
204
205
206
207
208
209
210
	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();
	}
211
	setUpdatesEnabled(true);
212
213
}

214
215
216
217
218
/**
 * @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.
 */
219
220
QList<Atomic *> MainWindow::getPanelsForKey(const QString &ini_key)
{
221
	QList<Atomic *> panel_list( control_panel_->findChildren<Atomic *>(Atomic::getQtKey(ini_key)) );
222
	for (auto it = panel_list.begin(); it != panel_list.end(); ++it) {
223
224
		//groups don't count towards finding INI keys (for this reason, they additionally
		//have the "no_ini" key set):
225
		if (qobject_cast<Group *>(*it))
226
			panel_list.erase(it);
227
228
229
230
	}
	return panel_list;
}

231
232
233
234
235
236
/**
 * @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.
 */
237
void MainWindow::saveIni(const QString &filename)
238
{
239
240
241
242
243
244
	/*
	 * 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
245
	const QString missing( control_panel_->setIniValuesFromGui(&gui_ini) );
246

247
	if (!missing.isEmpty()) {
248
249
250
		QMessageBox msgMissing;
		msgMissing.setWindowTitle("Warning ~ " + QCoreApplication::applicationName());
		msgMissing.setText(tr("<b>Missing mandatory INI values.</b>"));
251
252
		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."));
253
		msgMissing.setDetailedText(tr("Missing INI keys:\n") + missing);
254
255
256
257
258
259
		msgMissing.setIcon(QMessageBox::Warning);
		msgMissing.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel);
		msgMissing.setDefaultButton(QMessageBox::Cancel);
		int clicked = msgMissing.exec();
		if (clicked == QMessageBox::Cancel)
			return;
260
	}
261
	//if no file is specified we save to the currently open INI file (save vs. save as):
262
263
264
	gui_ini.writeIni(filename.isEmpty()? gui_ini.getFilename() : filename);
}

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

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

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

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

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

	const QFileInfo finfo(path);
	setSetting("auto::history::last_ini", "path", finfo.absoluteDir().path());
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
347
348
349
/**
 * @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;
}

350
351
352
353
354
/**
 * @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?
 */
355
void MainWindow::openIni(const QString &path, const bool &is_autoopen, const bool &fresh)
356
{
357
	this->getControlPanel()->getWorkflowPanel()->setEnabled(false); //hint at INIshell processing...
358
	setStatus(tr("Reading INI file..."), "info", true);
359
	refreshStatus(); //necessary if heavy operations follow
360
361
	if (fresh)
		clearGui();
362
	const bool success = ini_.parseFile(path); //load the file into the main INI parser
363
364
365
366
	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")),
367
		    (success? "info" : "warning")); //ill-formatted lines in INI file
368
	toolbar_save_ini_->setEnabled(true);
369
	file_save_ini_->setEnabled(true);
370
	autoload_->setVisible(true);
371
	ini_filename_->setText(path);
372
	autoload_box_->setText(tr("autoload this INI for ") + current_application_);
373
	if (!is_autoopen) //when a user clicks an INI file to open it we ask anew whether to autoopen
374
		autoload_box_->setCheckState(Qt::Unchecked);
375

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

380
381
382
383
384
385
/**
 * @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.
 */
386
bool MainWindow::closeIni()
387
{
388
	if (!help_loaded_ && //unless user currently has the help opened which we always allow to close
389
	    getSetting("user::inireader::warn_unsaved_ini", "value") == "TRUE") {
390
391
392
393
394
395
396
		/*
		 * 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.
		 */
397
398
399
400
401
402
		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>"));
403
			msgNotSaved.setInformativeText(tr(
404
			    "Some INI keys will be lost if you don't save the current INI file."));
405
			msgNotSaved.setDetailedText(ini_.getEqualityCheckMsg());
406
407
408
			msgNotSaved.setIcon(QMessageBox::Warning);
			msgNotSaved.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel | QMessageBox::Discard);
			msgNotSaved.setDefaultButton(QMessageBox::Cancel);
409

410
			auto *show_msg_again = new QCheckBox(tr("Don't show this warning again"));
411
412
413
414
415
416
417
418
			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");
			});

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

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

436
437
438
439
440
/**
 * @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.
 */
441
void MainWindow::clearGui(const bool &set_default)
442
{
443
	const bool perform_close = closeIni();
444
	if (!perform_close) //user clicked 'cancel'
445
		return;
446
	getControlPanel()->clearGui(set_default);
447
	ini_filename_->setText(QString());
448
	autoload_->setVisible(false);
449
450
}

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

	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()));
470
471
472

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

475
476
477
478
479
480
481
482
483
484
/**
 * @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)));
}

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

497
498
499
500
501
502
503
/**
 * @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()
{
504
505
506
507
508
	if (view_preview_->isEnabled()) {
		preview_->addIniTab();
		preview_->show();
		preview_->raise();
	}
509
510
}

511
512
513
/**
 * @brief Load the user help XML from the resources onto the GUI.
 */
514
void MainWindow::loadHelp(const QString &tab_name, const QString &frame_name)
515
516
517
518
{
	clearGui();
	openXml(":doc/help.xml", "Help");
	help_loaded_ = true;
519
520
521
522

	if (tab_name.isEmpty())
		return;

523
524
525
526
527
	const bool success = control_panel_->showTab(tab_name);
#ifdef DEBUG
	if (!success)
		qDebug() << "Help section does not exist:" << tab_name;
#endif //def DEBUG
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548

	auto parent = control_panel_->getSectionScrollarea(tab_name);
	const QList<Group *> panel_list( parent->findChildren<Group *>() );
	for (auto &panel : panel_list) { //run through panels in section
		QString section, key;
		const QString value( panel->getIniValue(section, key) );
		if (QString::compare(key, frame_name, Qt::CaseInsensitive) == 0) {
			const QString id( section + Cst::sep + key );
			//this ID must be set in the help XML:
			auto wid = getMainWindow()->findChild<QGroupBox *>("_primary_" + Atomic::getQtKey(id));
			QString stylesheet(wid->styleSheet()); //original (to reset with timer)
			QString stylesheet_copy( stylesheet );
			wid->setStyleSheet(stylesheet_copy.replace( //flash frame border color
			    colors::getQColor("frameborder").name(QColor::HexRgb).toLower(),
			    colors::getQColor("important").name()));
			QTimer::singleShot(Cst::msg_short_length, this,
			    [=]{ wid->setStyleSheet(stylesheet); } );
		}
	} //endfor panel_list
	this->raise();
} //TODO: scroll down to section; for now, tend to put frames that can flash to the top
549

550
551
552
553
554
555
/**
 * @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.
556
 * @param[in] is_settings_dialog Is it the settings that are being loaded?
557
 */
558
559
void MainWindow::openXml(const QString &path, const QString &app_name, const bool &fresh,
    const bool &is_settings_dialog)
560
{
561
	if (fresh) {
562
		const bool perform_close = closeIni();
563
		if (!perform_close)
564
			return;
565
		control_panel_->closeSettingsTab();
566
		control_panel_->clearGuiElements();
567
		help_loaded_ = false;
568
	}
569
	current_application_ = app_name;
570
571

	if (QFile::exists(path)) {
572
		setStatus(tr("Reading application XML..."), "info", true);
573
		refreshStatus();
574
575
		XMLReader xml;
		QString xml_error = QString();
576
		QString autoload_ini( xml.read(path, xml_error) );
577
578
579
		if (!xml_error.isNull()) {
			xml_error.chop(1); //trailing \n
			Error(tr("Errors occured when parsing the XML configuration file"),
580
			    tr("File: \"") + QDir::toNativeSeparators(path) + "\"", xml_error);
581
		}
582
		setStatus("Building GUI...", "info", true);
583
		buildGui(xml.getXml());
584
		setStatus("Ready.", "info", false);
585
		control_panel_->getWorkflowPanel()->buildWorkflowPanel(xml.getXml());
586
		if (!autoload_ini.isEmpty()) {
587
			if (QFile::exists(autoload_ini))
588
				openIni(autoload_ini);
589
			else
590
591
592
				log(QString("Can not load INI file \"%1\" automatically because it does not exist.").arg(
				    QDir::toNativeSeparators(autoload_ini)), "error");
		}
593
	} else {
594
		topLog(tr("An application or simulation file that has previously been found is now missing. Right-click the list to refresh."),
595
		     "error");
596
		setStatus(tr("File has been removed"), "error");
597
		return;
598
	}
599

600
	//run through all INIs that were saved to be autoloaded and check if it's the application we are opening:
601
	for (auto ini_node = global_xml_settings.firstChildElement().firstChildElement("user").firstChildElement(
602
	    "autoload").firstChildElement("ini");
603
604
605
606
	    !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);
607
			autoload_box_->setText(tr("autoload this INI for ") + current_application_);
608
			autoload_box_->blockSignals(false);
609
			openIni(ini_node.text(), true); //TODO: delete if non-existent
610
611
612
			break;
		}
	}
613
614
615
616
617
618
619
620

	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);
621
622
	toolbar_open_ini_->setEnabled(true); //toolbar entry
	file_open_ini_->setEnabled(true); //menu entry
623
624
	view_preview_->setEnabled(true);

625
626
627
628
	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
629
630
	path_label->setWordWrap(true);
	path_label->setStyleSheet("QLabel {font-style: italic}");
Mathias Bavay's avatar
Mathias Bavay committed
631
	QApplication::alert( this ); //notify the user that the task is finished
632
633
}

634
635
636
637
638
639
640
641
642
643
/**
 * @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.
 */
644
QWidgetList MainWindow::findPanel(QWidget *parent, const Section &section, const KeyValue &keyval)
645
{
646
	QWidgetList panels;
647
	//first, check if there is a panel for it loaded and available:
648
	panels = findSimplePanel(parent, section, keyval);
649
	int panel_count = parent->findChildren<Atomic *>().count();
650
	//if not found, check if one of our Replicators can create it:
651
	if (panels.isEmpty())
652
		panels = prepareReplicator(parent, section, keyval);
653
654
655
656
657
658
	//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.
659
	if (parent->findChildren<Atomic *>().count() != panel_count) //new elements were created
660
		return findPanel(parent, section, keyval); //recursion through Replicators that create Selectors that...
Michael Reisecker's avatar
Michael Reisecker committed
661
	return panels; //no more suitable dynamic panels found
662
663
}

664
665
666
667
668
669
670
671
672
/**
 * @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.
 */
673
QWidgetList MainWindow::findSimplePanel(QWidget *parent, const Section &section, const KeyValue &keyval)
674
675
676
{
	QString id(section.getName() + Cst::sep + keyval.getKey());
	/* simple, not nested, keys */
Michael Reisecker's avatar
Michael Reisecker committed
677
	return parent->findChildren<QWidget *>(Atomic::getQtKey(id));
678
679
}

680
681
682
683
684
685
686
687
688
689
/**
 * @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.
 */
690
QWidgetList MainWindow::prepareSelector(QWidget *parent, const Section &section, const KeyValue &keyval)
691
{
692
	QString id(section.getName() + Cst::sep + keyval.getKey());
693
	//INI key as it would be given in the file, i. e. with the parameters in place instead of the Selector's %:
694
	const QString regex_selector("^" + section.getName() + Cst::sep + R"(([\w\.]+)()" + Cst::sep + R"()(\w+?)([0-9]*$))");
695
696
697
	const QRegularExpression rex(regex_selector);
	const QRegularExpressionMatch matches = rex.match(id);

698
	if (matches.captured(0) == id) { //it could be from a selector with template children
699
700
701
702
703
704
		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));
705
		//this would be the ID given in an XML, i. e. with a "%" staing for a parameter:
706
		const QString gui_id = section.getName() + Cst::sep + "%" + Cst::sep + key_name + (number.isEmpty()? "" : "#");
707

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

Michael Reisecker's avatar
Michael Reisecker committed
715
		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
716
	}
717
	return QWidgetList();
718
719
}

720
721
722
723
724
725
726
727
728
729
/**
 * @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.
 */
730
QWidgetList MainWindow::prepareReplicator(QWidget *parent, const Section &section, const KeyValue &keyval)
731
732
{
	QString id(section.getName() + Cst::sep + keyval.getKey());
733
	//the key how it would look in an INI file:
734
	const QString regex_selector("^" + section.getName() + Cst::sep +
735
	    R"(([\w\.]+)" + Cst::sep + R"()*(\w*?)(?=\d)(\d*)$)");
736
737
738
	const QRegularExpression rex(regex_selector);
	const QRegularExpressionMatch matches = rex.match(id);

739
740
741
742
743
744
745
746
747
748
749
	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));
750
		const QString number(matches.captured(idx_number));
751
		//the key how it would look in an XML:
752
753
754
755
756
757
758
759
760
761
762
		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));
763
764
		for (int ii = 0; ii < replicator_list.count(); ++ii)
			replicator_list.at(ii)->setProperty("ini_value", number);
765
766
		//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
767
		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
768
	} //endif has match
769
770

	return QWidgetList(); //no suitable Replicator found
771
772
}

773
774
775
/**
 * @brief Build the main window's menu items.
 */
776
void MainWindow::createMenu()
777
{
778
	/* FILE menu */
779
	QMenu *menu_file = this->menuBar()->addMenu(tr("&File"));
780
	file_open_ini_ = new QAction(getIcon("document-open"), tr("&Open INI file..."), menu_file);
781
	menu_file->addAction(file_open_ini_); //note that this does not take ownership
782
	connect(file_open_ini_, &QAction::triggered, this, [=]{ toolbarClick("open_ini"); });
783
	file_open_ini_->setShortcut(QKeySequence::Open);
Mathias Bavay's avatar
Mathias Bavay committed
784
	//file_open_ini_->setMenuRole(QAction::ApplicationSpecificRole);
785
	file_save_ini_ = new QAction(getIcon("document-save"), tr("&Save INI file"), menu_file);
786
	file_save_ini_->setShortcut(QKeySequence::Save);
Mathias Bavay's avatar
Mathias Bavay committed
787
	//file_save_ini_->setMenuRole(QAction::ApplicationSpecificRole);
788
789
790
	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"); });
791
	file_save_ini_as_ = new QAction(getIcon("document-save-as"), tr("Save INI file &as..."), menu_file);
792
	file_save_ini_as_->setShortcut(QKeySequence::SaveAs);
Mathias Bavay's avatar
Mathias Bavay committed
793
	//file_save_ini_as_->setMenuRole(QAction::ApplicationSpecificRole);
794
795
796
797
	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();
798
799
800
801
802
	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);
803

804
805
	/* GUI menu */
	QMenu *menu_gui = this->menuBar()->addMenu(tr("&GUI"));
806
	gui_reset_ = new QAction(getIcon("document-revert"), tr("&Reset GUI to default values"), menu_gui);
807
808
809
	menu_gui->addAction(gui_reset_);
	gui_reset_->setEnabled(false);
	gui_reset_->setShortcut(Qt::CTRL + Qt::Key_Backspace);
Mathias Bavay's avatar
Mathias Bavay committed
810
	//gui_reset_->setMenuRole(QAction::ApplicationSpecificRole);
811
	connect(gui_reset_, &QAction::triggered, this, [=]{ toolbarClick("reset_gui"); });
812
	gui_clear_ = new QAction(getIcon("edit-delete"), tr("&Clear GUI"), menu_gui);
813
814
	menu_gui->addAction(gui_clear_);
	gui_clear_->setEnabled(false);
815
	gui_clear_->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Backspace);
Mathias Bavay's avatar
Mathias Bavay committed
816
	//gui_clear_->setMenuRole(QAction::ApplicationSpecificRole);
817
	connect(gui_clear_, &QAction::triggered, this, [=]{ toolbarClick("clear_gui"); });
818
	menu_gui->addSeparator();
819
820
821
	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
822
	//gui_close_all_->setMenuRole(QAction::ApplicationSpecificRole);
823

824
	/* VIEW menu */
825
	QMenu *menu_view = this->menuBar()->addMenu(tr("&View"));
826
827
828
829
	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
830
	//view_preview->setMenuRole(QAction::ApplicationSpecificRole);
831
	connect(view_preview_, &QAction::triggered, this, &MainWindow::viewPreview);
832
	auto *view_log = new QAction(getIcon("utilities-system-monitor"), tr("&Log"), menu_view);
833
	menu_view->addAction(view_log);
834
	connect(view_log, &QAction::triggered, this, &MainWindow::viewLogger);
835
	view_log->setShortcut(Qt::CTRL + Qt::Key_L);
Mathias Bavay's avatar
Mathias Bavay committed
836
	//view_log->setMenuRole(QAction::ApplicationSpecificRole);
837
838
839
840
841
842
	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);

843
	menu_view->addSeparator();
844
	auto *view_settings = new QAction(getIcon("preferences-system"), tr("&Settings"), menu_view);
Mathias Bavay's avatar
Mathias Bavay committed
845
	view_settings->setMenuRole(QAction::PreferencesRole);
846
	menu_view->addAction(view_settings);
847
	connect(view_settings, &QAction::triggered, this, &MainWindow::viewSettings);
848
849
	view_close_settings_ = new QAction(tr("Close settings"), menu_view);
	view_close_settings_->setEnabled(false);
Mathias Bavay's avatar
Mathias Bavay committed
850
	//view_close_settings_->setMenuRole(QAction::ApplicationSpecificRole);
851
852
	menu_view->addAction(view_close_settings_);
	connect(view_close_settings_, &QAction::triggered, this, &MainWindow::closeSettings);
853
854
855
856
857
858
859
#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);
860
	view_close_settings_->setShortcut(Qt::SHIFT + Qt::Key_F3);
861
#endif
862
	/* WINDOW menu */
863
	QMenu *menu_window = this->menuBar()->addMenu(tr("&Window"));
864
	menu_window->addSeparator();
865
	auto *window_show_workflow = new QAction(tr("Show wor&kflow"), menu_window);
866
867
	menu_window->addAction(window_show_workflow);
	connect(window_show_workflow, &QAction::triggered, this, &MainWindow::showWorkflow);
868
	window_show_workflow->setShortcut(Qt::CTRL + Qt::Key_K);
Mathias Bavay's avatar
Mathias Bavay committed
869
	//window_show_workflow->setMenuRole(QAction::ApplicationSpecificRole);
870
	auto *window_hide_workflow = new QAction(tr("&Hide workflow"), menu_window);
871
872
	menu_window->addAction(window_hide_workflow);
	connect(window_hide_workflow, &QAction::triggered, this, &MainWindow::hideWorkflow);
873
	window_hide_workflow->setShortcut(Qt::CTRL + Qt::Key_H);
Mathias Bavay's avatar
Mathias Bavay committed
874
	//window_hide_workflow->setMenuRole(QAction::ApplicationSpecificRole);
875

Michael Reisecker's avatar
Michael Reisecker committed
876
#ifdef DEBUG
877
	/* DEBUG menu */
Michael Reisecker's avatar
Michael Reisecker committed
878
	QMenu *menu_debug = this->menuBar()->addMenu("&Debug");
879
	auto *debug_run = new QAction("&Run action", menu_debug); //to run arbitrary code with a click
Michael Reisecker's avatar
Michael Reisecker committed
880
	menu_debug->addAction(debug_run);
881
	connect(debug_run, &QAction::triggered, this, &MainWindow::z_onDebugRunClick);
Michael Reisecker's avatar
Michael Reisecker committed
882
883
#endif //def DEBUG

884
	/* HELP menu */
Michael Reisecker's avatar
Michael Reisecker committed
885
	auto *menu_help_main = new QMenuBar(this->menuBar());
886
	QMenu *menu_help = menu_help_main->addMenu(tr("&Help"));
887
	auto *help = new QAction(getIcon("help-contents"), tr("&Help"), menu_help_main);
888
	help->setShortcut(QKeySequence::HelpContents);
889
	help->setMenuRole(QAction::ApplicationSpecificRole);
890
	menu_help->addAction(help);
891
	connect(help, &QAction::triggered, this