WSL/SLF GitLab Repository

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

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

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

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
	 *   |  Build GUI elements from XML  |  <----- Scan file system for application XMLs
	 *   ---------------------------------           (read and check: XMLParser class)
	 *                  |                                  ^
114
	 *         Find panels in XML  <----- inishell.cc      |_ WorkflowPanel::scanFoldersForApps()
115
	 *                  |
116
	 *              Build panels  <----- panels.cc ----->  The panels' constructors
117
118
119
	 *                  |
	 *        Set panels' properties  <----- the panels' setOptions() member functions
	 *                  |
120
	 * Give panels IDs to interact with INI changes  <----- Atomic::setEmphasisWidget()
121
122
123
124
125
	 *                  |                                     ^
	 *                  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
	 *    |      Panel's onPropertySet()      |  -----> CHECK VALUE
138
	 *    -------------------------------------           |
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
	/* create basic GUI items */
160
	this->setUnifiedTitleAndToolBarOnMac(true);
161
	this->setWindowTitle(QCoreApplication::applicationName());
162
163
	createMenu();
	createToolbar();
164
	createStatusbar();
165
166

	/* create and size child windows */
167
	preview_ = new PreviewWindow(this);
168
	settings_window_ = new SettingsWindow(this);
Michael Reisecker's avatar
Michael Reisecker committed
169
	help_window_ = new HelpWindow(this);
170
	sizeAllWindows();
Michael Reisecker's avatar
Michael Reisecker committed
171

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

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

194
195
196
197
198
/**
 * @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.
 */
199
200
QList<Atomic *> MainWindow::getPanelsForKey(const QString &ini_key)
{
Michael Reisecker's avatar
Michael Reisecker committed
201
	QList<Atomic *> panel_list( control_panel_->getSectionTab()->findChildren<Atomic *>(Atomic::getQtKey(ini_key)) );
202
	for (auto it = panel_list.begin(); it != panel_list.end(); ++it) {
203
204
		//groups don't count towards finding INI keys (for this reason, they additionally
		//have the "no_ini" key set):
205
		if (qobject_cast<Group *>(*it))
206
			panel_list.erase(it);
207
208
209
210
	}
	return panel_list;
}

211
212
213
214
215
216
/**
 * @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.
 */
