WSL/SLF GitLab Repository

Number.cc 22.9 KB
Newer Older
Mathias Bavay's avatar
Mathias Bavay committed
1
//SPDX-License-Identifier: GPL-3.0-or-later
2
3
4
5
6
/*****************************************************************************/
/*  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
7
   it under the terms of the GNU General Public License as published by
8
9
10
11
12
13
   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
14
   GNU General Public License for more details.
15

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

20
21
22
23
#include <src/panels/Number.h>
#include <src/main/colors.h>
#include <src/main/expressions.h>
#include <src/main/inishell.h>
24
25

#include <QDoubleSpinBox>
26
#include <QFontMetrics>
27
#include <QLocale>
28
#include <QSpinBox>
29
#include <QTimer>
30

31
#include <algorithm> //for max()
32
33
#include <climits> //for the number panel limits

34
35
36
37
#ifdef DEBUG
	#include <iostream>
#endif //def DEBUG

38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * @class KeyPressFilter
 * @brief Key press event listener for the Number panel.
 * @details We can not override 'event' in the panel itself because we want to
 * listen to the events of a child widget.
 * @param[in] object Object the event stems from (the SpinBox).
 * @param[in] event The type of event.
 * @return True if the event was accepted.
 */
bool KeyPressFilter::eventFilter(QObject *object, QEvent *event)
{
	if (event->type() == QEvent::KeyPress) {
		const QKeyEvent *key_event = static_cast<QKeyEvent *>(event);
51
		if ((key_event->key() >= Qt::Key_0 && key_event->key() <= Qt::Key_9) || (key_event->key() == Qt::Key_Minus)) {
52
			if (object->property("empty").toBool()) {
53
54
55
				/*
				 * A Number panel's value may be hidden to mark the panel as not set.
				 * If a user starts entering a number via the keyboard however, then
56
				 * this hidden value contributes. Here, we prevent this.
57
58
				 */
				object->setProperty("empty", "false"); //necessary if entered number happens to be the hidden value
59
60
61
62
63
64
65
66
				if (auto *spinbox1 = qobject_cast<QSpinBox *>(object)) { //try both types
					spinbox1->parent()->setProperty("ini_value", key_event->key() - Qt::Key_0);
					spinbox1->style()->unpolish(spinbox1);
					spinbox1->style()->polish(spinbox1);
				} else if (auto *spinbox2 = qobject_cast<QDoubleSpinBox *>(object)) {
					spinbox2->parent()->setProperty("ini_value", key_event->key() - Qt::Key_0);
					spinbox2->style()->unpolish(spinbox2);
					spinbox2->style()->polish(spinbox2);
67
68
69
70
71
72
73
74
				}
				return true; //we have already input the value - prevent 2nd time
			} //endif property
		} //endif key_event
	}
	return QObject::eventFilter(object, event); //pass to actual event of the object
}

75
76
77
78
/**
 * @class Number
 * @brief Default constructor for a Number panel.
 * @details A number panel displays and manipulates a float or integer value.
79
80
 * It can also switch to free text mode, allowing to enter an expression (according to SLF software
 * syntax) which it will check.
81
82
83
84
85
86
87
88
89
90
 * @param[in] section INI section the controlled value belongs to.
 * @param[in] key INI key corresponding to the value that is being controlled by this Number panel.
 * @param[in] options XML node responsible for this panel with all options and children.
 * @param[in] no_spacers Keep a tight layout for this panel.
 * @param[in] parent The parent widget.
 */
