WSL/SLF GitLab Repository

Number.cc 20.8 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

#include "Number.h"
#include "src/main/colors.h"
21
#include "src/main/expressions.h"
22
23
24
25
#include "src/main/inishell.h"

#include <QDoubleSpinBox>
#include <QSpinBox>
26
#include <QTimer>
27

28
#include <algorithm> //for max()
29
30
#include <climits> //for the number panel limits

31
32
33
34
#ifdef DEBUG
	#include <iostream>
#endif //def DEBUG

35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * @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);
48
		if ((key_event->key() >= Qt::Key_0 && key_event->key() <= Qt::Key_9) || (key_event->key() == Qt::Key_Minus)) {
49
			if (object->property("empty").toBool()) {
50
51
52
				/*
				 * 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
53
				 * this hidden value contributes. Here, we prevent this.
54
55
56
				 */
				object->setProperty("empty", "false"); //necessary if entered number happens to be the hidden value
				if (auto *spinbox = qobject_cast<QSpinBox *>(object)) { //try both types
57
					spinbox->parent()->setProperty("ini_value", key_event->key() - Qt::Key_0);
58
59
60
					spinbox->style()->unpolish(spinbox);
					spinbox->style()->polish(spinbox);
				} else if (auto *spinbox = qobject_cast<QDoubleSpinBox *>(object)) {
61
					spinbox->parent()->setProperty("ini_value", key_event->key() - Qt::Key_0);
62
63
64
65
66
67
68
69
70
71
					spinbox->style()->unpolish(spinbox);
					spinbox->style()->polish(spinbox);
				}
				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
}

72
73
74
75
/**
 * @class Number
 * @brief Default constructor for a Number panel.
 * @details A number panel displays and manipulates a float or integer value.
76
77
 * It can also switch to free text mode, allowing to enter an expression (according to SLF software
 * syntax) which it will check.
78
79
80
81
82
83
84
85
86
87
 * @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
88
	const QString format( options.toElement().attribute("format") );
89
	if (format == "decimal" || format.isEmpty()) {
90
		number_element_ = new QDoubleSpinBox;
91
		mode_ = NR_DECIMAL;
92
		//Note that the mode concerns only the QSpinBox, arithmetic expressions are decoupled
93
94
	} else if (format == "integer" || format == "integer+") {
		number_element_ = new QSpinBox;
95
		mode_ = (format == "integer")? NR_INTEGER : NR_INTEGERPLUS;
96
	} else {
Mathias Bavay's avatar
Mathias Bavay committed
97
		topLog(tr(R"(XML error: unknown number format for key "%1::%2")").arg(
98
		    section_, key_), "error");
99
		return;
100
	}
101
	QTimer::singleShot(1, this, [=]{ setEmpty(true); }); //indicate that even if 0 is displayed, nothing is set yet
102

103
	auto *key_label( new Label(QString(), QString(), options, no_spacers, key_, this) );
104
	if (key_label->isEmpty())
105
		setEmphasisWidget(number_element_); //start with SpinBox and switch if needed
106
	else
107
		setEmphasisWidget(key_label->label_);
Michael Reisecker's avatar
Michael Reisecker committed
108
	number_element_->setFixedWidth(Cst::width_number_min);
109
110
	key_filter_ = new KeyPressFilter;
	number_element_->installEventFilter(key_filter_);
111

112
	/* free text expression entering  */
113
114
	expression_element_ = new QLineEdit(this);
	expression_element_->hide();
Michael Reisecker's avatar
Michael Reisecker committed
115
	expression_element_->setFixedWidth(Cst::width_number_min);
116
	expression_element_->setToolTip(number_element_->toolTip());
117
	connect(expression_element_, &QLineEdit::textChanged, this, &Number::checkStrValue);
118
119

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

	switcher_layout_ = new QHBoxLayout;
130
	switcher_layout_->addWidget(number_element_, 0, Qt::AlignLeft);
131
	switcher_layout_->addWidget(switch_button_);
132
	if (options.toElement().attribute("notoggle").toLower() == "true")
133
		switch_button_->hide();
134

135
	/* layout of basic elements */
Mathias Bavay's avatar
Mathias Bavay committed
136
	auto *number_layout( new QHBoxLayout );
137
	setLayoutMargins(number_layout);
138
	number_layout->addLayout(switcher_layout_);
139
140
	if (!no_spacers)
		number_layout->addSpacerItem(buildSpacer()); //keep widgets to the left
141
	addHelp(number_layout, options, no_spacers);
142
143

	/* main layout */
Mathias Bavay's avatar
Mathias Bavay committed
144
	auto *layout( new QHBoxLayout );
145
	setLayoutMargins(layout);
146
147
	if (!key_label->isEmpty())
		layout->addWidget(key_label);
148
149
150
151
152
153
	layout->addLayout(number_layout);
	this->setLayout(layout);

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

154
155
156
157
158
159
160
161
/**
 * @brief The destructor with minimal cleanup.
 */
Number::~Number()
{
	delete key_filter_;
}

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/**
 * @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 is_default = false;
	bool success_inval, success_default;
	double inval = in_value.toDouble(&success_inval);
	double defval = this->property("default_value").toDouble(&success_default);
	is_default = (success_inval && success_default && qFuzzyCompare(inval, defval));

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

181
182
/**
 * @brief Reset both input types, then re-set the default value.
183
 * @param[in] set_default If true, reset the value to default. If false, delete the key.
184
 */
185
void Number::clear(const bool &set_default)
186
187
188
189
190
191
192
193
194
{
	QString def_number_val;
	if (auto *spinbox = qobject_cast<QSpinBox *>(number_element_)) {
		if (spinbox->minimum() > 0)
			def_number_val = QString::number(spinbox->minimum());
		else if (spinbox->maximum() < 0)
			def_number_val = QString::number(spinbox->maximum());
		else
			def_number_val = "0";
195
		spinbox->setValue(def_number_val.toInt());
196
197
198
199
200
201
202
203
	} else if (auto *spinbox = qobject_cast<QDoubleSpinBox *>(number_element_)) {
		if (spinbox->minimum() > 0)
			def_number_val = QString::number(spinbox->minimum());
		else if (spinbox->maximum() < 0)
			def_number_val = QString::number(spinbox->maximum());
		else
			def_number_val = "0";
		spinbox->setValue(def_number_val.toDouble());
204
	}
205
206
	expression_element_->setText(QString());

207
208
209
	QString default_value;
	if (set_default)
		default_value = (this->property("default_value").toString());
210
211
212
213
	if (default_value.isEmpty()) {
		if (switch_button_->isChecked())
			switch_button_->animateClick();
	}
214

215
216
	this->setProperty("ini_value", ini_value_);
	this->setProperty("ini_value", default_value.isEmpty()? def_number_val : default_value);
217
	if (default_value.isEmpty()) {
218
		setIniValue(QString());
219
		QTimer::singleShot(1, this, [=]{ setEmpty(true); });
220
	}
221
222

	setDefaultPanelStyles(set_default? default_value : QString());
223
224
}

225
226
227
228
/**
 * @brief Parse options for a Number panel from XML.
 * @param[in] options XML node holding the Number panel.
 */
229
void Number::setOptions(const QDomNode &options)
230
{
231
	const QDomElement element(options.toElement());
Mathias Bavay's avatar
Mathias Bavay committed
232
233
234
	const QString maximum( element.attribute("max") );
	const QString minimum( element.attribute("min") );
	const QString unit( element.attribute("unit") );
235
	show_sign = (element.attribute("sign").toLower() == "true");
236

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

240
241
242
243
244
		/* precision */
		if (!element.attribute("precision").isNull()) {
			bool precision_success;
			precision_ = static_cast<int>(element.attribute("precision").toUInt(&precision_success));
			if (!precision_success) {
245
				topLog(tr(R"(XML error: Could not extract precision for Number key "%1::%2")").arg(
246
				    section_, key_), "error");
247
				precision_ = default_precision_;
Mathias Bavay's avatar
Mathias Bavay committed
248
249
			} else { //the XML declared precision becomes the new default for this field
				default_precision_ = precision_;
250
251
			}
		}
252
		spinbox->setDecimals(precision_);
253

254
		/* minimum and maximum, choose whole range if they aren't set */
255
		bool success = true;
256
		double min = minimum.isEmpty()? std::numeric_limits<double>::lowest() : minimum.toDouble(&success);
257
		if (!success)
258
			topLog(tr(R"(XML error: Could not parse minimum double value for key "%1::%2")").arg(
259
			    section_, key_), "error");
260
261
		double max = maximum.isEmpty()? std::numeric_limits<double>::max() : maximum.toDouble(&success);
		if (!success)
262
			topLog(tr(R"(XML error: Could not parse maximum double value for key "%1::%2")").arg(
263
			    section_, key_), "error");
264
		spinbox->setRange(min, max);
265
		if (element.attribute("wrap").toLower() == "true") //circular wrapping when min/max is reached
266
267
268
			spinbox->setWrapping(true);

		/* unit and sign */
269
270
		if (!unit.isEmpty()) //for the space before the unit
			spinbox->setSuffix(" " + unit);
271
272
273
		if (show_sign)
			spinbox->setPrefix("+"); //for starting 0

274
275
276
		connect(number_element_, SIGNAL(valueChanged(const double &)), this,
		    SLOT(checkValue(const double &)));
	} else { //NR_INTEGER || NR_INTEGERPLUS
Mathias Bavay's avatar
Mathias Bavay committed
277
		auto *spinbox( qobject_cast<QSpinBox *>(number_element_) );
278
279

		/* minimum, maximum and wrapping */
280
		bool success = true;
281
		int min = 0; //for integer+
282
		if (mode_ == NR_INTEGER)
283
			min = minimum.isEmpty()? std::numeric_limits<int>::lowest() : minimum.toInt(&success);
284
		if (!success)
285
			topLog(tr(R"(XML error: Could not parse maximum integer value for key "%1::%2")").arg(
286
			    section_, key_), "error");
287
288
		int max = maximum.isEmpty()? std::numeric_limits<int>::max() : maximum.toInt(&success);
		if (!success)
289
			topLog(tr(R"(XML error: Could not parse maximum integer value for key "%1::%2")").arg(
290
			    section_, key_), "error");
291
		spinbox->setRange(min, max);
292
		if (element.attribute("wrap").toLower() == "true") //circular wrapping when min/max is reached
293
294
			spinbox->setWrapping(true);

295
		/* unit */
296
297
		if (!unit.isEmpty())
			spinbox->setSuffix(" " + unit);
298
299
		if (show_sign)
			spinbox->setPrefix("+"); //for starting 0
300
301
		connect(number_element_, SIGNAL(valueChanged(const int &)), this,
		    SLOT(checkValue(const int &)));
302
	} //endif format
303
304

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

312
313
314
	//user-set substitutions in expressions to style custom keys correctly:
	substitutions_ = expr::parseSubstitutions(options);

315
316
}