217
bool MainWindow::saveIni(const QString &filename)
218
{
219
220
221
222
223
224
	/*
	 * 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
225
	gui_ini.clear(true);  //only keep unknown keys (which are transported from input to output)
226
	const QString missing( control_panel_->getSectionTab()->setIniValuesFromGui(&gui_ini) );
227

228
	if (!missing.isEmpty()) {
229
230
231
		QMessageBox msgMissing;
		msgMissing.setWindowTitle("Warning ~ " + QCoreApplication::applicationName());
		msgMissing.setText(tr("<b>Missing mandatory INI values.</b>"));
232
233
		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."));
234
		msgMissing.setDetailedText(tr("Missing INI keys:\n") + missing);
235
236
237
238
		msgMissing.setIcon(QMessageBox::Warning);
		msgMissing.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel);
		msgMissing.setDefaultButton(QMessageBox::Cancel);
		int clicked = msgMissing.exec();
239
240
		if (clicked == QMessageBox::Cancel) {
			const QStringList first_missing( missing.split(",\n").at(0).split("::") );
Michael Reisecker's avatar
Michael Reisecker committed
241
			control_panel_->getSectionTab()->showPanel(first_missing.at(0), first_missing.at(1));
242
			return false;
243
		}
244
	}
245
	//if no file is specified we save to the currently open INI file (save vs. save as):
246
247
248
	const QString new_filename( filename.isEmpty()? gui_ini.getFilename() : filename );
	gui_ini.writeIni(new_filename);
	setStatus(tr("Saved to ") + new_filename, "info");
249
	return true;
250
251
}

252
253
254
/**
 * @brief Save the currently set values to a new INI file.
 */
255
256
void MainWindow::saveIniAs()
{
257
258
259
	QString start_path( getSetting("auto::history::last_ini", "path") );
	if (start_path.isEmpty())
		start_path = getSetting("auto::history::last_preview_write", "path");
260
261
262
	if (start_path.isEmpty())
		start_path = QDir::currentPath();

Mathias Bavay's avatar
Mathias Bavay committed
263
	const QString filename( QFileDialog::getSaveFileName(this, tr("Save INI file"),
264
	    start_path + "/" + ini_filename_->text(),
Mathias Bavay's avatar
Mathias Bavay committed
265
	    "INI files (*.ini *.INI);;All files (*)", nullptr, QFileDialog::DontUseNativeDialog) );
266
267
268
269
	if (filename.isNull()) //user cancelled out from the file picker
		return;
	bool perform_close = saveIni(filename);
	if (!perform_close) //user cancelled out from the "unsaved changes" warning
270
		return;
271
	ini_.setFilename(filename); //the new file is the new current file like in all programs
272
	ini_filename_->setText(filename);
273
	autoload_->setVisible(true); //if started from an empty GUI, this could still be disabled
274
275
	toolbar_save_ini_->setEnabled(true); //toolbar entry
	file_save_ini_->setEnabled(true); //menu entry
276

277
	setSetting("auto::history::last_ini", "path", QFileInfo( filename ).absoluteDir().path());
278
279
}

280
281
282
/**
 * @brief Select a path for an INI file to be opened, and then open it.
 */
283
void MainWindow::openIni()
284
{
285
	QString start_path( getSetting("auto::history::last_ini", "path") );
286
287
288
	if (start_path.isEmpty())
		start_path = QDir::currentPath();

Mathias Bavay's avatar
Mathias Bavay committed
289
	const QString path( QFileDialog::getOpenFileName(this, tr("Open INI file"), start_path,
290
	    "INI files (*.ini);;All files (*)", nullptr,
291
		QFileDialog::DontUseNativeDialog | QFileDialog::DontConfirmOverwrite) );
292
	if (path.isNull()) //cancelled
293
294
		return;
	openIni(path);
295

296
	setSetting("auto::history::last_ini", "path", QFileInfo( path ).absoluteDir().path());
297
298
}

299
300
/**
 * @brief Set the loaded panels' values from an INI file.
301
302
 * @param[in,out] ini The INI file in form of an INIParser (usually the main one).
 * This object may be modified when keys are recognized to be unknown to the application.
303
304
 * @return True if all INI keys are known to the loaded XML.
 */
305
bool MainWindow::setGuiFromIni(INIParser &ini)
306
307
308
{
	bool all_ok = true;
	bool first_error_message = true;
309
	for (auto &sec : *ini.getSections()) { //run through sections in INI file
310

311
		ScrollPanel *tab_scroll = getControlPanel()->getSectionTab()->getSectionScrollArea(sec.getName(),
312
		    QString(), QString(), true); //get the corresponding tab of our GUI
313
		if (tab_scroll == nullptr) //check if a dynamic section exists that can create it
314
			tab_scroll = getControlPanel()->getSectionTab()->tryDynamicSection(sec.getName());
315

316
317
318
319
320
321
322
323
324
325
		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 {
326
					sec[ii]->setIsUnknownToApp();
327
328
329
330
331
332
333
					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
334
335
336
			auto kv_list( sec.getKeyValueList() );
			for (size_t ii = 0; ii < kv_list.size(); ++ii) //mark all keys in unknown section as unknown
				sec[ii]->setIsUnknownToApp();
337
338
339
340
341
			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
Michael Reisecker's avatar
Michael Reisecker committed
342

343
344
345
346
	} //endfor sec
	return all_ok;
}

347
/**
348
 * @brief Apply changed appearance settings to the main window..
349
 */
350
void MainWindow::resetAppearance(const bool &restart)
351
{
352
353
354
355
356
357
	sizeAllWindows(); //we must set all this also when restarting, because otherwise
	delete toolbar_; //it would be saved right back as-is on program exit
	createToolbar();
	control_panel_->setSplitterSizes();

	if (restart) { //hard reset - will apply appearance settings
358
		qApp->processEvents(); //e. g. fire the resize timers on macOS
359
		qApp->quit();
360
		QProcess::startDetached(qApp->arguments().at(0), qApp->arguments());
361
	}
362
363
}

364
365
366
367
368
/**
 * @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?
 */
369
void MainWindow::openIni(const QString &path, const bool &is_autoopen, const bool &fresh)
370
{
371
	this->getControlPanel()->getWorkflowPanel()->setEnabled(false); //hint at INIshell processing...
372
	setStatus(tr("Reading INI file..."), "info", true);
373
	refreshStatus(); //necessary if heavy operations follow
374
	if (fresh)
375
		clearGui();
376

377
	const bool success = ini_.parseFile(path); //load the file into the main INI parser
378
379
380
381
	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")),
382
		    (success? "info" : "warning")); //ill-formatted lines in INI file
383
	toolbar_save_ini_->setEnabled(true);
384
	file_save_ini_->setEnabled(true);
385
	autoload_->setVisible(true);
386
	ini_filename_->setText(path);
387
	autoload_box_->setText(tr("autoload this INI for ") + current_application_);
388
	if (!is_autoopen) //when a user clicks an INI file to open it we ask anew whether to autoopen
389
		autoload_box_->setCheckState(Qt::Unchecked);
390

391
	this->getControlPanel()->getWorkflowPanel()->setEnabled(true);
Mathias Bavay's avatar
Mathias Bavay committed
392
	QApplication::alert( this ); //notify the user that the task is finished
393
394
}

395
396
397
398
399
400
/**
 * @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.
 */
401
bool MainWindow::closeIni()
402
{
403
	INIParser gui_ini( ini_ );
404
	(void) control_panel_->getSectionTab()->setIniValuesFromGui(&gui_ini);
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
	if (ini_ != gui_ini) {
		static const QString msg_marker( "An application has been opened," );
		QString do_not_show_setting( "user::warnings::warn_unsaved_ini" );
		if (ini_.getEqualityCheckMsg().startsWith(msg_marker))
			do_not_show_setting = "user::warnings::warn_unsaved_empty";

		if (getSetting(do_not_show_setting, "value") == "TRUE") {
			/*
			 * 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.
			 */

			QString msg( tr("INI settings will be lost") );
			if (ini_.getEqualityCheckMsg().startsWith(msg_marker))
				msg = tr("INI settings not saved yet");
423
			const QString informative( "Some INI keys will be lost if you don't save the current INI file." );
424
			QFlags<QMessageBox::StandardButton> buttons( QMessageBox::Save | QMessageBox::Cancel | QMessageBox::Discard );
425
			RememberDialog msgNotSaved(do_not_show_setting, msg, informative,
426
				ini_.getEqualityCheckMsg(), buttons, this);
427

428
429
430
			int clicked = msgNotSaved.exec();
			if (clicked == QMessageBox::Cancel)
				return false;
Michael Reisecker's avatar
Michael Reisecker committed
431
			if (clicked == QMessageBox::Save)
432
				(void) saveIni();
433
434
		} //endif help_loaded
	} //endif ini_ != gui_ini
435

436
	ini_.clear();
437
	toolbar_save_ini_->setEnabled(false);
438
	file_save_ini_->setEnabled(false);
439
	ini_filename_->setText(QString());
440
	autoload_->setVisible(false);
441
	return true;
442
443
}

444
445
446
447
448
/**
 * @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.
 */
449
void MainWindow::clearGui(const bool &set_default)
450
{
451
	const bool perform_close = closeIni();
452
	if (!perform_close) //user clicked 'cancel'
453
		return;
454
	getControlPanel()->clearGui(set_default);
455
	control_panel_->getSectionTab()->clearDynamicTabs();
456
	ini_filename_->setText(QString());
457
	autoload_->setVisible(false);
458
459
}

460
461
462
463
464
/**
 * @brief Store the main window sizes in the settings XML.
 */
void MainWindow::setWindowSizeSettings()
{
465
	setSplitterSizeSettings();
466
467
468
469
470
471
472
473
474
475
476
477
478

	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()));