Number::Number(const QString &section, const QString &key, const QDomNode &options, const bool &no_spacers,
    QWidget *parent) : Atomic(section, key, parent)
{
	/* number widget depending on which type of number to display */
Mathias Bavay's avatar
Mathias Bavay committed
91
	const QString format( options.toElement().attribute("format") );
92
	if (format == "decimal" || format.isEmpty()) {
93
		number_element_ = new QDoubleSpinBox;
94
		mode_ = NR_DECIMAL;
95
		//Note that the mode concerns only the QSpinBox, arithmetic expressions are decoupled
96
97
	} else if (format == "integer" || format == "integer+") {
		number_element_ = new QSpinBox;
98
		mode_ = (format == "integer")? NR_INTEGER : NR_INTEGERPLUS;
99
	} else {
Mathias Bavay's avatar
Mathias Bavay committed
100
		topLog(tr(R"(XML error: unknown number format for key "%1::%2")").arg(
101
		    section_, key_), "error");
102
		return;
103
	}
104
	QTimer::singleShot(1, this, [=]{ setEmpty(true); }); //indicate that even if 0 is displayed, nothing is set yet
105

106
	auto *key_label( new Label(QString(), QString(), options, no_spacers, key_, this) );
107
	if (key_label->isEmpty())
108
		setEmphasisWidget(number_element_); //start with SpinBox and switch if needed
109
	else
110
		setEmphasisWidget(key_label->label_);
Michael Reisecker's avatar
Michael Reisecker committed
111
	number_element_->setFixedWidth(Cst::width_number_min);
112
113
	key_filter_ = new KeyPressFilter;
	number_element_->installEventFilter(key_filter_);
114
115
	focus_filter_ = new FocusEventFilter;
	number_element_->installEventFilter(focus_filter_); //select all text on focus receive
116
	number_element_->setLocale(QLocale::C); //display '.' as decimal separator
117

118
	/* free text expression entering  */
119
120
	expression_element_ = new QLineEdit(this);
	expression_element_->hide();
Michael Reisecker's avatar
Michael Reisecker committed
121
	expression_element_->setFixedWidth(Cst::width_number_min);
122
	expression_element_->setToolTip(number_element_->toolTip());
123
	connect(expression_element_, &QLineEdit::textChanged, this, &Number::checkStrValue);
124
	expression_element_->installEventFilter(focus_filter_);
125

126
	/* switch button and layout for number element plus buttons */
127
128
129
130
	switch_button_ = new QToolButton;
	connect(switch_button_, &QToolButton::toggled, this, &Number::switchToggle);
	switch_button_->setAutoRaise(true);
	switch_button_->setCheckable(true);
131
	switch_button_->setStyleSheet("QToolButton:checked {background-color: " +
132
	    colors::getQColor("number").name() + "}");
133
	switch_button_->setIcon(getIcon("displaymathmode"));
Mathias Bavay's avatar
Mathias Bavay committed
134
	switch_button_->setToolTip(tr("Enter an expression such as ${other_ini_key}, ${env:my_env_var} or ${{arithm. expression}}"));
135

136
137
138
139
140
	/* action button */
	validity_button_ = new QToolButton; //a button that can pop up if the text has a certain format
	validity_button_->hide();
	validity_button_->setStyleSheet("* {border: none}");
	validity_button_->setCursor(Qt::PointingHandCursor);
141
	validity_button_->setFocusPolicy(Qt::NoFocus); //to be the same as in Textfield
142
	QSize sz_label;
Mathias Bavay's avatar
Mathias Bavay committed
143
144
	sz_label.setWidth(fontMetrics().boundingRect(Cst::u_warning).width());
	sz_label.setHeight(fontMetrics().boundingRect(Cst::u_warning).height());
145
146
147
148
	validity_button_->setFixedSize(sz_label);
	validity_button_->setText(Cst::u_warning);
	connect(validity_button_, &QToolButton::clicked, this, &Number::onValidButtonClicked);

149
	switcher_layout_ = new QHBoxLayout;
150
	switcher_layout_->addWidget(number_element_, 0, Qt::AlignLeft);
151
	switcher_layout_->addWidget(switch_button_);
152
	if (options.toElement().attribute("notoggle").toLower() == "true")
153
		switch_button_->hide();
154
	switcher_layout_->addWidget(validity_button_);
155

156
	/* layout of basic elements */
Mathias Bavay's avatar
Mathias Bavay committed
157
	auto *number_layout( new QHBoxLayout );
158
	setLayoutMargins(number_layout);
159
	number_layout->addLayout(switcher_layout_);
160
161
	if (!no_spacers)
		number_layout->addSpacerItem(buildSpacer()); //keep widgets to the left
162
	addHelp(number_layout, options, no_spacers);
163
164

	/* main layout */
Mathias Bavay's avatar
Mathias Bavay committed
165
	auto *layout( new QHBoxLayout );
166
	setLayoutMargins(layout);
167
168
	if (!key_label->isEmpty())
		layout->addWidget(key_label);
169
170
171
172
173
174
	layout->addLayout(number_layout);
	this->setLayout(layout);

	setOptions(options); //min, max, default, ...
}

