WSL/SLF GitLab Repository

MainWindow.cc 51.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/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
#ifdef DEBUG
524
	const bool success = control_panel_->showTab(tab_name);
525
526
	if (!success)
		qDebug() << "Help section does not exist:" << tab_name;
527
528
#else
	(void) control_panel_->showTab(tab_name);
529
#endif //def DEBUG
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550

	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
551

552
553
554
555
556
557
558
559
560
561
/**
 * @brief Event handler for the "View::Close settings" menu: close the settings tab.
 */
void MainWindow::closeSettings()
{
	control_panel_->closeSettingsTab();
	toolbar_clear_gui_->setEnabled(false);
	gui_clear_->setEnabled(false);
}

562
563
564
565
566
567
/**
 * @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.
568
 * @param[in] is_settings_dialog Is it the settings that are being loaded?
569
 */
570
571
void MainWindow::openXml(const QString &path, const QString &app_name, const bool &fresh,
    const bool &is_settings_dialog)
572
{
573
	if (fresh) {
574
		const bool perform_close = closeIni();
575
		if (!perform_close)
576
			return;
577
		control_panel_->closeSettingsTab();
578
		control_panel_->clearGuiElements();
579
		help_loaded_ = false;
580
	}
581
582
	if (!is_settings_dialog)
		current_application_ = app_name;
583
584

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

613
	//run through all INIs that were saved to be autoloaded and check if it's the application we are opening:
614
	for (auto ini_node = global_xml_settings.firstChildElement().firstChildElement("user").firstChildElement(
615
	    "autoload").firstChildElement("ini");
616
617
618
619
	    !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);
620
			autoload_box_->setText(tr("autoload this INI for ") + current_application_);
621
			autoload_box_->blockSignals(false);
622
			openIni(ini_node.text(), true); //TODO: delete if non-existent
623
624
625
			break;
		}
	}
626
627
628
629
630
631
632
633

	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);
634
635
	toolbar_open_ini_->setEnabled(true); //toolbar entry
	file_open_ini_->setEnabled(true); //menu entry
636
	view_preview_->setEnabled(true);
637
	toolbar_preview_->setEnabled(true);
638

639
640
641
642
	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
643
644
	path_label->setWordWrap(true);
	path_label->setStyleSheet("QLabel {font-style: italic}");
Mathias Bavay's avatar
Mathias Bavay committed
645
	QApplication::alert( this ); //notify the user that the task is finished
646
647
}

648
649
650
651
652
653
654
655
656
657
/**
 * @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.
 */
658
QWidgetList MainWindow::findPanel(QWidget *parent, const Section &section, const KeyValue &keyval)
659
{
660
	QWidgetList panels;
661
	//first, check if there is a panel for it loaded and available:
662
	panels = findSimplePanel(parent, section, keyval);
663
	int panel_count = parent->findChildren<Atomic *>().count();
664
	//if not found, check if one of our Replicators can create it:
665
	if (panels.isEmpty())
666
		panels = prepareReplicator(parent, section, keyval);
667
668
669
670
671
672
	//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.
673
	if (parent->findChildren<Atomic *>().count() != panel_count) //new elements were created
674
		return findPanel(parent, section, keyval); //recursion through Replicators that create Selectors that...
Michael Reisecker's avatar
Michael Reisecker committed
675
	return panels; //no more suitable dynamic panels found
676
677
}

678
679
680
681
682
683
684
685
686
/**
 * @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.
 */
687
QWidgetList MainWindow::findSimplePanel(QWidget *parent, const Section &section, const KeyValue &keyval)
688
689
690
{
	QString id(section.getName() + Cst::sep + keyval.getKey());
	/* simple, not nested, keys */
Michael Reisecker's avatar
Michael Reisecker committed
691
	return parent->findChildren<QWidget *>(Atomic::getQtKey(id));
692
693
}

694
695
696
697
698
699
700
701
702
703
/**
 * @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.
 */