479
480
481
482
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::SETTINGS),
		"width", QString::number(settings_window_->width()));
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::SETTINGS),
		"height", QString::number(settings_window_->height()));
Michael Reisecker's avatar
Michael Reisecker committed
483
484
485
486
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::HELP),
		"width", QString::number(help_window_->width()));
	setSetting("auto::sizes::window_" + QString::number(dim::window_type::HELP),
		"height", QString::number(help_window_->height()));
487
488
489

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

492
493
494
495
496
497
498
499
500
501
/**
 * @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)));
}

502
503
504
505
506
507
/**
 * @brief Create a context menu for the toolbar.
 */
void MainWindow::createToolbarContextMenu()
{
	/* allow user to enable toolbar dragging */
508
	auto *fix_toolbar_position = new QAction(tr("Fix toolbar position"), toolbar_);
509
510
511
512
513
	fix_toolbar_position->setCheckable(true);
	fix_toolbar_position->setChecked(getSetting("user::appearance::fix_toolbar_pos", "value") == "TRUE");
	toolbar_context_menu_.addAction(fix_toolbar_position);
}

514
515
516
517
518
519
520
/**
 * @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()
{
521
522
523
524
525
	if (view_preview_->isEnabled()) {
		preview_->addIniTab();
		preview_->show();
		preview_->raise();
	}
526
527
}

528
529
/**
 * @brief Load the user help XML from the resources onto the GUI.
Michael Reisecker's avatar
Michael Reisecker committed
530
531
 * @param[in] tab_name Optional tab name to focus.
 * @param[in] frame_name Optional frame name to highlight.
532
 */
