WSL/SLF GitLab Repository

Atomic.cc 18.4 KB
Newer Older
Michael Reisecker's avatar
Michael Reisecker committed
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
Michael Reisecker's avatar
Michael Reisecker committed
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.
Michael Reisecker's avatar
Michael Reisecker committed
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/>.
Michael Reisecker's avatar
Michael Reisecker committed
17
*/
18
19

#include "Atomic.h"
20
#include "src/main/constants.h"
21
#include "src/main/inishell.h"
22
#include "src/main/os.h"
23

24
#include <QAction>
Michael Reisecker's avatar
Michael Reisecker committed
25
#include <QCryptographicHash>
26
#include <QCursor>
27
#include <QFontMetrics>
Michael Reisecker's avatar
Michael Reisecker committed
28

29
#include <utility>
30

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

35
36
37
38
39
40
41
42
43
44
45
46
/**
 * @class Atomic
 * @brief Base class of most panels.
 * @details The panels inherit from this class which provides some common functionality
 * like handling the object IDs.
 * @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 panel.
 * @param[in] parent The parent widget.
 */
Atomic::Atomic(QString section, QString key, QWidget *parent)
    : QWidget(parent), section_(std::move(section)), key_(std::move(key))
{
Michael Reisecker's avatar
Michael Reisecker committed
47
	ini_ = getMainWindow()->getIni();
48
	createContextMenu();
49
50
51
52

	style_timer_.setSingleShot(true);
	style_timer_.setInterval(Cst::msg_short_length);
	connect(&style_timer_, &QTimer::timeout, this, [this]{ this->setHighlightedStyle(false); });
Michael Reisecker's avatar
Michael Reisecker committed
53
54
55
56
57
58
59
60
}

/**
 * @brief Check if a panel value is mandatory or currently at the default value.
 * @param[in] in_value The current value of the panel.
 */
void Atomic::setDefaultPanelStyles(const QString &in_value)
{
61
62
	const bool is_default = (QString::compare(in_value, this->property("default_value").toString(),
	    Qt::CaseInsensitive) == 0);
63
	setPanelStyle(DEFAULT, is_default && !this->property("default_value").isNull() && !in_value.isNull());
64
65
66
67
	if (this->property("is_mandatory").toBool()) {
		const bool missing = in_value.isEmpty();
		setPanelStyle(MANDATORY, missing);
	}
Michael Reisecker's avatar
Michael Reisecker committed
68
69
70
71
72
}

/**
 * @brief Get an alphanumeric key from an aribtrary one.
 * @details Unfortunately, our arg1::arg2 syntax runs into Qt's sub-controls syntax, but the targeting
73
74
75
 * with stylesheets only allows _ and - anyway so we hash the ID. This also ensures that all object
 * names we set manually that contain an underscore or dash do not coincide with an INI setting name.
 *
Michael Reisecker's avatar
Michael Reisecker committed
76
77
78
79
 * @param[in] ini_key Key as given in the XML
 * @return A key without special chars.
 */
QString Atomic::getQtKey(const QString &ini_key)
80
81
82
{ //sounds heavy but is fast
	return QString(QCryptographicHash::hash((ini_key.toLower().toLocal8Bit()),
	    QCryptographicHash::Md5).toHex());
83
84
}

85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
 * @brief Return this panel's set INI value.
 * @details This function is called by the main output routine for all panels.
 * @param[out] section Gets set to this panel's section.
 * @param[out] key Gets stored to this panel's key.
 * @return This panel's value.
 */
QString Atomic::getIniValue(QString &section, QString &key) const noexcept
{
	section = section_;
	key = key_;
	return ini_value_;
}

99
100
/**
 * @brief Reset panel to the default value.
101
 * @param[in] set_default If true, reset the value to default. If false, delete the key.
102
 */
103
void Atomic::clear(const bool &set_default)
104
105
106
107
108
109
{
	/*
	 * The property needs to actually change to have a signal emitted. So first we set
	 * the property (since we set it to ini_value_, a check in onPropertySet() will
	 * make sure that nothing is calculated), and then set it to the default value
	 * (which will only take effect if it's different).
110
111
	 * TODO: This means unneccessary calls that may not be trivial (depending on the panel),
	 * so a redesign could be desired.
112
113
	 */
	this->setProperty("ini_value", ini_value_);
114
	this->setProperty("ini_value", set_default? this->property("default_value") : QString());
115
116
}

