WSL/SLF GitLab Repository

SectionTab.cc 17 KB
Newer Older
Michael Reisecker's avatar
Michael Reisecker committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*****************************************************************************/
/*  Copyright 2021 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
   it under the terms of the GNU General Public License as published by
   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
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with INIshell.  If not, see <http://www.gnu.org/licenses/>.
*/

19
#include "RememberDialog.h"
20
21
#include "SectionTab.h"
#include "SectionButton.h"
22
#include "src/gui_elements/Group.h"
23
24
#include "src/main/colors.h"
#include "src/main/inishell.h"
25
#include "src/main/os.h"
26
#include "src/main/settings.h"
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include "src/main/XMLReader.h"

/**
 * @class ScrollPanel
 * @brief The main scroll panel controlling all dynamically built panels.
 * @param[in] section The section this panel corresponds to. It could be used for styling but is
 * unused otherwise.
 * @param[in] parent The parent widget (the main tab bar).
 */
ScrollPanel::ScrollPanel(const QString &tab_color, QWidget *parent)
	: QWidget(parent)
{
	/* create a scroll area and put a Group in it */
	main_area_ = new QScrollArea;
	main_area_->setWidgetResizable(true); //best to let the manager do its job - troubles ahead if we don't
	main_area_->setStyleSheet("QScrollArea {border: none}"); //we have one from the tabs already
	main_group_ = new Group(QString(), QString(), false, false, false, false, QString(), QString(), tab_color);
	main_area_->setWidget(main_group_);

	/* main layout */
	auto *layout( new QVBoxLayout );
	layout->addWidget(main_area_);
	this->setLayout(layout);
}

/**
 * @brief Retrieve the main grouping element of a scroll panel (there is one per tab).
 * @return The main Group of this scroll panel holding all widgets.
 */
Group * ScrollPanel::getGroup() const
{
	return this->main_group_;
}

61
62
63
64
65
/**
 * @class SectionTab
 * @brief A subclassed tab bar that the main GUI building recursion can be started on.
 * @param[in] parent The parent widget.
 */
66
67
SectionTab::SectionTab(QWidget *parent) : QTabWidget(parent)
{
68
	//do nothing
69
70
}

71
72
73
74
75
76
77
78
79
80
81
82
83
/**
 * @brief Retrieve the ScrollPanel of a section.
 * @details Sections are grouped in tab bar elements. For this, they are put in that specific
 * tab panel's ScrollPanel which can be retrieved by this function (to build top level panels into).
 * If the section/tab does not exist yet it is created.
 * @param[in] section The INI section for which to get the corresponding ScrollPanel.
 * @param[in] background_color Background color to give to a whole section that is being created.
 * @param[in] color Font color to give a new section.
 * @param[in] no_create Set to true when parsing an INI to "force" INI sections matching the XML.
 * @param[in] dynamic Is this a dynamic section that is being created?
 * @return The found or newly created ScrollPanel.
 */
ScrollPanel * SectionTab::getSectionScrollArea(const QString &section, const QString &background_color,
84
	const QString &color, const bool&no_create, const int &insert_idx)
85
86
87
{ //find or create section tab

	for (int ii = 0; ii < this->count(); ++ii) {
88
		if (QString::compare(os::cleanKDETabStr(this->tabBar()->tabText(ii)), section, Qt::CaseInsensitive) == 0)
89
90
91
			return qobject_cast<ScrollPanel *>(this->widget(ii)); //cast to access members
	}
	if (!no_create) {
92
93
94
		int new_index = insert_idx;
		if (insert_idx == -1)
			new_index = this->count();
95
		auto *new_scroll( new ScrollPanel(background_color) );
Michael Reisecker's avatar
Michael Reisecker committed
96
97
		this->insertTab(new_index, new_scroll, section);
		this->tabBar()->setTabTextColor(new_index, colors::getQColor(color));
98

99
100
101
102
103
104
		if (insert_idx != -1) {
			auto button = new SectionButton(SectionButton::minus, section);
			this->tabBar()->setTabButton(new_index, QTabBar::RightSide, button);
		} else if (isDynamicParent(section)) {
			auto button = new SectionButton(SectionButton::plus, section);
			this->tabBar()->setTabButton(new_index, QTabBar::RightSide, button);
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
		}
		return new_scroll;
	} else { //don't add sections for keys found in ini files
		return nullptr;
	}
}