175
176
177
178
179
180
/**
 * @brief The destructor with minimal cleanup.
 */
Number::~Number()
{
	delete key_filter_;
181
	delete focus_filter_;
182
183
}

184
185
186
187
188
189
190
191
192
193
194
/**
 * @brief Check if the Number value is mandatory or currently at the default value.
 * @details Usually this is handled in the Atomic base class, but here we want to
 * perform numeric checks instead of string comparison.
 * @param[in] in_value The current value of the panel.
 */
void Number::setDefaultPanelStyles(const QString &in_value)
{
	bool success_inval, success_default;
	double inval = in_value.toDouble(&success_inval);
	double defval = this->property("default_value").toDouble(&success_default);
195
	const bool is_default = (success_inval && success_default && qFuzzyCompare(inval, defval));
196
197
198
199
200
201

	setPanelStyle(DEFAULT, is_default && !this->property("default_value").isNull() && !in_value.isNull());
	if (this->property("is_mandatory").toBool())
		setPanelStyle(MANDATORY, in_value.isEmpty());
}

202
203
/**
 * @brief Reset both input types, then re-set the default value.
204
 * @param[in] set_default If true, reset the value to default. If false, delete the key.
205
 */
206
void Number::clear(const bool &set_default)
207
208
{
	QString def_number_val;
209
210
211
212
213
	if (auto *spinbox1 = qobject_cast<QSpinBox *>(number_element_)) {
		if (spinbox1->minimum() > 0)
			def_number_val = QString::number(spinbox1->minimum());
		else if (spinbox1->maximum() < 0)
			def_number_val = QString::number(spinbox1->maximum());
214
215
		else
			def_number_val = "0";
216
217
218
219
220
221
		spinbox1->setValue(def_number_val.toInt());
	} else if (auto *spinbox2 = qobject_cast<QDoubleSpinBox *>(number_element_)) {
		if (spinbox2->minimum() > 0)
			def_number_val = QString::number(spinbox2->minimum());
		else if (spinbox2->maximum() < 0)
			def_number_val = QString::number(spinbox2->maximum());
222
223
		else
			def_number_val = "0";
224
		spinbox2->setValue(def_number_val.toDouble());
225
	}
226
227
	expression_element_->setText(QString());

228
229
230
	QString default_value;
	if (set_default)
		default_value = (this->property("default_value").toString());
231
232
233
234
	if (default_value.isEmpty()) {
		if (switch_button_->isChecked())
			switch_button_->animateClick();
	}
235

236
237
	this->setProperty("ini_value", ini_value_);
	this->setProperty("ini_value", default_value.isEmpty()? def_number_val : default_value);
238
	if (default_value.isEmpty()) {
239
		setIniValue(QString());
240
		QTimer::singleShot(1, this, [=]{ setEmpty(true); });
241
	}
242
243

	setDefaultPanelStyles(set_default? default_value : QString());
244
245
}

246
247
248
249
/**
 * @brief Parse options for a Number panel from XML.
 * @param[in] options XML node holding the Number panel.
 */