117
118
119
120
/**
 * @brief Set property to highlight this widget.
 * @details This is in addition to mandatory and valid styles, and is used
 * for example to link to panels.
121
122
 * @param[in] on True to highlight, false for normal.
 * @param[in] reset_interval Time after which to reset to normal (ms).
123
 */
124
void Atomic::setHighlightedStyle(const bool &on, const int &reset_interval)
125
{
126
	Group *me = qobject_cast<Group *>(this);
127
	if (me) { //I'm a frame --> flash border
128
129
		QString stylesheet_copy( getEmphasisWidget()->styleSheet() );
		getEmphasisWidget()->setStyleSheet(stylesheet_copy.replace( //flash frame border color
130
131
			colors::getQColor(on? "frameborder" : "sl_yellow").name(QColor::HexRgb).toLower(),
			colors::getQColor(on? "sl_yellow" : "frameborder").name()));
132
	} else { //panel --> highlight
133
134
		emphasis_widget_->setProperty("highlight", (on? "true" : "false"));
	}
135
136
	this->style()->unpolish(emphasis_widget_);
	this->style()->polish(emphasis_widget_);
137
	//emphasis_widget_->repaint(); //needed for e. g. border styling
138
139
140

	if (reset_interval != -1)
		style_timer_.start();
141
142
}

143
/**
Michael Reisecker's avatar
Michael Reisecker committed
144
145
146
147
 * @brief Set a panel's styling widget.
 * @details The widget pointed to this way is used for highlighting purposes
 * (such as missing values or faulty expressions).
 * @param[in] emphasis_widet The panel's widget to style (often the key label).
Michael Reisecker's avatar
Michael Reisecker committed
148
 * @param[in] set_object_name Whether to set the object's name here or later.
149
 */
150
void Atomic::setEmphasisWidget(QWidget *emphasis_widget, const bool &set_object_name)
151
{
152
153
	emphasis_widget_ = emphasis_widget;
	emphasis_widget->setObjectName("_primary_" + getQtKey(getId()));
154
	if (set_object_name) //template panels may want to do this themselves (handling substitutions)
155
		this->setObjectName(getQtKey(getId()));
156
	QObject *const property_watcher( new PropertyWatcher(emphasis_widget) );
157
	this->connect(property_watcher, SIGNAL(changedValue()), SLOT(onPropertySet())); //old style for easy delegation
158
	this->installEventFilter(property_watcher);
159
160
	this->setContextMenuPolicy(Qt::CustomContextMenu);
	connect(this, &QWidget::customContextMenuRequested, this, &Atomic::onConextMenuRequest);
161
162
163
}

/**
Michael Reisecker's avatar
Michael Reisecker committed
164
165
 * @brief Set a property indicating that the value this panel controls is defaulted or mandatory,
 * or to be highlighted in a different way.
166
167
168
 * @param[in] set Set style on/off.
 * @param[in] widget If given, set the style for this widget instead of the primary one
 * (used in Choice panel for example).
169
 */
170
void Atomic::setPanelStyle(const PanelStyle &style, const bool &set, QWidget *widget)
171
{
172
	QWidget *widget_to_set = (widget == nullptr)? emphasis_widget_ : widget;
173
	if (widget_to_set == nullptr) //e. g. horizontal panel
174
		return;
175

176
177
178
179
	QString style_string;
	switch (style) {
	case MANDATORY:
		style_string = "mandatory";
180
		{ //if the styling widget is a label we also append an asterisk to the label of missing values:
181
			QLabel *key_label( qobject_cast<QLabel *>(getEmphasisWidget()) );
182
183
			if (key_label) {
				if (set) {
184
185
						if (!key_label->text().startsWith(Cst::u_star + " "))
							key_label->setText(Cst::u_star + " " + key_label->text());
186
				} else {
187
					if (key_label->text().startsWith(Cst::u_star + " ")) {
188
						QString pure_label( key_label->text() ); //non-const copy
189
						key_label->setText(pure_label.mid(2));
190
191
192
193
					}
				} //endif set
			} //endif key_label
		}
194
195
196
		break;
	case DEFAULT:
		style_string = "shows_default";
197
		break;
Michael Reisecker's avatar
Michael Reisecker committed
198
199
	case INVALID:
		style_string = "invalid";
200
	}
201
202
203
	widget_to_set->setProperty(style_string.toLocal8Bit(), set? "true" : "false");
	this->style()->unpolish(widget_to_set); //if a property is set dynamically, we might have to refresh
	this->style()->polish(widget_to_set);
204
205
} //https://wiki.qt.io/Technical_FAQ#How_can_my_stylesheet_account_for_custom_properties.3F

