WSL/SLF GitLab Repository

dynamic_panels.cc 13.3 KB
Newer Older
Mathias Bavay's avatar
Mathias Bavay committed
1
//SPDX-License-Identifier: GPL-3.0-or-later
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*****************************************************************************/
/*  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/>.
*/

20
21
22
23
24
#include <src/panels/dynamic_panels.h>
#include <src/panels/Replicator.h>
#include <src/panels/Selector.h>
#include <src/main/constants.h>
#include <src/main/inishell.h>
25
26
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

#include <QStringList>

#ifdef DEBUG
	#include <iostream>
#endif //def DEBUG

/**
 * @brief Extract the character and numeric parts of a Replicator's key.
 * @details For example, they key "FILTER1::EXPRESSION" would be split up
 * into ("FILTER", 1, "EXPRESSION").
 * @param full_key They INI key to split up.
 * @param pre String before the number.
 * @param extracted_no Numeric part of the key.
 * @param post String after the number.
 * @param rightmost If true, search number from the right. If false, search from
 * the left (currently unused).
 * @return True if a number could be extracted and thus the key could be split up.
 */
bool splitRepKey(const QString &full_key, QString &pre, int &extracted_no, QString &post,
	const bool &rightmost)
{
	pre = QString();
	post = QString();
	int extracted = 0;
	bool found = false;

	if (rightmost) {
53
		for (int rpos = static_cast<int>(full_key.length()) - 1; rpos > -1; --rpos) { //start anchor at last char and iterate to 1st
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
			for (int len = 1; len <= rpos + 1; ++len) { //1 to max length of candidate (start of word to current anchor)
				const QString candidate( full_key.mid(rpos - len + 1, len));
				bool success;
				const int no = candidate.toInt(&success);
				if (success) { //str --> int conversion successful for this substring
					extracted = no;
					pre = full_key.mid(0, rpos - len + 1);
					post = full_key.mid(rpos + 1);
					found = true; //don't exit yet - we try if it is a multi digit number
				} else {
					break;
				}
			}
			if (found)
				break;
		} //endfor rpos
	} else { //leftmost
		for (int lpos = 0; lpos < full_key.length(); ++lpos) { //start anchor at 1st char and iterate to last
			for (int len = 1; len <= full_key.length() - lpos; ++len) { //1 to max length (current anchor to end of word)
				const QString candidate( full_key.mid(lpos, len));
				bool success;
				const int no = candidate.toInt(&success);
				if (success) {
					extracted = no;
					pre = full_key.mid(0, lpos - 1);
					post = full_key.mid(lpos + len + 1);
					found = true;
				} else {
					break;
				}
			}
			if (found)
				break;
		} //endfor lpos
	} //endif rightmost

	extracted_no = extracted;
	return found;
92
93
94
95

	//Up until v2.0.6 a different implementation relying on regular expressions was used.
	//The Replicator's regex was:
	//"^" + section.getName() + Cst::sep + R"(([\w\.]+)" + Cst::sep + R"()*(\w*?)(?=\d)(\d*)$)");
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
}

/**
 * @brief From a real INI key, get the name/key a Replicator would have
 * to be able to create this INI key.
 * @param full_key The key to get the parent's name for.
 * @param multi Replace all numbers in the string with Replicator tag
 * (special case) or only the rightmost one (default)?
 * @return The parent Replicator's name if this is a "dynamic" INI key,
 * and the key unchanged if not.
 */
QString getRepName(const QString &full_key, const bool &multi)
{
	QString rep_key( full_key );
	while (true) {
		//split key into character, numeric, character parts:
		QString pre, post;
		int no;
		const bool converted = splitRepKey(rep_key, pre, no, post);
		if (!converted)
			break;
		//replace number with special Replicator tag:
		rep_key = pre + "#" + post;
		if (!multi)
			break; //replace only rightmost number
	}
	return rep_key;
}

/**
 * @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 True if the Replicator was activated and created child panels.
 */
bool prepareReplicator(QWidget *parent, const Section &section, const KeyValue &keyval)
{
	const QString id( section.getName() + Cst::sep + keyval.getKey() );

	QString pre, post; //unused here
	int no; //extract number from a Replicator's key
141
	splitRepKey(id, pre, no, post); //e. g. "ARG1::CUTOFF" --> 1
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310

	QList<Replicator *> replicator_list = parent->findChildren<Replicator *>();
	bool found = false;
	for (int ii = 0; ii < replicator_list.count(); ++ii) {
		if (replicator_list.at(ii)->canSpawnPanel(id)) {
			replicator_list.at(ii)->setProperty("ini_value", QString::number(no));
			found = true; //allow multiple Replicator' to spawn it
		}
	} //endfor ii
	return found;
}

/**
 * @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.
 */
QWidgetList prepareSelector(QWidget *parent, const Section &section, const KeyValue &keyval)
{
	const QString id( section.getName() + Cst::sep + keyval.getKey() );
	//INI key as it would be given in the file, i. e. with the parameters in place instead of the Selector's %:
	const QString regex_selector("^" + section.getName() + Cst::sep + R"(([\w\*\-\.]+)()" + Cst::sep + R"()(\w+?)([0-9]*$))");
	const QRegularExpression rex(regex_selector);
	const QRegularExpressionMatch matches( rex.match(id) );

	if (matches.captured(0) == id) { //it could be from a selector with template children
		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) );

		QString gui_id( section.getName() + Cst::sep + "%" + Cst::sep + key_name + number );

		//try to find Selector:
		QList<Selector *> selector_list = parent->findChildren<Selector *>(Atomic::getQtKey(gui_id));
		if (selector_list.isEmpty()) //this would be the ID given in an XML, i. e. with a "%" standing for a parameter:
			gui_id = getRepName(gui_id); //a rep has already replaced it
		selector_list = parent->findChildren<Selector *>(Atomic::getQtKey(gui_id));

		for (int ii = 0; ii < selector_list.size(); ++ii)
			selector_list.at(ii)->setProperty("ini_value", parameter); //cf. notes in prepareReplicator
		//now all the right child widgets 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

		return parent->findChildren<QWidget *>(Atomic::getQtKey(key_id));
	}
	return QWidgetList();
}

