WSL/SLF GitLab Repository

Textfield.cc 10.2 KB
Newer Older
Mathias Bavay's avatar
Mathias Bavay committed
1
2
//SPDX-License-Identifier: GPL-3.0-or-later
/*****************************************************************************/
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
24
#include <src/panels/Textfield.h>
#include <src/panels/Label.h>
#include <src/main/constants.h>
#include <src/main/expressions.h>
#include <src/main/inishell.h>
25

26
#include <QDesktopServices>
27
#include <QFontMetrics>
28
29
#include <QHBoxLayout>

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

34
35
36
37
38
// //regular expressions for validation and parsing of coordinates
const QString Textfield::regex_wgs84_decimal = R"(\Alatlon\s*\(([-\d\.]+)(?:,)\s*([-\d\.]+)(?:,\s*([-\d\.]+))?\))";
const QString Textfield::regex_wgs84_dms = R"(\Alatlon\s*\(([-\d\.]+d\s*[-\d\.]*'?\s*[-\d\.]*"?)(?:,)\s*([-\d\.]+d\s*[-\d\.']*'?\s*[-\d\.]*"?)(?:,\s*([-\d\.]+))?\))";
const QString Textfield::regex_xy = R"(\Axy\s*\(([-\d\.]+)(?:,)\s*([-\d\.]+)(?:,\s*([-\d\.]+))?\))";

39
40
41
42
/**
 * @class Textfield
 * @brief Default constructor for a Textfield.
 * @details A Textfield is used to enter plain text.
Michael Reisecker's avatar
Michael Reisecker committed
43
 * @param[in] section INI section the controlled value belongs to.
44
45
46
47
48
49
50
51
52
 * @param[in] key INI key corresponding to the value that is being controlled by this Textfield.
 * @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.
 */
Textfield::Textfield(const QString &section, const QString &key, const QDomNode &options, const bool &no_spacers,
    QWidget *parent) : Atomic(section, key, parent)
{
	/* label and text box */
53
	auto *key_label( new Label(QString(), QString(), options, no_spacers, key_, this) );
54
	setEmphasisWidget(key_label->label_);
55
	textfield_ = new QLineEdit; //single line text box
56
	connect(textfield_, &QLineEdit::textEdited, this, &Textfield::checkValue);
57
58
	focus_filter_ = new FocusEventFilter;
	textfield_->installEventFilter(focus_filter_); //select all text on focus receive
59

60
	/* action button */
61
62
	validity_button_ = new QToolButton; //a button that can pop up if the text has a certain format
	validity_button_->setStyleSheet("* {border: none}");
63
	QSize sz_label;
Mathias Bavay's avatar
Mathias Bavay committed
64
65
66
	static const int maxSymbolWidth = std::max(fontMetrics().boundingRect(Cst::u_globe).width(), fontMetrics().boundingRect(Cst::u_warning).width());
	sz_label.setWidth(maxSymbolWidth);
	sz_label.setHeight(fontMetrics().boundingRect(Cst::u_globe).height());
67
	validity_button_->setFixedSize(sz_label);
68
	validity_button_->setFocusPolicy(Qt::NoFocus); //unexpected tab stops when invisible otherwise
69
	connect(validity_button_, &QToolButton::clicked, this, &Textfield::onValidButtonClicked);
70

71
	/* layout of textbox plus button and the main layout */
72
73
74
	auto *validity_button_layout( new QHBoxLayout );
	validity_button_layout->addWidget(textfield_);
	validity_button_layout->addWidget(validity_button_);
Mathias Bavay's avatar
Mathias Bavay committed
75
	auto *textfield_layout( new QHBoxLayout );
76
77
	setLayoutMargins(textfield_layout);
	textfield_layout->addWidget(key_label, 0, Qt::AlignLeft);
78
	textfield_layout->addLayout(validity_button_layout);
79
80

	/* choose size of text box */
Mathias Bavay's avatar
Mathias Bavay committed
81
	const QString size( options.toElement().attribute("size") );
82
	if (size.toLower() == "small")
83
		textfield_->setMinimumWidth(Cst::tiny);
84
85
	else
		textfield_->setMinimumWidth(Cst::width_textbox_medium);
86
87
88
	if (!no_spacers && size.toLower() != "large") //"large": text box extends to window width
		textfield_layout->addSpacerItem(buildSpacer());

89
	addHelp(textfield_layout, options, no_spacers);
90
	this->setLayout(textfield_layout);
91
	setOptions(options);
92
93
}

94
/**
Michael Reisecker's avatar
Michael Reisecker committed
95
 * @brief Default destructor with minimal cleanup.
96
 */
97
98
99
Textfield::~Textfield()
{
	delete focus_filter_;
100
101
102
}

/**
103
104
 * @brief Parse options for a Textfield from XML.
 * @param[in] options XML node holding the Textfield.
105
106
107
108
 */
void Textfield::setOptions(const QDomNode &options)
{
	validation_regex_ = options.toElement().attribute("validate");
109
	if (options.toElement().attribute("lenient").toLower() == "true")
110
		needs_prefix_for_evaluation_ = false; //always evaluate arithmetic expressions
111
112
	if (!options.toElement().attribute("placeholder").isEmpty())
		textfield_->setPlaceholderText(options.toElement().attribute("placeholder"));
113
114
115

	//user-set substitutions in expressions to style custom keys correctly:
	substitutions_ = expr::parseSubstitutions(options);
116
117
}

118
119
120
121
122
/**
 * @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.
 */
123
124
void Textfield::onPropertySet()
{
Mathias Bavay's avatar
Mathias Bavay committed
125
	const QString text_to_set( this->property("ini_value").toString() );
126
127
	if (ini_value_ == text_to_set)
		return;
128
	textfield_->setText(text_to_set);
129
	checkValue(text_to_set);
130
131
}

132
133
134
135
136
137
/**
 * @brief Perform checks on the entered text.
 * @details This function checks if the entered text stands for a recognized format such
 * as expressions (like the Number panel does), and if yes, styles the text box.
 * @param[in] text The current text to check.
 */
138
139
void Textfield::checkValue(const QString &text)
{
140
	setDefaultPanelStyles(text);
141
142
143
144
145
146
147
148
149
	static const int idx_full = 0; //regex index

	if (validation_regex_.isNull()) { //check for (arithmetic) expressions

		bool evaluation_success;
		const bool is_expression = expr::checkExpression(text, evaluation_success,
			substitutions_, needs_prefix_for_evaluation_);
		const bool is_invalid = is_expression && !evaluation_success;
		setInvalidStyle(is_invalid);
150
151
		//we use a fixed-width button for this because a Textfield can span the whole line
		//--> then those with and without icons are still aligned
152
153
154
		validity_button_->setText(is_invalid? Cst::u_warning : (is_expression? Cst::u_valid : ""));
		validity_button_->setCursor(is_expression? Qt::PointingHandCursor : Qt::ArrowCursor);
		validity_button_->setToolTip(is_expression?
Mathias Bavay's avatar
Mathias Bavay committed
155
			(is_invalid? tr("Wrong format for the expression") : tr("Valid expression")) : "");
156
157
158
		validity_button_->setProperty("invalid", is_invalid? "true" : "false");
		validity_button_->style()->unpolish(validity_button_);
		validity_button_->style()->polish(validity_button_);
159
160
161

	} else { //check against regex specified in XML

Mathias Bavay's avatar
Mathias Bavay committed
162
			const bool isCoords = (validation_regex_.compare("coordinates", Qt::CaseInsensitive)==0);
163
			static const QString regex_coordinates_ = regex_wgs84_decimal+"|"+regex_xy+"|"+regex_wgs84_dms;
164
			const QString regex = isCoords? regex_coordinates_ : validation_regex_;
Mathias Bavay's avatar
Mathias Bavay committed
165
166
167
168
169
			const QString u_valid = isCoords? Cst::u_globe : Cst::u_valid;
			const QString toolTipValid = isCoords? tr("Show online map") : tr("Valid expression");
			const QString toolTipInvalid = isCoords? tr("Wrong format for the provided coordinates") : tr("Wrong format for the provided value");
			
			const QRegularExpression rex_xml(regex);
170
			const QRegularExpressionMatch xml_match(rex_xml.match(text));
171
			const bool is_invalid = xml_match.captured(idx_full) != text && !text.isEmpty();
Mathias Bavay's avatar
Mathias Bavay committed
172
			
173
			setInvalidStyle(is_invalid);
Mathias Bavay's avatar
Mathias Bavay committed
174
			validity_button_->setText(is_invalid? Cst::u_warning : (text.isEmpty()? "" : u_valid));
175
			validity_button_->setCursor(text.isEmpty()? Qt::ArrowCursor : Qt::PointingHandCursor); //mark as link
Mathias Bavay's avatar
Mathias Bavay committed
176
177
			validity_button_->setToolTip(text.isEmpty()? "" : (is_invalid? toolTipInvalid : toolTipValid));
			
178
			//set "invalid" style on navigation button also:
179
180
181
			validity_button_->setProperty("invalid", is_invalid? "true" : "false");
			validity_button_->style()->unpolish(validity_button_);
			validity_button_->style()->polish(validity_button_);
182

183
184
185
186
	}
	setIniValue(text); //checks are just a hint - set anyway
}

187
188
/**
 * @brief Event listener for the action button.
189
190
191
 * @details The button is only shown for coordinates and will open the Browser
 * with a map link, or it acts to display info about valid/invalid expressions
 * (then leading to the help files).
192
 */
193
void Textfield::onValidButtonClicked() //for now this button only pops up for coordinates
194
{
195
	if (validity_button_->text().isEmpty())
196
		return;
197
	if (validity_button_->text() != Cst::u_globe) { //"valid" resp. "invalid" icons
198
199
200
201
		getMainWindow()->loadHelp("Input panels 1", "help-textfield");
		return;
	}

202
	//capture the lat and lon components to build the geohacks URL
203
204
205
206
	static const QString regex_coordinates_( regex_wgs84_decimal+"|"+regex_wgs84_dms );
	static const int idx_lat = 1, idx_lon = 2, idx_lat_dms = 4, idx_lon_dms = 5; //altitudes are at positions 3 and 6
	
	static const QRegularExpression rex_coord(regex_coordinates_);
207
	const QRegularExpressionMatch coord_match(rex_coord.match(textfield_->text()));
208
209
210
211
	QString url("https://tools.wmflabs.org/geohack/geohack.php?params=");
	
	if (!coord_match.captured(idx_lat).isEmpty() && !coord_match.captured(idx_lon).isEmpty()) {
		//decimal wgs84
212
213
214
215
216
		const QChar latchar = coord_match.captured(idx_lat).toDouble() < 0? 'S' : 'N';
		const QChar lonchar = coord_match.captured(idx_lon).toDouble() < 0? 'W' : 'E';
		url += coord_match.captured(idx_lat) + "_" + latchar + "_";
		url += coord_match.captured(idx_lon) + "_" + lonchar;
		QDesktopServices::openUrl(url);
217
218
219
220
221
222
	} else if(!coord_match.captured(idx_lat_dms).isEmpty() && !coord_match.captured(idx_lon_dms).isEmpty()) {
		//dms wgs84
		QString lat( coord_match.captured(idx_lat_dms) );
		QString lon( coord_match.captured(idx_lon_dms) );
		const QChar latchar = lat.section('d',1,1).toDouble() < 0? 'S' : 'N';
		const QChar lonchar = lon.section('d',1,1).toDouble() < 0? 'W' : 'E';
223
224
		lat.remove(QRegularExpression(" |\"")); lat.replace(QRegularExpression("d|'"), "_");
		lon.remove(QRegularExpression(" |\"")); lon.replace(QRegularExpression("d|'"), "_");
225
226
227
228
		
		url += lat + "_" + latchar + "_";
		url += lon + "_" + lonchar;
		QDesktopServices::openUrl(url);
229
	}
230
}