317
318
319
320
321
322
323
/**
 * @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
{
Mathias Bavay's avatar
Mathias Bavay committed
324
	const QStringList dec( str_number.split(QRegExp("[,.]")) );
325
326
327
328
329
	if (dec.size() > 1) //there's a decimal sign
		return dec.at(1).length();
	return 0; //integer
}

330
/**
331
332
333
334
335
 * @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.
336
 */
337
void Number::setEmpty(const bool &is_empty)
338
{
339
	number_element_->setProperty("empty", is_empty);
340
341
	this->style()->unpolish(number_element_);
	this->style()->polish(number_element_);
342
}
343

344
345
346
347
348
349
/**
 * @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.
 */
350
351
void Number::checkValue(const double &to_check)
{
352
	if (show_sign) {
353
		auto *spinbox = dynamic_cast<QDoubleSpinBox *>(number_element_);
354
		spinbox->setPrefix(to_check >= 0? "+" : "");
355
	}
356

357
	setDefaultPanelStyles(QString::number(to_check));
358
	//once something is entered it counts towards the INI file (after stylesheets):
359
	QTimer::singleShot(1, this, [=]{ setEmpty(false); });
360
	setIniValue(QString::number(to_check, 'f', precision_));
361
362
}

363
364
365
366
367
/**
 * @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.
 */