250
void Number::setOptions(const QDomNode &options)
251
{
252
	const QDomElement element(options.toElement());
Mathias Bavay's avatar
Mathias Bavay committed
253
254
255
	const QString maximum( element.attribute("max") );
	const QString minimum( element.attribute("min") );
	const QString unit( element.attribute("unit") );
256
	show_sign = (element.attribute("sign").toLower() == "true");
257

258
	if (mode_ == NR_DECIMAL) {
Mathias Bavay's avatar
Mathias Bavay committed
259
		auto *spinbox( qobject_cast<QDoubleSpinBox *>(number_element_) ); //cast for members
260

261
262
263
264
265
		/* precision */
		if (!element.attribute("precision").isNull()) {
			bool precision_success;
			precision_ = static_cast<int>(element.attribute("precision").toUInt(&precision_success));
			if (!precision_success) {
266
				topLog(tr(R"(XML error: Could not extract precision for Number key "%1::%2")").arg(
267
				    section_, key_), "error");
268
				precision_ = default_precision_;
Mathias Bavay's avatar
Mathias Bavay committed
269
270
			} else { //the XML declared precision becomes the new default for this field
				default_precision_ = precision_;
271
272
			}
		}
273
		spinbox->setDecimals(precision_);
274

275
		/* minimum and maximum, choose whole range if they aren't set */
276
		bool success = true;
277
		double min = minimum.isEmpty()? std::numeric_limits<double>::lowest() : minimum.toDouble(&success);
278
		if (!success)
279
			topLog(tr(R"(XML error: Could not parse minimum double value for key "%1::%2")").arg(
280
			    section_, key_), "error");
281
282
		double max = maximum.isEmpty()? std::numeric_limits<double>::max() : maximum.toDouble(&success);
		if (!success)
283
			topLog(tr(R"(XML error: Could not parse maximum double value for key "%1::%2")").arg(
284
			    section_, key_), "error");
285
		spinbox->setRange(min, max);
286
		if (element.attribute("wrap").toLower() == "true") //circular wrapping when min/max is reached
287
288
289
			spinbox->setWrapping(true);

		/* unit and sign */
290
291
		if (!unit.isEmpty()) //for the space before the unit
			spinbox->setSuffix(" " + unit);
292
293
294
		if (show_sign)
			spinbox->setPrefix("+"); //for starting 0

295
296
297
		connect(number_element_, SIGNAL(valueChanged(const double &)), this,
		    SLOT(checkValue(const double &)));
	} else { //NR_INTEGER || NR_INTEGERPLUS
Mathias Bavay's avatar
Mathias Bavay committed
298
		auto *spinbox( qobject_cast<QSpinBox *>(number_element_) );
299
300

		/* minimum, maximum and wrapping */
301
		bool success = true;
302
		int min = 0; //for integer+
303
		if (mode_ == NR_INTEGER)
304
			min = minimum.isEmpty()? std::numeric_limits<int>::lowest() : minimum.toInt(&success);
305
		if (!success)
306
			topLog(tr(R"(XML error: Could not parse maximum integer value for key "%1::%2")").arg(
307
			    section_, key_), "error");
308
309
		int max = maximum.isEmpty()? std::numeric_limits<int>::max() : maximum.toInt(&success);
		if (!success)
310
			topLog(tr(R"(XML error: Could not parse maximum integer value for key "%1::%2")").arg(
311
			    section_, key_), "error");
312
		spinbox->setRange(min, max);
313
		if (element.attribute("wrap").toLower() == "true") //circular wrapping when min/max is reached
314
315
			spinbox->setWrapping(true);

316
		/* unit */
317
318
		if (!unit.isEmpty())
			spinbox->setSuffix(" " + unit);
319
320
		if (show_sign)
			spinbox->setPrefix("+"); //for starting 0
321
322
		connect(number_element_, SIGNAL(valueChanged(const int &)), this,
		    SLOT(checkValue(const int &)));
323
	} //endif format
324
325

	/* allow to set "empty" via the property system */
326
	QString bg_color( colors::getQColor("app_bg").name() );
327
	//find font color to use for hidden spinbox text dependent on background color:
328
	if (options.toElement().attribute("optional").toLower() == "false")
Michael Reisecker's avatar
Michael Reisecker committed
329
		if (!qobject_cast<QLabel *>(getEmphasisWidget())) //the entry widget is used for styling
330
			bg_color = colors::getQColor("mandatory").name();
331
332
	number_element_->setStyleSheet("* [empty=\"true\"] {color: " + bg_color + "}");

333
334
335
	//user-set substitutions in expressions to style custom keys correctly:
	substitutions_ = expr::parseSubstitutions(options);

336
337
}

