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
20

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

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

28
#include <utility>
29

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

34
35
36
37
38
39
40
41
42
43
44
45
/**
 * @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
46
	ini_ = getMainWindow()->getIni();
47
	createContextMenu();
48
49
50
51

	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
52
53
54
55
56
57
58
59
60
61
}

/**
 * @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)
{
	setPanelStyle(FAULTY, false); //first we disable temporary styles
	setPanelStyle(VALID, false);
62
63
	const bool is_default = (QString::compare(in_value, this->property("default_value").toString(),
	    Qt::CaseInsensitive) == 0);
64
	setPanelStyle(DEFAULT, is_default && !this->property("default_value").isNull() && !in_value.isNull());
65
66
67
68
	if (this->property("is_mandatory").toBool()) {
		const bool missing = in_value.isEmpty();
		setPanelStyle(MANDATORY, missing);
	}
Michael Reisecker's avatar
Michael Reisecker committed
69
70
71
72
73
}

/**
 * @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
74
75
76
 * 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
77
78
79
80
 * @param[in] ini_key Key as given in the XML
 * @return A key without special chars.
 */
QString Atomic::getQtKey(const QString &ini_key)
81
82
83
{ //sounds heavy but is fast
	return QString(QCryptographicHash::hash((ini_key.toLower().toLocal8Bit()),
	    QCryptographicHash::Md5).toHex());
84
85
}

86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
 * @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_;
}

100
101
/**
 * @brief Reset panel to the default value.
102
 * @param[in] set_default If true, reset the value to default. If false, delete the key.
103
 */
104
void Atomic::clear(const bool &set_default)
105
106
107
108
109
110
{
	/*
	 * 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).
111
112
	 * TODO: This means unneccessary calls that may not be trivial (depending on the panel),
	 * so a redesign could be desired.
113
114
	 */
	this->setProperty("ini_value", ini_value_);
115
	this->setProperty("ini_value", set_default? this->property("default_value") : QString());
116
117
}

118
119
120
121
/**
 * @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.
122
123
 * @param[in] on True to highlight, false for normal.
 * @param[in] reset_interval Time after which to reset to normal (ms).
124
 */
125
void Atomic::setHighlightedStyle(const bool &on, const int &reset_interval)
126
{
127
128
129
130
131
132
133
134
135
	Group *me = qobject_cast<Group *>(this);
	if (me) {
		QString stylesheet_copy( getEmphasisWidget()->styleSheet() );
		getEmphasisWidget()->setStyleSheet(stylesheet_copy.replace( //flash frame border color
			colors::getQColor(on? "frameborder" : "important").name(QColor::HexRgb).toLower(),
			colors::getQColor(on? "important" : "frameborder").name()));
	} else {
		emphasis_widget_->setProperty("highlight", (on? "true" : "false"));
	}
136
137
	this->style()->unpolish(emphasis_widget_);
	this->style()->polish(emphasis_widget_);
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
184
185
186
187
188
189
190
191
192
193
194
			if (key_label) {
				if (set) {
						if (!key_label->text().endsWith(" *"))
							key_label->setText(key_label->text() + " *");
				} else {
					if (key_label->text().endsWith(" *")) {
						QString pure_label( key_label->text() ); //non-const copy
						pure_label.chop(2);
						key_label->setText(pure_label);
					}
				} //endif set
			} //endif key_label
		}
195
196
197
		break;
	case DEFAULT:
		style_string = "shows_default";
198
199
200
		break;
	case FAULTY:
		style_string = "faulty";
201
202
203
		break;
	case VALID:
		style_string = "valid";
204
	}
205
206
207
	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);
208
209
} //https://wiki.qt.io/Technical_FAQ#How_can_my_stylesheet_account_for_custom_properties.3F

210
/**
211
 * @brief Switch between "faulty" and "valid" panel styles.
212
213
 * @param[in] on True to set "faulty", false to set "valid".
 */
214
void Atomic::setValidPanelStyle(const bool &on)
215
{
216
217
	setPanelStyle(VALID, on);
	setPanelStyle(FAULTY, !on);
218
219
}

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