368
369
void Number::checkValue(const int &to_check)
{
370
	if (show_sign) {
371
		auto *spinbox = dynamic_cast<QSpinBox *>(number_element_);
372
		spinbox->setPrefix(to_check >= 0? "+" : "");
373
	}
374
	setDefaultPanelStyles(QString::number(to_check));
375
	QTimer::singleShot(1, this, [=]{ setEmpty(false); });
376
	setIniValue(to_check);
377

378
379
}

380
381
/**
 * @brief Check an expression entered in free text mode.
382
383
384
 * @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.
385
 */
386
387
void Number::checkStrValue(const QString &str_check)
{
388
	bool evaluation_success;
389
390
	setDefaultPanelStyles(str_check);

391
	if (expr::checkExpression(str_check, evaluation_success, substitutions_) || !evaluation_success)
Michael Reisecker's avatar
Michael Reisecker committed
392
		setInvalidStyle(!evaluation_success && !str_check.isEmpty());
393
	QTimer::singleShot(1, this, [=]{ setEmpty(false); });
394
	setIniValue(str_check); //it is just a hint - save anyway
395
396
}

397
/**
398
399
400
401
402
403
 * @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.
404
 */
405
bool Number::isNumber(const QString &expression) const
Michael Reisecker's avatar
Michael Reisecker committed
406
{ //note that this does not catch scientific notation, meaning it will be written out as such again
Mathias Bavay's avatar
Mathias Bavay committed
407
408
	static const QRegularExpression regex_number(R"(^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$)");
	const QRegularExpressionMatch match(regex_number.match(expression));
Michael Reisecker's avatar
Michael Reisecker committed
409
	return (match.captured(0) == expression);
410
411
}

412
413
414
415
416
/**
 * @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.
 */