338
339
340
341
342
343
344
/**
 * @brief Extract how many decimals a number given as string has.
 * @details Looks for "," or ".".
 * @param[in] str_number The number in a string.
 */
int Number::getPrecisionOfNumber(const QString &str_number) const
{
345
	const QStringList dec( str_number.split(QRegularExpression("[,.]")) );
346
347
348
349
350
	if (dec.size() > 1) //there's a decimal sign
		return dec.at(1).length();
	return 0; //integer
}

351
/**
352
353
354
355
356
 * @brief Set an empty value for the QSpinBox.
 * @details The QSpinBox starts up with the minimum value because it has to display something,
 * even if it should be empty. Here we try to hide this value as best as we can. Qt's mechanism
 * for this (special value = special meaning) is not ideal for unbounded spin boxes.
 * @param[in] is_empty Hide text if true, show if false.
357
 */
358
void Number::setEmpty(const bool &is_empty)
359
{
360
	number_element_->setProperty("empty", is_empty);
361
362
	this->style()->unpolish(number_element_);
	this->style()->polish(number_element_);
363
}
364

365
366
367
368
369
370
/**
 * @brief Perform checks on the entered number.
 * @details Default and INI file numbers are already checked in onPropertySet(), because if we
 * receive a number here it comes from a range controlled QSpinBox and is therefore valid.
 * @param[in] to_check The double value to check.
 */
371
372
void Number::checkValue(const double &to_check)
{
373
	if (show_sign) {
374
		auto *spinbox = dynamic_cast<QDoubleSpinBox *>(number_element_);
375
		spinbox->setPrefix(to_check >= 0? "+" : "");
376
	}
377

378
	setDefaultPanelStyles(QString::number(to_check));
379
	//once something is entered it counts towards the INI file (after stylesheets):
380
	QTimer::singleShot(1, this, [=]{ setEmpty(false); });
381
	setIniValue(QString::number(to_check, 'f', precision_));
382
383
}

384
385
386
387
388
/**
 * @brief Perform checks on the entered integer number.
 * @details Coming from a QSpinBox, this value is already validated.
 * @param[in] to_check The integer value to check.
 */
389
390
void Number::checkValue(const int &to_check)
{
391
	if (show_sign) {
392
		auto *spinbox = dynamic_cast<QSpinBox *>(number_element_);
393
		spinbox->setPrefix(to_check >= 0? "+" : "");
394
	}
395
	setDefaultPanelStyles(QString::number(to_check));
396
	QTimer::singleShot(1, this, [=]{ setEmpty(false); });
397
	setIniValue(to_check);
398

399
400
}

401
402
/**
 * @brief Check an expression entered in free text mode.
403
404
405
 * @details This function checks for an expression accepted by SLF software, and if positive,
 * sets styles according to if the evaluation of said expression was successful or not.
 * @param[in] str_check The string to check for a valid expression.
406
 */