/**
 * @brief Retrieve all key names that are found in (a dynamic panel's) XML child nodes.
 * @details A Replicator's child panels are built on request, so when a new INI key
 * is encountered they may not exist yet. This is why we keep a list of keys a Replicator
 * is able to spawn panels for.
 * @param[in] section The parent panel's INI section.
 * @param[in] node The (child) node to recursively parse for "key" attributes in all panels.
 * @return A List of INI keys the Replicator can create on the fly.
 */
QStringList getAllKeys(const QString &section, const QDomNode &node)
{
	QStringList current_level;
	for (QDomElement el = node.firstChildElement(); !el.isNull(); el = el.nextSiblingElement()) {
		const QString current_key( el.attribute("key") );
		if (!current_key.isNull())
			current_level.append(section + Cst::sep + current_key);
		current_level.append( getAllKeys(section, el) ); //recursion on all child nodes
	}
	return current_level;

	/*
	 * In this function the complete XML node is parsed for "key" attributes.
	 * Running on a Replicator's child panel node for e. g. the collection of keys
	 * then represents the child panels this Replicator can spawn.
	 * This mode of operation to check if a certain Replicator should be triggered for
	 * some encountered INI key is for e. g. used for Replicators without a key, i. e.
	 * the special case of Replicators that are solemnly a GUI element to group settings.
	 * It is also used for another special case where multiple occurrences of '#' should
	 * be replaced at once, i. e. a single-level Replicator with the key "A#::B#" --> "A1::B1".
	 *
	 * This is enough for current SLF software but it is not generic enough to handle
	 * arbitrarily complex XML files. For this, instead of a list of keys we would need
	 * a tree with logic to check which level of Replicator/Selector in a nested array can produce
	 * exactly the right enumeration in a key like "%::A#::B#::C#::%", i. e. an inheritance
	 * system parallel to QWidget's.
	 * It is left as a TODO: when the need arises and should fit the current structure.
	 */
}

/**
 * @brief Check if an XML node contains child panels that are Replicators.
 * @param node The node to search.
 * @return True if the node contains child panels of type Replicator.
 */
bool hasReplicatorChildren(const QDomNode &node)
{
	bool has_rep = false;
	for (QDomElement el = node.firstChildElement(); !el.isNull(); el = el.nextSiblingElement()) {
		if (el.attribute("replicate").toLower() == "true")
			has_rep = true;
		else
			has_rep = hasReplicatorChildren(el); //recursion on all child nodes
		if (has_rep)
			break;
	}
	return has_rep;
}

/**
 * @brief Substitute values in keys and texts.
 * @details This is for child elements that inherit a property from its parent which should be
 * displayed (e. g. "TA" in "TA::FILTER1 = ..."). The function traverses through the whole child
 * tree recursively.
 * @param[in] parent_element Parenting XML node holding the desired values.
 * @param[in] replace String to replace.
 * @param[in] replace_with Text to replace the string with.
 */
void substituteKeys(QDomElement &parent_element, const QString &replace, const QString &replace_with, const bool &once)
{
	for (QDomElement element = parent_element.firstChildElement(); !element.isNull(); element = element.nextSiblingElement()) {
		QString key(element.attribute("key"));
		QString text(element.attribute("caption"));
		QString label(element.attribute("label"));

		if (element.tagName() == "parameter") {
			if (once) {	//only subsitutute first occurrence, leave the rest to subsequent panels (e. g. TA::FILTER#::ARG#):
				element.setAttribute("key", key.replace(key.indexOf(replace), 1, replace_with));
				element.setAttribute("caption", text.replace(key.indexOf(replace), 1, replace_with)); //for labels
				element.setAttribute("label", label.replace(label.indexOf(replace), 1, replace_with));
			} else {
				element.setAttribute("key", key.replace(replace, replace_with));
				element.setAttribute("caption", text.replace(replace, replace_with));
				element.setAttribute("label", label.replace(replace, replace_with));
			}
		}
		substituteKeys(element, replace, replace_with, once);
	} //endfor element
}


/**
 * @brief Clear panels which can add/remove children at will.
 * @details This function clears panels that have the ability to create and more importantly delete
 * an arbitrary number of child panels. Those stand for a group of INI keys rather than a single one,
 * and can create child panels for INI keys such as "STATION1, STATION2, ...".
 */
template <class T>
void clearDynamicPanels()
{
	/*
	 * Fetching a list of suitable panels and iterating through them does not work out
	 * because clearing one dynamic panel can delete another one, which means the pointers
	 * we receive could be invalidated.
	 * So, we look for the first dynamic panel with children, clear that, and start the
	 * search again until there are no more dynamic panels with children found.
	 */
	const QList<T *> panel_list( getMainWindow()->getControlPanel()->getSectionTab()->findChildren<T *>() );
	for (auto &pan : panel_list) {
		//the panels themselves are not deleted, so we stop if it's empty:
		if (pan->count() > 0) {
			pan->clear();
			clearDynamicPanels<T>(); //repeat until all dynamic panels are empty
			return;
311
		} //look for all "key" attributes - no matter where they are found
312
313
314
315
316
	}
}
//this is a list of panels that can produce and delete an aribtrary number of child panels:
template void clearDynamicPanels<Selector>();
template void clearDynamicPanels<Replicator>();