533
void MainWindow::loadHelp(const QString &tab_name, const QString &frame_name)
534
{
Michael Reisecker's avatar
Michael Reisecker committed
535
	help_window_->loadHelp();
Michael Reisecker's avatar
Michael Reisecker committed
536
	if (!tab_name.isEmpty())
Michael Reisecker's avatar
Michael Reisecker committed
537
		(void) help_window_->getSectionTab()->showPanel(tab_name, frame_name);
Michael Reisecker's avatar
Michael Reisecker committed
538
}
539

540
541
542
543
544
545
/**
 * @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.
546
 * @param[in] is_settings_dialog Is it the settings that are being loaded?
547
 */
548
549
void MainWindow::openXml(const QString &path, const QString &app_name, const bool &fresh,
    const bool &is_settings_dialog)
550
{
551
	if (fresh) {
552
		const bool perform_close = closeIni();
553
		if (!perform_close)
554
			return;
555
		control_panel_->clearGuiElements();
556
	}
557
558
	if (!is_settings_dialog)
		current_application_ = app_name;
559
560

	if (QFile::exists(path)) {
561
		setStatus(tr("Reading application XML..."), "info", true);
562
		refreshStatus();
563
		XMLReader xml;
Mathias Bavay's avatar
Mathias Bavay committed
564
565
		QString xml_error;
		const QString autoload_ini( xml.read(path, xml_error) );
566
567
568
		if (!xml_error.isNull()) {
			xml_error.chop(1); //trailing \n
			Error(tr("Errors occured when parsing the XML configuration file"),
569
			    tr("File: \"") + QDir::toNativeSeparators(path) + "\"", xml_error);
570
		}
571
		setStatus("Building GUI...", "info", true);
572
		buildGui(xml.getXml());
573
		setStatus("Ready.", "info", false);
574
		control_panel_->getWorkflowPanel()->buildWorkflowPanel(xml.getXml());
575
		if (!autoload_ini.isEmpty()) {
576
			if (QFile::exists(autoload_ini))
577
				openIni(autoload_ini);
578
			else
579
580
581
				log(QString("Can not load INI file \"%1\" automatically because it does not exist.").arg(
				    QDir::toNativeSeparators(autoload_ini)), "error");
		}
582
	} else {
583
		topLog(tr("An application or simulation file that has previously been found is now missing. Right-click the list to refresh."),
584
		     "error");
585
		setStatus(tr("File has been removed"), "error");
586
		return;
587
	}
588

589
	//run through all INIs that were saved to be autoloaded and check if it's the application we are opening:
590
	for (auto ini_node = global_xml_settings.firstChildElement().firstChildElement("user").firstChildElement(
591
	    "autoload").firstChildElement("ini");
592
593
594
595
	    !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);
596
			autoload_box_->setText(tr("autoload this INI for ") + current_application_);
597
			autoload_box_->blockSignals(false);
598
			openIni(ini_node.text(), true); //TODO: delete if non-existent
599
600
601
			break;
		}
	}
602
603
604
605
606
607
608
609

	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);
610
611
	toolbar_open_ini_->setEnabled(true); //toolbar entry
	file_open_ini_->setEnabled(true); //menu entry