/**
 * @brief Retrieve a section's ScrollPanel with known section tab index.
 * @param[in] index The section's index in the tab bar.
 * @return ScrollPanel for the section.
 */
ScrollPanel * SectionTab::getSectionScrollArea(const int &index)
{
	return qobject_cast<ScrollPanel *>(this->widget(index));
}

122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
 * @brief Run through panels and query their user-set values.
 * @details This function finds all panel classes, gets their set values, and modifies the
 * corresponding INI setting. It also performs checks for missing values.
 * Note: Originally INI values were updated live in the panels on input, eliminating the need
 * for this loop. However, more and more intermediate loops needed to be introduced (for example
 * to deactivate panels that are hidden by a parent element like a Dropdown or Choice).
 * Furthermore, dealing with missing mandatory values and keys that are present multiple times
 * (e. g. needing to check if a second panel manipulates the same key before deleting it in a
 * Selector) convoluted the program so heavily that this solution is much cleaner and more robust.
 * @param[in] ini The INIParser for which to set the values (the one that will be output).
 * @return A comma-separated list of missing mandatory INI keys.
 */
QString SectionTab::setIniValuesFromGui(INIParser *ini)
{
	//TODO: For some combination of properties (optional, will be defaulted by the
	//software, ...) we may be able to skip output of some keys to avoid
	//information redundancy. Parameter checks:
	//const QString default_value( panel->property("default_value").toString() ); //default value
	//( !ini->get(section, key).isNull()) ) //key present in input INI file
	//( is_mandatory ) //key is set non-optional

	QString missing;
	for (int ii = 0; ii < this->count(); ++ii) { //run through sections
		const QWidget *section_tab( this->widget(ii) );
		const QList<Atomic *> panel_list( section_tab->findChildren<Atomic *>() );
		for (auto &panel : panel_list) { //run through panels in section
			if (!panel->isVisibleTo(section_tab)) //visible if section_tab was visible?
				continue;
			QString section, key;
			const QString value( panel->getIniValue(section, key) );
			if (panel->property("no_ini").toBool())
				continue;
			if (key.isNull()) //GUI element that is not an interactive panel
				continue;

			const bool is_mandatory = panel->property("is_mandatory").toBool();
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
			if (is_mandatory && value.isEmpty()) { //collect missing non-optional keys
				bool is_dynamic_widget = false;
				bool dyn_is_empty = false;

				auto *rep = qobject_cast<Replicator *>(panel);
				if (rep) {
					is_dynamic_widget = true;
					dyn_is_empty = rep->isEmpty();
				} else {
					auto *sel = qobject_cast<Selector *>(panel);
					if (sel) {
						is_dynamic_widget = true;
						dyn_is_empty = sel->isEmpty();
					}
				}
				if (!is_dynamic_widget || dyn_is_empty) //a dynamic panel is only missing if it's empty
					missing += section + "::" + key + ",\n";
			}
177
178
179
180
181
182
183
184
			if (!value.isEmpty())
				ini->set(section, key, value, is_mandatory);
		} //endfor panel_list
	} //endfor ii
	missing.chop(2); //trailing ", "
	return missing;
}

185
186
187
188
189
190
/**
 * @brief Mark and remember a certain section to be dynamic.
 * @param[in] section The ini section to mark as dynamic.
 * @param[in] node The section's complete XML node. Note that this only works
 * in dedicated <section> tags!
 */
191
192
void SectionTab::setSectionDynamic(const QString &section, const QDomNode &node)
{
193
	dynamic_sections_.push_back(section);
194
	dynamic_nodes_.push_back(node);
195
196
197
	dynamic_running_indices_.push_back(1); //1st of its kind
}