/**
 * @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
255
QSpacerItem * Atomic::buildSpacer()
256
{
257
	//If the spacer is smaller than the total width (including child panels of Alternative etc.,
258
259
	//then AlignLeft does not help anymore and everything starts wandering to the right.
	return new QSpacerItem(getMainWindow()->width() * 5, 0, QSizePolicy::Maximum); //huge spacer
260
261
262

	//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.
263
	//For the usual XMLs it should not matter, but in the future it should be tested off-site how
264
	//to keep widgets to the left in the best manner.
265
266
267
268
269
}

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

306
307
308
309
310
311
/**
 * @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.
 */
312
void Atomic::setBufferedUpdatesEnabled(const int &time)
313
{
314
315
	//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.
316
	//I suspect that the QScrollAreas resizing the widgets might be the culprit.
317
	QTimer::singleShot(time, this, &Atomic::onTimerBufferedUpdatesEnabled);
318
319
}

320
321
322
323
324
/**
 * @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.
325
 * @param[in] element_min_width Minimum size the panel should have.
326
327
328
 * @param[in] element_max_width Maximum allowed width to return.
 * @return The maximum text width, or the hard set limit.
 */
329
int Atomic::getElementTextWidth(const QStringList &text_list, const int &element_min_width, const int &element_max_width)
330
{
Mathias Bavay's avatar
Mathias Bavay committed
331
	const QFontMetrics font_metrics( this->font() );
332
	int width = 0;
333
334
	for (auto &text : text_list) {
		const int text_width = font_metrics.boundingRect(text).width();
335
336
		if (text_width > width)
			width = text_width;
337
	}
338
339
340
341
342
	if (width > element_max_width)
		width = element_max_width;
	if (width < element_min_width)
		width = element_min_width;
	return width;
343
344
}

345
346
347
348
349
350
351
/**
 * @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
352
353
	const QDomElement op( options.toElement() );
	QString stylesheet( widget->metaObject()->className() );
354
	stylesheet += " {";
355
	if (op.attribute("caption_bold").toLower() == "true")
356
		stylesheet += "font-weight: bold; ";
357
	if (op.attribute("caption_italic").toLower() == "true")
358
359
360
		stylesheet += "font-style: italic; ";
	if (!op.attribute("caption_font").isNull())
		stylesheet += "font-family: \"" + op.attribute("caption_font") + "\"; ";
361
	if (op.attribute("caption_underline").toLower() == "true")
362
363
364
365
366
367
368
369
370
		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);
}

371
372
/**
 * @brief Add attributes to a given font.
373
374
 * @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.
375
376
377
378
 * @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.
 */
379
380
381
QFont Atomic::setFontOptions(const QFont &item_font, const QDomElement &options)
{
	QFont retFont(item_font);
382
383
384
	retFont.setBold(options.attribute("bold").toLower() == "true");
	retFont.setItalic(options.attribute("italic").toLower() == "true");
	retFont.setUnderline(options.attribute("underline").toLower() == "true");
385
386
	if (!options.attribute("font").isEmpty())
		retFont.setFamily(options.attribute("font"));
Michael Reisecker's avatar
Michael Reisecker committed
387
388
	if (!options.attribute("font_size").isEmpty())
		retFont.setPointSize(options.attribute("font_size").toInt());
389
390
391
	return retFont;
}

Michael Reisecker's avatar
Michael Reisecker committed
392
393
394
395
396
397
398
399
400
401
/**
 * @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.
}

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

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

Michael Reisecker's avatar
Michael Reisecker committed
419
/**
420
 * @brief Store this panel's current value in a uniform way.
Michael Reisecker's avatar
Michael Reisecker committed
421
 * @details Gets called after range checks have been performed, i. e. when we
422
423
 * 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
424
425
426
 *  - Values through users entering in the GUI
 *  - Default values in the XML
 *  - Values from an INI file
427
 * @param[in] value Set the current value of the panel.
Michael Reisecker's avatar
Michael Reisecker committed
428
429
430
 */
void Atomic::setIniValue(const QString &value)
{
431
	ini_value_ = os::cleanKDETabStr(value);
Michael Reisecker's avatar
Michael Reisecker committed
432
}
433

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

446
447
448
449
450
451
452
453
/**
 * @brief Re-enable GUI painting.
 * @details Cf. notes in setBufferedUpdatesEnabled().
 */
void Atomic::onTimerBufferedUpdatesEnabled()
{
	setUpdatesEnabled(true);
}
454
455
456
457
458
459

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