407
408
void Number::checkStrValue(const QString &str_check)
{
409
	bool evaluation_success;
410
	setDefaultPanelStyles(str_check);
411
412
413
414
415
416
417
418
419
420
421
422
423
424
	if (str_check.isEmpty()) { //"empty" not considered "invalid"
		setInvalidStyle(false); //(also gives mandatory styling priority)
		validity_button_->hide();
		return;
	}
	const bool is_expression = expr::checkExpression(str_check, evaluation_success, substitutions_);
	const bool is_valid = is_expression && evaluation_success;
	setInvalidStyle(!is_valid);
	validity_button_->setText(is_valid? Cst::u_valid : Cst::u_warning);
	validity_button_->show();
	validity_button_->setToolTip(is_valid? tr("Expression has correct syntax") : tr("Expression has wrong syntax"));
	validity_button_->setProperty("invalid", !is_valid? "true" : "false"); //set "invalid" style for additional button text
	validity_button_->style()->unpolish(validity_button_);
	validity_button_->style()->polish(validity_button_);
425
	QTimer::singleShot(1, this, [=]{ setEmpty(false); });
426
	setIniValue(str_check); //it is just a hint - save anyway
427
428
}

429
/**
430
431
432
433
434
435
 * @brief Check if a string is free text for an expression or a number.
 * @details This function is used to check which mode to enter. Since some keys can have
 * hardcoded substitutions for numbers we allow all text to switch free text mode, not just
 * expressions.
 * @param[in] expression The string to check for an number.
 * @return True if the string represents a number.
436
 */
437
bool Number::isNumber(const QString &expression) const
Michael Reisecker's avatar
Michael Reisecker committed
438
{ //note that this does not catch scientific notation, meaning it will be written out as such again
Mathias Bavay's avatar
Mathias Bavay committed
439
440
	static const QRegularExpression regex_number(R"(^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$)");
	const QRegularExpressionMatch match(regex_number.match(expression));
Michael Reisecker's avatar
Michael Reisecker committed
441
	return (match.captured(0) == expression);
442
443
}

444
445
446
447
448
/**
 * @brief Event listener for changed INI values.
 * @details The "ini_value" property is set when parsing default values and potentially again
 * when setting INI keys while parsing a file.
 */
449
void Number::onPropertySet()
450
{
Mathias Bavay's avatar
Mathias Bavay committed
451
	const QString str_value( this->property("ini_value").toString() );
452
453
	if (ini_value_ == str_value)
		return;
454

455
	if (!isNumber(str_value)) { //free text mode --> switch element and delegate checks
456
457
458
459
		expression_element_->setText(str_value);
		switch_button_->setChecked(true);
		return;
	}
460
461
462
463
464
	//should only happen when we force a redraw by setting the property empty (like in the Settings window):
	if (str_value.isEmpty()) {
		ini_value_ = QString();
		return;
	}
465

466
	if (auto *spinbox1 = qobject_cast<QSpinBox *>(number_element_)) { //integer
467
468
		bool convert_success;
		int ival = str_value.toInt(&convert_success);
469
470
		if (!convert_success) { //could also stem from XML, but let's not clutter the message for users
			topLog(tr(R"(Could not convert INI value to integer for key "%1::%2")").arg(
471
472
			    section_, key_), "warning");
			topStatus(tr("Invalid numeric INI value"), "warning");
473
474
			return;
		}
475
		if (ival < spinbox1->minimum() || ival > spinbox1->maximum()) {
476
			topLog(tr(R"(Integer INI value out of range for key "%1::%2" - truncated)").arg(
477
478
			    section_, key_), "warning");
			topStatus(tr("Truncated numeric INI value"), "warning");
479
		}
480
481
		spinbox1->setValue(str_value.toInt());
		if (ival == spinbox1->minimum()) {
482
			emit checkValue(ival); //if the default isn't changed from zero then nothing would be emitted
483
			QTimer::singleShot(1, this, [=]{ setEmpty(false); }); //avoid keeping empty style when default val is minimum spinbox1 val
484
		}
485
	} else if (auto *spinbox2 = qobject_cast<QDoubleSpinBox *>(number_element_)) { //floating point
486
487
488
		bool convert_success;
		double dval = str_value.toDouble(&convert_success);
		if (!convert_success) {
489
			topLog(tr(R"(Could not convert INI value to double for key "%1::%2")").arg(
490
491
			    section_, key_), "warning");
			topStatus(tr("Invalid numeric INI value"), "warning");
492
493
			return;
		}
494
		if (dval < spinbox2->minimum() || dval > spinbox2->maximum()) {
495
			topLog(tr(R"(Double INI value out of range for key "%1::%2" - truncated)").arg(
496
497
			    section_, key_), "warning");
			topStatus(tr("Truncated numeric INI value"), "warning");
498
499
		}

500
		const int ini_precision = getPrecisionOfNumber(str_value); //read number of decimal in INI
501
		//this also enables to overwrite in expression mode; no default in XML --> use smaller ones too:
502
		if (ini_precision > spinbox2->decimals()) {
503
			precision_ = ini_precision;
504
			spinbox2->setDecimals(precision_);
505
506
		}
		//allow to switch back to a smaller number of digits for new INI files:
507
		if (spinbox2->decimals() > std::max(ini_precision, default_precision_)) {
508
			precision_ = std::max(ini_precision, default_precision_);
509
510
			if (precision_ == 0)
				precision_ = 1; //force at least 1 digit
511
			spinbox2->setDecimals(precision_);
512
		}
513

514
515
		spinbox2->setValue(str_value.toDouble());
		spinbox2->setDecimals(precision_); //needs to be re-set every time
516

517
		if (qFuzzyCompare(dval, spinbox2->minimum())) { //fuzzy against warnings
518
			emit checkValue(dval); //cf. above
519
520
			QTimer::singleShot(1, this, [=]{ setEmpty(false); });
		}
521
522
523
524
		//At this point the INI value is already set, but since this is coming from an actual INI
		//file (or the XML) we reset to the exact value to have the same precision and circumvent
		//"unsaved changes" warnings resp. a numeric check in the INIParser's == operator:
		setIniValue(str_value);
525
526
	}
}
527