198
199
200
201
202
203
/**
 * @brief Check if a section is marked as dynamic and can therefore
 * spawn child sections.
 * @param[in] section The ini section name to check.
 * @return True if this is a parent section.
 */
204
205
206
207
208
209
bool SectionTab::isDynamicParent(const QString &section) const
{
	const int dyn_idx = dynamic_sections_.indexOf(QRegExp(section, Qt::CaseInsensitive));
	return (dyn_idx != -1);
}

210
/**
211
 * @brief Check if a section was derived from any dynamic parent section.
212
213
214
215
 * @details This function checks for the syntax string + number.
 * @param[in] section The ini section name to check.
 * @return True if this is a child section that was spawned by a parent section.
 */
216
217
218
int SectionTab::isDynamicChild(const QString &section) const
{
	for (int ii = 0; ii < dynamic_sections_.length(); ++ii) {
219
220
221
222
		if (isDynamicChildOf(section, dynamic_sections_.at(ii)))
		if (section.startsWith(dynamic_sections_.at(ii), Qt::CaseInsensitive))
			return true;
		//else: the correct one may still come later
223
224
225
226
	} //endfor ii
	return false;
}

227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/**
 * @brief Check if a section was derived from a specific parent section.
 * @param section The section to check.
 * @param parent_section Parent section to check.
 * @return True if this is child of the given parent section.
 */
bool SectionTab::isDynamicChildOf(const QString &section, const QString &parent_section) const
{
	if (!section.startsWith(parent_section, Qt::CaseInsensitive))
		return false;
	if (QString::compare(section, parent_section, Qt::CaseInsensitive) == 0)
		return false; //a section is not a child of itself

	const QString postfix( section.mid(parent_section.length()) ); //the potential number
	bool success;
	(void) postfix.toInt(&success);
	return success;
}

/**
 * @brief Count how many children a dynamic section currently has.
 * @param parent_section The parent dynamic section to check.
 * @return Number of currently opened child sections.
 */
int SectionTab::countChildren(const QString &parent_section) const
{
	int counter = 0;
	for (int ii = 0; ii < this->tabBar()->count(); ++ii) {
		if (isDynamicChildOf(os::cleanKDETabStr(this->tabBar()->tabText(ii)), parent_section))
			counter++;
	}
	return counter;
}

/**
 * @brief Get the parent section to a dynamic child section.
 * @param section The ini section to get parent section for.
 * @return The section's parent section.
 */
QString SectionTab::getParentOf(const QString &section)
{
	for (auto &parsec : dynamic_sections_) {
		if (isDynamicChildOf(section, parsec))
			return parsec;
	}
	return QString(); //not found
}

275
276
277
278
279
/**
 * @brief Take a parent section and clone it to a new one.
 * @param[in] section The section to clone.
 * @param[in] new_tab_name The name to give to the new tab. Leave empty to pick one.
 */
280
void SectionTab::spawnDynamicSection(const QString &section, const QString &new_tab_name)
281
{
282
	int index = getIndex(section, Qt::CaseInsensitive);
283
	const int dyn_idx = dynamic_sections_.indexOf(QRegExp(section, Qt::CaseInsensitive));
284
285
286
287
288
289
290
291
292
293
294
295

	bool found = false;
	for (int ii = index + 1; ii < this->count(); ++ii) {
		const QString postfix( os::cleanKDETabStr(
			this->tabBar()->tabText(ii)).mid(section.length()) ); //the potential number
		bool success = true;
		if (!postfix.isEmpty()) //section is the start of a tab name --> check if remainder is a number:
			(void) postfix.toInt(&success); //(so that e. g. "InputEditing" is not seen as child of "Input")
		if (!os::cleanKDETabStr(this->tabBar()->tabText(ii)).startsWith(section, Qt::CaseInsensitive) || !success) {
			index = ii - 1; //order after all dynamic sections of this kind
			found = true;
			break;
296
		}
297
	}
298
299
300
	if (!found) //parent panel is the last one --> append
		index = this->count() - 1;

Michael Reisecker's avatar
Michael Reisecker committed
301
302
303
304
305
	QString new_name( new_tab_name );
	if (new_name.isNull()) { //from a + button click (i. e. not from an INI file) --> pick new name
		new_name = (section + "%1").arg(dynamic_running_indices_.at(dyn_idx));
		dynamic_running_indices_[dyn_idx]++;
	}
306
	auto dyn_scroll = getSectionScrollArea(new_name, QString(), "normal", false, index + 1);
307
	recursiveBuild(dynamic_nodes_.at(dyn_idx), dyn_scroll->getGroup(), new_name);
308
309
	if (new_tab_name.isNull()) //do not focus tab from ini read-in
		this->setCurrentIndex(index + 1);
310
311
}