612
	view_preview_->setEnabled(true);
613
	toolbar_preview_->setEnabled(true);
614

615
616
617
618
	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
619
620
	path_label->setWordWrap(true);
	path_label->setStyleSheet("QLabel {font-style: italic}");
Michael Reisecker's avatar
Michael Reisecker committed
621
	QApplication::alert(this); //notify the user that the task is finished
622
623
}

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

653
654
655
656
657
658
659
660
661
/**
 * @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.
 */
662
QWidgetList MainWindow::findSimplePanel(QWidget *parent, const Section &section, const KeyValue &keyval)
663
{
Mathias Bavay's avatar
Mathias Bavay committed
664
	const QString id(section.getName() + Cst::sep + keyval.getKey());
665
	/* simple, not nested, keys */
Michael Reisecker's avatar
Michael Reisecker committed
666
	return parent->findChildren<QWidget *>(Atomic::getQtKey(id));
667
668
}

669
670
671
672
673
674
675
676
677
678
/**
 * @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.
 */
679
QWidgetList MainWindow::prepareSelector(QWidget *parent, const Section &section, const KeyValue &keyval)
680
{
Mathias Bavay's avatar
Mathias Bavay committed
681
	const QString id( section.getName() + Cst::sep + keyval.getKey() );
682
	//INI key as it would be given in the file, i. e. with the parameters in place instead of the Selector's %:
683
	const QString regex_selector("^" + section.getName() + Cst::sep + R"(([\w\*\-\.]+)()" + Cst::sep + R"()(\w+?)([0-9]*$))");
684
	const QRegularExpression rex(regex_selector);
Mathias Bavay's avatar
Mathias Bavay committed
685
	const QRegularExpressionMatch matches( rex.match(id) );
686

687
	if (matches.captured(0) == id) { //it could be from a selector with template children
688
689
690
		static const size_t idx_parameter = 1;
		static const size_t idx_keyname = 3;
		static const size_t idx_optional_number = 4;
Mathias Bavay's avatar
Mathias Bavay committed
691
692
693
		const QString parameter( matches.captured(idx_parameter) );
		const QString key_name( matches.captured(idx_keyname) );
		const QString number( matches.captured(idx_optional_number) );
694
		//this would be the ID given in an XML, i. e. with a "%" staing for a parameter:
Mathias Bavay's avatar
Mathias Bavay committed
695
		const QString gui_id( section.getName() + Cst::sep + "%" + Cst::sep + key_name + (number.isEmpty()? "" : "#") );
696

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

Michael Reisecker's avatar
Michael Reisecker committed
704
		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
705
	}
706
	return QWidgetList();
707
708
}

709
710
711
712
713
714
715
716
717
718
/**
 * @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.
 */