206
/**
Michael Reisecker's avatar
Michael Reisecker committed
207
208
 * @brief Style a panel to contain invalid values.
 * @param[in] invalid True to set "invalid", false to disable (= "valid").
209
 */
Michael Reisecker's avatar
Michael Reisecker committed
210
void Atomic::setInvalidStyle(const bool &invalid)
211
{
Michael Reisecker's avatar
Michael Reisecker committed
212
	setPanelStyle(INVALID, invalid);
213
214
}

215
216
217
218
219
220
221
222
223
/**
 * @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.
 */
Michael Reisecker's avatar
Michael Reisecker committed
224
void Atomic::substituteKeys(QDomElement &parent_element, const QString &replace, const QString &replace_with)
225
{ //TODO: clean up the substitutions with proper logic from bottom up
226
227
	for (QDomElement element = parent_element.firstChildElement(); !element.isNull(); element = element.nextSiblingElement()) {
		QString key(element.attribute("key"));
228
229
		//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));
230
		QString text(element.attribute("caption"));
231
		element.setAttribute("caption", text.replace(key.indexOf(replace), 1, replace_with)); //for labels
232
		QString label(element.attribute("label"));
233
234
		element.setAttribute("label", label.replace(label.indexOf(replace), 1, replace_with));
		substituteKeys(element, replace, replace_with);
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
	}
}

/**
 * @brief Build a standard spacer item for widget positioning.
 * @details This is the only little workaround we use for our layouts. The scroll areas are allowed
 * to resize everything, and sometimes they do so aggressively; for example, they ignore left-aligned
 * elements and move them to the right if the screen gets very big. If on the other hand we try to
 * adjust the sizes of all child widgets manually via .adjustSize() update(), etc., then we run into
 * troubles that require far uglier hacks if they can be solved at all. It has proven best to try not
 * to meddle with the layout manager.
 * The solution here is to add a huge spacer to a list of left-aligned widgets if we want to keep all of
 * them a fixed (small) size.
 * @return Spacer element that can be added to a layout.
 */
Michael Reisecker's avatar
Michael Reisecker committed
250
QSpacerItem * Atomic::buildSpacer()
251
{
252
	//If the spacer is smaller than the total width (including child panels of Alternative etc.,
253
254
	//then AlignLeft does not help anymore and everything starts wandering to the right.
	return new QSpacerItem(getMainWindow()->width() * 5, 0, QSizePolicy::Maximum); //huge spacer
255
256
257

	//TODO: This does leave some problems, namely when certain panels, e. g. a Selector, are nested.
	//The plus/minus buttons can start to wander, since there we disable the spacers.
258
	//For the usual XMLs it should not matter, but in the future it should be tested off-site how
259
	//to keep widgets to the left in the best manner.
260
261
262
263
264
}

/**
 * @brief Set margins of a layout.
 * @details This controls how much space there is between widgets, i. e. the spacing in the main GUI.
Michael Reisecker's avatar
Michael Reisecker committed
265
 * @param[in] layout Any layout.
266
 */
Michael Reisecker's avatar
Michael Reisecker committed
267
void Atomic::setLayoutMargins(QLayout *layout)
268
269
270
271
272
273
274
{
	//                  left, top, right, bottom
	layout->setContentsMargins(2, 1, 2, 1); //with our nested groups we want to keep them tight
}

/**
 * @brief Convenience call to add a Helptext object to the end of a vertical layout.
Michael Reisecker's avatar
Michael Reisecker committed
275
 * @details <help> elements are displayed in a Helptext, "help" attributes in the tooltip.
276
277
 * @param[in] layout Layout to add the Helptext to.
 * @param[in] options Parent XML node controlling the appearance of the help text.
278
 * @param[in] force Add a Helptext even if it's empty (used by panels that can change the text).
279
 */