312
313
314
315
316
317
/**
 * @brief Try for a currently unknown section if it could be a child section,
 * and if so, create it.
 * @param[in] section The ini section to check
 * @return A new tab for the new section, or nullptr if this is not a dynamic child.
 */
318
ScrollPanel * SectionTab::tryDynamicSection(const QString &section)
319
{
320
321
322
323
	for (int ii = 0; ii < dynamic_sections_.length(); ++ii) {
		if (section.startsWith(dynamic_sections_.at(ii), Qt::CaseInsensitive)) {
			const QString postfix( section.mid(dynamic_sections_.at(ii).length()) );
			bool success;
324
			const int sec_no = postfix.toInt(&success);
325
			if (success) { //we use the 1st one that fits
326
327
328
329
				spawnDynamicSection(dynamic_sections_.at(ii), section);
				if (sec_no > dynamic_running_indices_.at(ii))
					dynamic_running_indices_[ii] = sec_no + 1; //start at highest index on "+" click
				return getSectionScrollArea(section);
330
331
332
333
334
335
			}
		}
	} //endfor ii
	return nullptr;
}

336
337
338
339
/**
 * @brief Remove a dynamic child section.
 * @param[in] section The ini section to remove.
 */
340
341
void SectionTab::removeDynamicSection(const QString &section)
{
Michael Reisecker's avatar
Michael Reisecker committed
342
	if (getSetting("user::warnings::warn_unsaved_tab", "value") == "TRUE") {
343
		const QString msg( tr("INI settings will be lost") );
344
345
		const QString informative( tr("If you close this tab, all of its settings will be lost.") );
		const QString details( tr(
346
347
			"If you did not make any changes to the default values of this tab/section, you can safely ignore this message.") );
		const QFlags<QMessageBox::StandardButton> buttons( QMessageBox::Ok | QMessageBox::Cancel );
Michael Reisecker's avatar
Michael Reisecker committed
348
		RememberDialog msgNotSaved("user::warnings::warn_unsaved_tab", msg, informative, details, buttons, this);
349
350
351
352
353
354

		int clicked = msgNotSaved.exec();
		if (clicked == QMessageBox::Cancel)
			return;
	}

355
356
357
358
359
	const QString parent_section( getParentOf(section) );
	if (countChildren(parent_section) == 1) {
		const int dyn_idx = dynamic_sections_.indexOf(parent_section, Qt::CaseInsensitive);
		dynamic_running_indices_[dyn_idx] = 1;
	}
360
	for (int ii = 0; ii < this->tabBar()->count(); ++ii) {
361
		if (os::cleanKDETabStr(this->tabBar()->tabText(ii)) == section) {
362
363
364
			this->widget(ii)->deleteLater();
			break;
		}
365
	} //endfor ii
366
367
368
369
370
371
372
373
374
}

/**
 * @brief Select tab specified by its caption.
 * @param[in] tab_name The tab/section to select.
 * @return True if the tab could be selected.
 */