417
void Number::onPropertySet()
418
{
Mathias Bavay's avatar
Mathias Bavay committed
419
	const QString str_value( this->property("ini_value").toString() );
420
421
	if (ini_value_ == str_value)
		return;
422

423
	if (!isNumber(str_value)) { //free text mode --> switch element and delegate checks
424
425
426
427
		expression_element_->setText(str_value);
		switch_button_->setChecked(true);
		return;
	}
428
429
430
431
432
	//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;
	}
433

434
	if (auto *spinbox = qobject_cast<QSpinBox *>(number_element_)) { //integer
435
436
		bool convert_success;
		int ival = str_value.toInt(&convert_success);
437
438
		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(
439
440
			    section_, key_), "warning");
			topStatus(tr("Invalid numeric INI value"), "warning");
441
442
			return;
		}
443
444
		if (ival < spinbox->minimum() || ival > spinbox->maximum()) {
			topLog(tr(R"(Integer INI value out of range for key "%1::%2" - truncated)").arg(
445
446
			    section_, key_), "warning");
			topStatus(tr("Truncated numeric INI value"), "warning");
447
448
		}
		spinbox->setValue(str_value.toInt());
449
		if (ival == spinbox->minimum()) {
450
			emit checkValue(ival); //if the default isn't changed from zero then nothing would be emitted
451
452
			QTimer::singleShot(1, this, [=]{ setEmpty(false); }); //avoid keeping empty style when default val is minimum spinbox val
		}
453
	} else if (auto *spinbox = qobject_cast<QDoubleSpinBox *>(number_element_)) { //floating point
454
455
456
		bool convert_success;
		double dval = str_value.toDouble(&convert_success);
		if (!convert_success) {
457
			topLog(tr(R"(Could not convert INI value to double for key "%1::%2")").arg(
458
459
			    section_, key_), "warning");
			topStatus(tr("Invalid numeric INI value"), "warning");
460
461
			return;
		}
462
463
		if (dval < spinbox->minimum() || dval > spinbox->maximum()) {
			topLog(tr(R"(Double INI value out of range for key "%1::%2" - truncated)").arg(
464
465
			    section_, key_), "warning");
			topStatus(tr("Truncated numeric INI value"), "warning");
466
467
		}

468
		const int ini_precision = getPrecisionOfNumber(str_value); //read number of decimal in INI
469
		//this also enables to overwrite in expression mode; no default in XML --> use smaller ones too:
470
471
		if (ini_precision > spinbox->decimals()) {
			precision_ = ini_precision;
472
473
474
			spinbox->setDecimals(precision_);
		}
		//allow to switch back to a smaller number of digits for new INI files:
475
476
		if (spinbox->decimals() > std::max(ini_precision, default_precision_)) {
			precision_ = std::max(ini_precision, default_precision_);
477
478
			if (precision_ == 0)
				precision_ = 1; //force at least 1 digit
479
480
			spinbox->setDecimals(precision_);
		}
481

482
		spinbox->setValue(str_value.toDouble());
483
484
		spinbox->setDecimals(precision_); //needs to be re-set every time

485
		if (qFuzzyCompare(dval, spinbox->minimum())) { //fuzzy against warnings
486
			emit checkValue(dval); //cf. above
487
488
			QTimer::singleShot(1, this, [=]{ setEmpty(false); });
		}
489
490
491
492
		//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);
493
494
	}
}
495

496
497
498
499
500
/**
 * @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.
 */
501
502
void Number::switchToggle(bool checked)
{
503
	if (checked) { //(arithmetic) expression / free text mode
504
505
506
		switcher_layout_->replaceWidget(number_element_, expression_element_);
		number_element_->hide();
		expression_element_->show();
507
508
		if (!qobject_cast<QLabel*>(getEmphasisWidget()))
			setEmphasisWidget(expression_element_); //switch widget to style with properties
509
		setIniValue(expression_element_->text()); //always use the visible number in INI
510
		setDefaultPanelStyles(expression_element_->text());
511
		checkStrValue(expression_element_->text());
512
	} else { //spin box mode
513
514
515
		switcher_layout_->replaceWidget(expression_element_, number_element_);
		expression_element_->hide();
		number_element_->show();
516
517
		if (!qobject_cast<QLabel*>(getEmphasisWidget()))
			setEmphasisWidget(number_element_);
518
		if (mode_ == NR_DECIMAL) {
519
			setIniValue(dynamic_cast<QDoubleSpinBox *>(number_element_)->value());
520
			setDefaultPanelStyles(
521
			    QString::number(dynamic_cast<QDoubleSpinBox *>(number_element_)->value()));
522
		} else {
523
			setIniValue(dynamic_cast<QSpinBox *>(number_element_)->value());
524
			setDefaultPanelStyles(
525
			    QString::number(dynamic_cast<QSpinBox *>(number_element_)->value()));
526
		}
527
528
	}
}