280
Helptext * Atomic::addHelp(QHBoxLayout *layout, const QDomNode &options, const bool& tight, const bool &force)
281
{ //changes here might have to be mirrored in other places, e. g. Choice::setOptions()
Mathias Bavay's avatar
Mathias Bavay committed
282
	QDomElement help_element( options.firstChildElement("help") ); //dedicated <help> tag if there is one
283
284
	if (help_element.isNull())
		help_element = options.firstChildElement("h"); //shortcut notation
285
	const bool single_line = (help_element.attribute("wrap") == "false"); //single line extending the scroll bar
Mathias Bavay's avatar
Mathias Bavay committed
286
	const QString helptext( help_element.text() );
287
	if (emphasis_widget_ != nullptr) {
Mathias Bavay's avatar
Mathias Bavay committed
288
		const QString inline_help( options.toElement().attribute("help") ); //help in attribute as opposed to element
289
		emphasis_widget_->setToolTip(inline_help.isEmpty()? key_ : inline_help);
290
	}
291
	if (force || !helptext.isEmpty()) {
Mathias Bavay's avatar
Mathias Bavay committed
292
		auto *help( new Helptext(helptext, tight, single_line) );
293
294
		static constexpr int gap_width = 10; //little bit of space after the panels
		layout->addSpacerItem(new QSpacerItem(gap_width, 0, QSizePolicy::Fixed, QSizePolicy::Fixed));
295
296
297
298
		layout->addWidget(help, 0, Qt::AlignRight);
		return help;
	}
	return nullptr;
Michael Reisecker's avatar
Michael Reisecker committed
299
300
}

301
302
303
304
305
306
/**
 * @brief Call setUpdatesEnabled() with a tiny delay
 * @details There is GUI flickering when hiding widgets, even for simple ones. It is occasional, but
 * sometimes quite annoying. Disabling and re-enabling the painting is not enough, but firing a timer
 * to do so helps quite a bit. This is used by panels that can show/hide child panels.
 */
307
void Atomic::setBufferedUpdatesEnabled(const int &time)
308
{
309
310
	//TODO: there is still some flickering, but the mechanisms against it don't work,
	//and it's also a little strange in which direction the widgets jump.
311
	//I suspect that the QScrollAreas resizing the widgets might be the culprit.
312
	QTimer::singleShot(time, this, &Atomic::onTimerBufferedUpdatesEnabled);
313
314
}

315
316
317
318
319
/**
 * @brief Find the widest text in a list.
 * @details This function calculates the on-screen width of a list of texts and returns the
 * greatest one, optionally capping at a fixed value.
 * @param[in] text_list List of texts to check.
320
 * @param[in] element_min_width Minimum size the panel should have.
321
322
323
 * @param[in] element_max_width Maximum allowed width to return.
 * @return The maximum text width, or the hard set limit.
 */
324
int Atomic::getElementTextWidth(const QStringList &text_list, const int &element_min_width, const int &element_max_width)
325
{
Mathias Bavay's avatar
Mathias Bavay committed
326
	const QFontMetrics font_metrics( this->font() );
327
	int width = 0;
328
329
	for (auto &text : text_list) {
		const int text_width = font_metrics.boundingRect(text).width();
330
331
		if (text_width > width)
			width = text_width;
332
	}
333
334
335
336
337
	if (width > element_max_width)
		width = element_max_width;
	if (width < element_min_width)
		width = element_min_width;
	return width;
338
339
}

340
341
342
343
344
345
346
/**
 * @brief Set font stylesheet for an object.
 * @param[in] widget The widget to style the font of.
 * @param[in] options XML user options to parse.
 */
void Atomic::setFontOptions(QWidget *widget, const QDomNode &options)
{
Mathias Bavay's avatar
Mathias Bavay committed
347
348
	const QDomElement op( options.toElement() );
	QString stylesheet( widget->metaObject()->className() );
349
	stylesheet += " {";
350
	if (op.attribute("caption_bold").toLower() == "true")
351
		stylesheet += "font-weight: bold; ";
352
	if (op.attribute("caption_italic").toLower() == "true")
353
354
355
		stylesheet += "font-style: italic; ";
	if (!op.attribute("caption_font").isNull())
		stylesheet += "font-family: \"" + op.attribute("caption_font") + "\"; ";
356
	if (op.attribute("caption_underline").toLower() == "true")
357
358
359
360
361
362
363
364
365
		stylesheet += "text-decoration: underline; ";
	if (!op.attribute("caption_size").isNull())
		stylesheet += "font-size: " + op.attribute("caption_size") + "pt; ";
	if (!op.attribute("caption_color").isNull())
		stylesheet += "color: " + colors::getQColor(op.attribute("caption_color")).name() + "; ";
	stylesheet += "}";
	widget->setStyleSheet(stylesheet);
}