bool SectionTab::showTab(const QString &tab_name)
{
375
376
377
378
379
	const int idx = getIndex(tab_name);
	if (idx == -1) //not found
		return false;
	this->setCurrentIndex(idx);
	return true;
380
}
Michael Reisecker's avatar
Michael Reisecker committed
381

382
383
384
385
/**
 * @brief Highlight a certain frame within a tab.
 * @details For this to work the frame must have a "key" property to reference
 * in the XML.
386
 * @param[in] section The tab/section the frame is in.
387
 * @param[in] element_key The element's key to show and highlight.
388
389
 * @return
 */
Michael Reisecker's avatar
Michael Reisecker committed
390
bool SectionTab::showPanel(const QString &section, const QString &element_key)
391
{
Michael Reisecker's avatar
Michael Reisecker committed
392
393

#ifdef DEBUG
394
	const bool success = showTab(section);
Michael Reisecker's avatar
Michael Reisecker committed
395
	if (!success)
396
		qDebug() << "Help section does not exist:" << section;
Michael Reisecker's avatar
Michael Reisecker committed
397
#else
398
	(void) showTab(section);
Michael Reisecker's avatar
Michael Reisecker committed
399
400
401
#endif //def DEBUG

	bool found = false;
402
	const auto parent = this->getSectionScrollArea(section);
403
	const QList<Atomic *> panel_list( parent->findChildren<Atomic *>() );
404
	for (auto &panel : panel_list) { //run through elements in section and highlight all matching ones
Michael Reisecker's avatar
Michael Reisecker committed
405
		QString section, key;
406
		(void) panel->getIniValue(section, key);
407
408
		if (QString::compare(key, element_key, Qt::CaseInsensitive) == 0) {
			const QString id( section + Cst::sep + key ); //a frame can also have an (arbitrary) key
409
410
411
412
413
			auto panel = getMainWindow()->getControlPanel()->getSectionTab()->findChild<Atomic *>(Atomic::getQtKey(id));
			if (!panel)
				continue;
			parent->ensureWidgetVisible(panel); //scroll the ScrollBar until visible
			panel->setHighlightedStyle(); //TODO: make it work for Selectors (their keys can not match)
414
			found = true; //at least one was highlighted
Michael Reisecker's avatar
Michael Reisecker committed
415
416
417
418
		}
	} //endfor panel_list
	this->raise();
	return found;
419
420
}

421
422
423
424
425
426
/**
 * @brief Get tab index from a section name.
 * @param[in] section Section name to retrieve the index for.
 * @param[in] sensitivity Case sensitive search?
 * @return Tab index of the section, or -1 if not found.
 */
427
428
429
int SectionTab::getIndex(const QString &section, const Qt::CaseSensitivity &sensitivity)
{
	for (int ii = 0; ii < this->count(); ++ii) {
430
		if (QString::compare(os::cleanKDETabStr(this->tabBar()->tabText(ii)), section, sensitivity) == 0)
431
432
433
434
435
			return ii;
	}
	return -1; //not found
}

436
437
438
439
440
441
442
443
444
445
/**
 * @brief Clear all tab pages.
 * @details Unlike the native QTabWidget::clear(), this here also deletes the pages.
 */
void SectionTab::clear()
{
	while (this->count() > 0) {
		this->widget(0)->deleteLater();
		this->removeTab(0);
	}
446
447
448
	dynamic_sections_.clear();
	dynamic_nodes_.clear();
	dynamic_running_indices_.clear();
449
}
450

451
452
453
/**
 * @brief Remove all dynamic child tabs.
 */
454
455
456
void SectionTab::clearDynamicTabs()
{
	for (int ii = this->count() - 1; ii > -1; --ii) {
457
		if (isDynamicChild(os::cleanKDETabStr(this->tabBar()->tabText(ii)))) {
458
459
460
461
462
			this->widget(ii)->deleteLater();
			this->removeTab(ii);
		}
	}
	for (int ii = 0; ii < dynamic_running_indices_.count(); ++ii)
463
		dynamic_running_indices_[ii] = 1;
464
}