719
QWidgetList MainWindow::prepareReplicator(QWidget *parent, const Section &section, const KeyValue &keyval)
720
{
Mathias Bavay's avatar
Mathias Bavay committed
721
	const QString id( section.getName() + Cst::sep + keyval.getKey() );
722
	//the key how it would look in an INI file:
723
	const QString regex_selector("^" + section.getName() + Cst::sep +
724
	    R"(([\w\.]+)" + Cst::sep + R"()*(\w*?)(?=\d)(\d*)$)");
725
	const QRegularExpression rex( regex_selector );
Mathias Bavay's avatar
Mathias Bavay committed
726
	const QRegularExpressionMatch matches( rex.match(id) );
727

728
729
730
731
732
733
734
735
736
	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;
Mathias Bavay's avatar
Mathias Bavay committed
737
738
739
		QString parameter( matches.captured(idx_parameter) ); //has trailing "::" if existent
		const QString key( matches.captured(idx_key) );
		const QString number( matches.captured(idx_number) );
740
		//the key how it would look in an XML:
Mathias Bavay's avatar
Mathias Bavay committed
741
		const QString gui_id( section.getName() + Cst::sep + parameter + key + "#" ); //Replicator's name
742
743
744
745
746
747
748
749
750
751

		/*
		 * 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));
752
753
		for (int ii = 0; ii < replicator_list.count(); ++ii)
			replicator_list.at(ii)->setProperty("ini_value", number);
754
755
		//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
756
		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
757
	} //endif has match
758
759

	return QWidgetList(); //no suitable Replicator found
760
761
}

762
763
764
/**
 * @brief Build the main window's menu items.
 */
765
void MainWindow::createMenu()
766
{
767
	/* FILE menu */
768
	QMenu *menu_file = this->menuBar()->addMenu(tr("&File"));
769
	file_open_ini_ = new QAction(getIcon("document-open"), tr("&Open INI file..."), menu_file);
770
	menu_file->addAction(file_open_ini_); //note that this does not take ownership
771
	connect(file_open_ini_, &QAction::triggered, this, [=]{ toolbarClick("open_ini"); });
772
	file_open_ini_->setShortcut(QKeySequence::Open);
773
	file_save_ini_ = new QAction(getIcon("document-save"), tr("&Save INI file"), menu_file);
774
	file_save_ini_->setShortcut(QKeySequence::Save);
775
776
777
	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"); });
778
	file_save_ini_as_ = new QAction(getIcon("document-save-as"), tr("Save INI file &as..."), menu_file);
779
	file_save_ini_as_->setShortcut(QKeySequence::SaveAs);
780
781
782
783
	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();
784
785
786
787
788
	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);
789

790
791
	/* GUI menu */
	QMenu *menu_gui = this->menuBar()->addMenu(tr("&GUI"));
Michael Reisecker's avatar
Michael Reisecker committed
792
	gui_reset_ = new QAction(getIcon("document-revert"), tr("&Reset to default values"), menu_gui);
793
794
795
796
	menu_gui->addAction(gui_reset_);
	gui_reset_->setEnabled(false);
	gui_reset_->setShortcut(Qt::CTRL + Qt::Key_Backspace);
	connect(gui_reset_, &QAction::triggered, this, [=]{ toolbarClick("reset_gui"); });
Michael Reisecker's avatar
Michael Reisecker committed
797
	gui_clear_ = new QAction(getIcon("edit-delete"), tr("&Clear"), menu_gui);
798
799
	menu_gui->addAction(gui_clear_);
	gui_clear_->setEnabled(false);
800
	gui_clear_->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Backspace);
801
	connect(gui_clear_, &QAction::triggered, this, [=]{ toolbarClick("clear_gui"); });
802
	menu_gui->addSeparator();
803
804
805
	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);
806

807
	/* VIEW menu */
808
	QMenu *menu_view = this->menuBar()->addMenu(tr("&View"));
809
810
811
812
813
	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
	connect(view_preview_, &QAction::triggered, this, &MainWindow::viewPreview);
814
	auto *view_log = new QAction(getIcon("utilities-system-monitor"), tr("&Log"), menu_view);
815
	menu_view->addAction(view_log);
816
	connect(view_log, &QAction::triggered, this, &MainWindow::viewLogger);
817
	view_log->setShortcut(Qt::CTRL + Qt::Key_L);
Michael Reisecker's avatar
Michael Reisecker committed
818
	auto *window_toggle_workflow = new QAction(getIcon("utilities-terminal"), tr("Toggle wor&kflow"), menu_view);
819
820
821
	menu_view->addAction(window_toggle_workflow);
	connect(window_toggle_workflow, &QAction::triggered, this, &MainWindow::toggleWorkflow);
	window_toggle_workflow->setShortcut(Qt::CTRL + Qt::Key_K);
Michael Reisecker's avatar
Michael Reisecker committed
822
	auto *view_refresh = new QAction(getIcon("view-refresh"), tr("&Refresh applications"), menu_view);
823
824
825
826
	menu_view->addAction(view_refresh);
	connect(view_refresh, &QAction::triggered, this,
	    [=]{ control_panel_->getWorkflowPanel()->scanFoldersForApps(); });
	view_refresh->setShortcut(QKeySequence::Refresh);
827
	menu_view->addSeparator();
828
	auto *view_settings = new QAction(getIcon("preferences-system"), tr("&Settings"), menu_view);
Mathias Bavay's avatar
Mathias Bavay committed
829
	view_settings->setMenuRole(QAction::PreferencesRole);
830
	menu_view->addAction(view_settings);
831
	connect(view_settings, &QAction::triggered, this, &MainWindow::viewSettings);
832
833
834
835