528
529
530
531
532
/**
 * @brief Toggle between number and (arithmetic) expression mode.
 * @details This function shows/hides the spin box/text field and initiates the necessary checks.
 * @param[in] checked True if entering expression mode.
 */
533
534
void Number::switchToggle(bool checked)
{
535
	if (checked) { //(arithmetic) expression / free text mode
536
537
538
		switcher_layout_->replaceWidget(number_element_, expression_element_);
		number_element_->hide();
		expression_element_->show();
539
		validity_button_->show();
540
541
		if (!qobject_cast<QLabel*>(getEmphasisWidget()))
			setEmphasisWidget(expression_element_); //switch widget to style with properties
542
		setIniValue(expression_element_->text()); //always use the visible number in INI
543
		setDefaultPanelStyles(expression_element_->text());
544
		checkStrValue(expression_element_->text());
545
	} else { //spin box mode
546
547
		switcher_layout_->replaceWidget(expression_element_, number_element_);
		expression_element_->hide();
548
549
		validity_button_->hide();
		setInvalidStyle(false);
550
		number_element_->show();
551
552
		if (!qobject_cast<QLabel*>(getEmphasisWidget()))
			setEmphasisWidget(number_element_);
553
		if (mode_ == NR_DECIMAL) {
554
			setIniValue(dynamic_cast<QDoubleSpinBox *>(number_element_)->value());
555
			setDefaultPanelStyles(
556
			    QString::number(dynamic_cast<QDoubleSpinBox *>(number_element_)->value()));
557
		} else {
558
			setIniValue(dynamic_cast<QSpinBox *>(number_element_)->value());
559
			setDefaultPanelStyles(
560
			    QString::number(dynamic_cast<QSpinBox *>(number_element_)->value()));
561
		}
562
563
	}
}
564
565
566
567
568
569
570
571
572
573

/**
 * @brief Event listener for the action button.
 * @details That button displays info about valid/invalid expressions
 * and is clickable to go to the help files.
 */
void Number::onValidButtonClicked()
{
	getMainWindow()->loadHelp("Input panels 1", "help-number");
}