704
QWidgetList MainWindow::prepareSelector(QWidget *parent, const Section &section, const KeyValue &keyval)
705
{
706
	QString id(section.getName() + Cst::sep + keyval.getKey());
707
	//INI key as it would be given in the file, i. e. with the parameters in place instead of the Selector's %:
708
	const QString regex_selector("^" + section.getName() + Cst::sep + R"(([\w\.]+)()" + Cst::sep + R"()(\w+?)([0-9]*$))");
709
710
711
	const QRegularExpression rex(regex_selector);
	const QRegularExpressionMatch matches = rex.match(id);

712
	if (matches.captured(0) == id) { //it could be from a selector with template children
713
714
715
716
717
718
		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));
719
		//this would be the ID given in an XML, i. e. with a "%" staing for a parameter:
720
		const QString gui_id = section.getName() + Cst::sep + "%" + Cst::sep + key_name + (number.isEmpty()? "" : "#");
721

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

Michael Reisecker's avatar
Michael Reisecker committed
729
		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
730
	}
731
	return QWidgetList();
732
733
}

734
735
736
737
738
739
740
741
742
743
/**
 * @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.
 */
744
QWidgetList MainWindow::prepareReplicator(QWidget *parent, const Section &section, const KeyValue &keyval)
745
746
{
	QString id(section.getName() + Cst::sep + keyval.getKey());
747
	//the key how it would look in an INI file:
748
	const QString regex_selector("^" + section.getName() + Cst::sep +
749
	    R"(([\w\.]+)" + Cst::sep + R"()*(\w*?)(?=\d)(\d*)$)");
750
751
752
	const QRegularExpression rex(regex_selector);
	const QRegularExpressionMatch matches = rex.match(id);

753
754
755
756
757
758
759
760
761
762
763
	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));
764
		const QString number(matches.captured(idx_number));
765
		//the key how it would look in an XML:
766
767
768
769
770
771
772
773
774
775
776
		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));
777
778
		for (int ii = 0; ii < replicator_list.count(); ++ii)
			replicator_list.at(ii)->setProperty("ini_value", number);
779
780
		//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
781
		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
782
	} //endif has match
783
784

	return QWidgetList(); //no suitable Replicator found
785
786
}

787
788
789
/**
 * @brief Build the main window's menu items.
 */
790
void MainWindow::createMenu()
791
{
792
	/* FILE menu */
793
	QMenu *menu_file = this->menuBar()->addMenu(tr("&File"));
794
	file_open_ini_ = new QAction(getIcon("document-open"), tr("&Open INI file..."), menu_file);
795
	menu_file->addAction(file_open_ini_); //note that this does not take ownership
796
	connect(file_open_ini_, &QAction::triggered, this, [=]{ toolbarClick("open_ini"); });
797
	file_open_ini_->setShortcut(QKeySequence::Open);
Mathias Bavay's avatar
Mathias Bavay committed
798
	//file_open_ini_->setMenuRole(QAction::ApplicationSpecificRole);
799
	file_save_ini_ = new QAction(getIcon("document-save"), tr("&Save INI file"), menu_file);
800
	file_save_ini_->setShortcut(QKeySequence::Save);
Mathias Bavay's avatar
Mathias Bavay committed
801
	//file_save_ini_->setMenuRole(QAction::ApplicationSpecificRole);
802
803
804
	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"); });
805
	file_save_ini_as_ = new QAction(getIcon("document-save-as"), tr("Save INI file &as..."), menu_file);
806
	file_save_ini_as_->setShortcut(QKeySequence::SaveAs);
Mathias Bavay's avatar
Mathias Bavay committed
807
	//file_save_ini_as_->setMenuRole(QAction::ApplicationSpecificRole);
808
809
810
811
	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();
812
813
814
815
816
	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);
817

818
819
	/* GUI menu */
	QMenu *menu_gui = this->menuBar()->addMenu(tr("&GUI"));
820
	gui_reset_ = new QAction(getIcon("document-revert"), tr("&Reset GUI to default values"), menu_gui);
821
822
823
	menu_gui->addAction(gui_reset_);
	gui_reset_->setEnabled(false);
	gui_reset_->setShortcut(Qt::CTRL + Qt::Key_Backspace);