366
367
/**
 * @brief Add attributes to a given font.
368
369
 * @details This function can be used for setting the font of objects that are not Q_OBJECTs, such
 * as QListWidgetItems, as well as for objects where the stylesheet should not be disturbed.
370
371
372
373
 * @param[in] item_font The original font.
 * @param[in] options XML user options to parse.
 * @return The modified font; object.setFont() needs to be set to this from outside.
 */
374
375
376
QFont Atomic::setFontOptions(const QFont &item_font, const QDomElement &options)
{
	QFont retFont(item_font);
377
378
379
	retFont.setBold(options.attribute("bold").toLower() == "true");
	retFont.setItalic(options.attribute("italic").toLower() == "true");
	retFont.setUnderline(options.attribute("underline").toLower() == "true");
380
381
	if (!options.attribute("font").isEmpty())
		retFont.setFamily(options.attribute("font"));
Michael Reisecker's avatar
Michael Reisecker committed
382
383
	if (!options.attribute("font_size").isEmpty())
		retFont.setPointSize(options.attribute("font_size").toInt());
384
385
386
	return retFont;
}

Michael Reisecker's avatar
Michael Reisecker committed
387
388
389
390
391
392
393
394
395
396
/**
 * @brief Event handler for property changes.
 * @details Suitable panels override this to react to changes of the INI value from outside.
 */
void Atomic::onPropertySet()
{
	//Ignore this signal for unsuitable panels. Suitable ones have their own implementation.
}

/**
397
 * @brief Convert a number to string and pass to INI value setter.
Michael Reisecker's avatar
Michael Reisecker committed
398
399
400
401
402
403
404
405
 * @param[in] value The integer to convert.
 */
void Atomic::setIniValue(const int &value)
{
	setIniValue(QString::number(value));
}

/**
406
 * @brief Convert a number to string and pass to INI value setter.
Michael Reisecker's avatar
Michael Reisecker committed
407
408
409
410
411
 * @param[in] value The double value to convert.
 */
void Atomic::setIniValue(const double &value)
{
	setIniValue(QString::number(value));
412
413
}

Michael Reisecker's avatar
Michael Reisecker committed
414
/**
415
 * @brief Store this panel's current value in a uniform way.
Michael Reisecker's avatar
Michael Reisecker committed
416
 * @details Gets called after range checks have been performed, i. e. when we
417
418
 * want to propagate the change to the INI. It will be read by the main program when the
 * INI is being written out. It is called by changes to:
Michael Reisecker's avatar
Michael Reisecker committed
419
420
421
 *  - Values through users entering in the GUI
 *  - Default values in the XML
 *  - Values from an INI file
422
 * @param[in] value Set the current value of the panel.
Michael Reisecker's avatar
Michael Reisecker committed
423
424
425
 */
void Atomic::setIniValue(const QString &value)
{
426
	ini_value_ = os::cleanKDETabStr(value);
Michael Reisecker's avatar
Michael Reisecker committed
427
}
428

429
430
431
/**
 * @brief Prepare the panel's custom context menu.
 */
432
433
void Atomic::createContextMenu()
{
Mathias Bavay's avatar
Mathias Bavay committed
434
	QAction *info_entry( panel_context_menu_.addAction(key_) );
435
436
437
438
439
440
	info_entry->setEnabled(false);
	panel_context_menu_.addSeparator();
	panel_context_menu_.addAction(tr("Reset to default"));
	panel_context_menu_.addAction(tr("Delete key"));
}

441
442
443
444
445
446
447
448
/**
 * @brief Re-enable GUI painting.
 * @details Cf. notes in setBufferedUpdatesEnabled().
 */
void Atomic::onTimerBufferedUpdatesEnabled()
{
	setUpdatesEnabled(true);
}
449
450
451
452
453
454

/**
 * @brief Show context menu to clear this panel.
 */
void Atomic::onConextMenuRequest(const QPoint &/*pos*/)
{
455
456
457
	if (key_.isEmpty())
		return;
	if (qobject_cast<Group *>(this)) //e. g. Selector/Replicator containers
458
		return;
Mathias Bavay's avatar
Mathias Bavay committed
459
	QAction *selected( panel_context_menu_.exec(QCursor::pos()) );
460
461
462
463
464
465
466
	if (selected) {
		if (selected->text() == tr("Reset to default"))
			clear();
		else if (selected->text() == tr("Delete key"))
			clear(false);
	}
}