Mathias Bavay's avatar
Mathias Bavay committed
824
	//gui_reset_->setMenuRole(QAction::ApplicationSpecificRole);
825
	connect(gui_reset_, &QAction::triggered, this, [=]{ toolbarClick("reset_gui"); });
826
	gui_clear_ = new QAction(getIcon("edit-delete"), tr("&Clear GUI"), menu_gui);
827
828
	menu_gui->addAction(gui_clear_);
	gui_clear_->setEnabled(false);
829
	gui_clear_->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Backspace);
Mathias Bavay's avatar
Mathias Bavay committed
830
	//gui_clear_->setMenuRole(QAction::ApplicationSpecificRole);
831
	connect(gui_clear_, &QAction::triggered, this, [=]{ toolbarClick("clear_gui"); });
832
	menu_gui->addSeparator();
833
834
835
	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
836
	//gui_close_all_->setMenuRole(QAction::ApplicationSpecificRole);
837

838
	/* VIEW menu */
839
	QMenu *menu_view = this->menuBar()->addMenu(tr("&View"));
840
841
842
843
	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
844
	//view_preview->setMenuRole(QAction::ApplicationSpecificRole);
845
	connect(view_preview_, &QAction::triggered, this, &MainWindow::viewPreview);
846
	auto *view_log = new QAction(getIcon("utilities-system-monitor"), tr("&Log"), menu_view);
847
	menu_view->addAction(view_log);
848
	connect(view_log, &QAction::triggered, this, &MainWindow::viewLogger);
849
	view_log->setShortcut(Qt::CTRL + Qt::Key_L);
Mathias Bavay's avatar
Mathias Bavay committed
850
	//view_log->setMenuRole(QAction::ApplicationSpecificRole);
851
852
853
854
855
856
	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);

857
	menu_view->addSeparator();
858
	auto *view_settings = new QAction(getIcon("preferences-system"), tr("&Settings"), menu_view);
Mathias Bavay's avatar
Mathias Bavay committed
859
	view_settings->setMenuRole(QAction::PreferencesRole);
860
	menu_view->addAction(view_settings);
861
	connect(view_settings, &QAction::triggered, this, &MainWindow::viewSettings);
862
863
864
865
866
867
#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);
#else
	view_settings->setShortcut(Qt::Key_F3);
#endif
868
	/* WINDOW menu */
869
	QMenu *menu_window = this->menuBar()->addMenu(tr("&Window"));
870
	menu_window->addSeparator();
871
	auto *window_show_workflow = new QAction(tr("Show wor&kflow"), menu_window);
872
873
	menu_window->addAction(window_show_workflow);
	connect(window_show_workflow, &QAction::triggered, this, &MainWindow::showWorkflow);
874
	window_show_workflow->setShortcut(Qt::CTRL + Qt::Key_K);
Mathias Bavay's avatar
Mathias Bavay committed
875
	//window_show_workflow->setMenuRole(QAction::ApplicationSpecificRole);
876
	auto *window_hide_workflow = new QAction(tr("&Hide workflow"), menu_window);
877
878
	menu_window->addAction(window_hide_workflow);
	connect(window_hide_workflow, &QAction::triggered, this, &MainWindow::hideWorkflow);
879
	window_hide_workflow->setShortcut(Qt::CTRL + Qt::Key_H);
Mathias Bavay's avatar
Mathias Bavay committed
880
	//window_hide_workflow->setMenuRole(QAction::ApplicationSpecificRole);
881

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

890
	/* HELP menu */
891
#if !defined Q_OS_MAC
Michael Reisecker's avatar
Michael Reisecker committed
892
	auto *menu_help_main = new QMenuBar(this->menuBar());
893
894
895
896
	this->menuBar()->setCornerWidget(menu_help_main); //push help menu to the right
#else
	auto *menu_help_main = this->menuBar()->addMenu("&Help");
#endif
897
	QMenu *menu_help = menu_help_main->addMenu(tr("&Help"));
898