1 /***************************************************************************
2                           rkoptionset  -  description
3                              -------------------
4     begin                : Mon Oct 31 2011
5     copyright            : (C) 2011, 2012, 2014 by Thomas Friedrichsmeier
6     email                : thomas.friedrichsmeier@kdemail.net
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "rkoptionset.h"
19 
20 #include <QVBoxLayout>
21 #include <QTreeWidget>
22 #include <QHeaderView>
23 #include <QPushButton>
24 #include <QStackedWidget>
25 #include <QLabel>
26 #include <QMimeData>
27 
28 #include <KLocalizedString>
29 
30 #include "rkstandardcomponent.h"
31 #include "../misc/rkcommonfunctions.h"
32 #include "../misc/rkaccordiontable.h"
33 #include "../misc/rkstandardicons.h"
34 #include "../misc/xmlhelper.h"
35 
36 #include "../debug.h"
37 
38 #define KEYCOLUMN_UNINITIALIZED_VALUE QString ("___#!RK!___Keycol_uninitialized")
39 
RKOptionSet(const QDomElement & element,RKComponent * parent_component,QWidget * parent_widget)40 RKOptionSet::RKOptionSet (const QDomElement &element, RKComponent *parent_component, QWidget *parent_widget) : RKComponent (parent_component, parent_widget) {
41 	RK_TRACE (PLUGIN);
42 
43 	XMLHelper *xml = parent_component->xmlHelper ();
44 	updating = false;
45 	last_known_status = Processing;
46 	n_invalid_rows = n_unfinished_rows = 0;
47 
48 	min_rows = xml->getIntAttribute (element, "min_rows", 0, DL_INFO);
49 	min_rows_if_any = xml->getIntAttribute (element, "min_rows_if_any", 1, DL_INFO);
50 	max_rows = xml->getIntAttribute (element, "max_rows", INT_MAX, DL_INFO);
51 
52 	// build UI framework
53 	QVBoxLayout *layout = new QVBoxLayout (this);
54 	layout->setContentsMargins (0, 0, 0, 0);
55 	switcher = new QStackedWidget (this);
56 	layout->addWidget (switcher);
57 	accordion = new RKAccordionTable (this);
58 	switcher->addWidget (accordion);
59 
60 	connect (accordion, static_cast<void (RKAccordionTable::*)(int)>(&RKAccordionTable::activated), this, &RKOptionSet::currentRowChanged);
61 	connect (accordion, &RKAccordionTable::addRow, this, &RKOptionSet::addRow);
62 	connect (accordion, &RKAccordionTable::removeRow, this, &RKOptionSet::removeRow);
63 
64 	updating_notice = new QLabel (i18n ("Updating status, please wait"), this);
65 	switcher->addWidget (updating_notice);
66 	update_timer.setInterval (0);
67 	update_timer.setSingleShot (true);
68 	connect (&update_timer, &QTimer::timeout, this, &RKOptionSet::slotUpdateUnfinishedRows);
69 
70 	// create some meta properties
71 	serialization_of_set = new RKComponentPropertyBase (this, false);
72 	addChild ("serialized", serialization_of_set);
73 	connect (serialization_of_set, &RKComponentPropertyBase::valueChanged, this, &RKOptionSet::serializationPropertyChanged);
74 
75 	row_count = new RKComponentPropertyInt (this, false, 0);
76 	row_count->setInternal (true);
77 	addChild ("row_count", row_count);		// NOTE: read-only
78 	return_to_row = active_row = -1;
79 	current_row = new RKComponentPropertyInt (this, false, active_row);
80 	current_row->setInternal (true);
81 	addChild ("current_row", current_row);		// NOTE: read-write
82 	connect (current_row, &RKComponentPropertyBase::valueChanged, this, &RKOptionSet::currentRowPropertyChanged);
83 
84 	// first build the contents, as we will need to refer to the elements inside, later
85 	model = new RKOptionSetDisplayModel (this);
86 	contents_container = new RKComponent (this, accordion->editorWidget ());
87 	accordion->editorWidget ()->layout ()->addWidget (contents_container);
88 	QDomElement content_element = xml->getChildElement (element, "content", DL_ERROR);
89 	RKComponentBuilder *builder = new RKComponentBuilder (contents_container, content_element);
90 	builder->buildElement (content_element, *xml, accordion->editorWidget (), false);	// NOTE that parent widget != parent component, here, by intention. The point is that the display should not be disabled along with the contents
91 	builder->parseLogic (xml->getChildElement (element, "logic", DL_INFO), *xml, false);
92 	builder->makeConnections ();
93 	addChild ("contents", contents_container);
94 	connect (standardComponent (), &RKStandardComponent::standardInitializationComplete, this, &RKOptionSet::fetchDefaults);
95 
96 	// create columns
97 	XMLChildList options = xml->getChildElements (element, "optioncolumn", DL_WARNING);
98 
99 	QStringList visible_column_labels;
100 	for (int i = 0; i < options.size (); ++i) {
101 		const QDomElement &e = options.at (i);
102 		QString id = xml->getStringAttribute (e, "id", QString (), DL_ERROR);
103 		QString label = xml->i18nStringAttribute (e, "label", QString (), DL_DEBUG);
104 		QString governor = xml->getStringAttribute (e, "connect", QString (), DL_INFO);
105 		bool external = xml->getBoolAttribute (e, "external", false, DL_INFO);
106 
107 		while (child_map.contains (id)) {
108 			RK_DEBUG (PLUGIN, DL_ERROR, "optionset already contains a property named %s. Renaming to _%s", qPrintable (id), qPrintable (id));
109 			id = '_' + id;
110 		}
111 
112 		ColumnInfo col_inf;
113 		col_inf.column_name = id;
114 		col_inf.external = external;
115 		col_inf.governor = governor;
116 		if (external && e.hasAttribute ("default")) col_inf.default_value = xml->getStringAttribute (e, "default", QString (), DL_ERROR);
117 
118 		RKComponentPropertyStringList *column_property = new RKComponentPropertyStringList (this, false);
119 		column_property->setInternal (external);	// Yes, looks strange, indeed. External properties should simply not be serialized / restored...
120 		addChild (id, column_property);
121 		connect (column_property, &RKComponentPropertyBase::valueChanged, this, &RKOptionSet::columnPropertyChanged);
122 
123 		if (!label.isEmpty ()) {
124 			col_inf.display_index = visible_column_labels.size ();
125 			col_inf.column_label = label;
126 			visible_column_labels.append (label);
127 			visible_columns.append (column_property);
128 		} else {
129 			col_inf.display_index = -1;
130 		}
131 
132 		column_map.insert (column_property, col_inf);
133 	}
134 
135 	keycolumn = 0;
136 	QString keycol = xml->getStringAttribute (element, "keycolumn", QString (), DL_DEBUG);
137 	if (!keycol.isEmpty ()) {
138 		keycolumn = static_cast<RKComponentPropertyStringList*> (child_map.value (keycol));
139 		if (!column_map.contains (keycolumn)) {
140 			RK_DEBUG (PLUGIN, DL_ERROR, "optionset does not contain an optioncolumn named %s. Falling back to manual insertion mode", qPrintable (keycol));
141 			keycolumn = 0;
142 		} else if (!column_map[keycolumn].external) {
143 			RK_DEBUG (PLUGIN, DL_ERROR, "keycolumn (%s) is not marked as external. Falling back to manual insertion mode", qPrintable (keycol));
144 			keycolumn = 0;
145 		} else {
146 			updating = true;
147 			keycolumn->setValue (KEYCOLUMN_UNINITIALIZED_VALUE);
148 			updating = false;
149 		}
150 	}
151 
152 	QMap<RKComponentPropertyStringList *, ColumnInfo>::iterator it = column_map.begin ();
153 	for (; it != column_map.end (); ++it) {
154 		ColumnInfo &ci = it.value ();
155 		if (!ci.governor.isEmpty ()) {		// there *can* be columns without governor for driven or connected option sets
156 			// Establish connections between columns and their respective governors. Since the format differs, the connection is done indirectly, through this component.
157 			// So, here, we set up a map of properties to columns, and connect to the change signals.
158 			RKComponentBase *governor = contents_container->lookupComponent (ci.governor, &ci.governor_modifier);
159 			if (governor && governor->isProperty ()) {
160 				RKComponentPropertyBase *gov_prop = static_cast<RKComponentPropertyBase*> (governor);
161 				if (ci.external) {
162 					if (!ci.governor_modifier.isEmpty ()) {
163 						RK_DEBUG (PLUGIN, DL_ERROR, "Cannot connect external column '%s' in optionset to property with modifier (%s).", qPrintable (ci.column_name), qPrintable (ci.governor));
164 						continue;
165 					}
166 				}
167 				columns_to_update.insertMulti (gov_prop, it.key ());
168 				connect (gov_prop, &RKComponentPropertyBase::valueChanged, this, &RKOptionSet::governingPropertyChanged);
169 			} else {
170 				RK_DEBUG (PLUGIN, DL_ERROR, "did not find governing property %s for column %s of optionset", qPrintable (ci.governor), qPrintable (ci.column_name));
171 			}
172 		}
173 	}
174 
175 	model->column_labels = visible_column_labels;
176 	accordion->setShowAddRemoveButtons (!keycolumn);
177 	accordion->setModel (model);
178 }
179 
~RKOptionSet()180 RKOptionSet::~RKOptionSet () {
181 	RK_TRACE (PLUGIN);
182 }
183 
fetchDefaults()184 void RKOptionSet::fetchDefaults () {
185 	RK_TRACE (PLUGIN);
186 	RK_ASSERT (default_row_state.isEmpty ());
187 	contents_container->fetchPropertyValuesRecursive (&default_row_state, false, QString (), true);
188 	if (min_rows && !keycolumn && (rowCount () <= 0)) addRow (rowCount ());
189 	contents_container->enablednessProperty ()->setBoolValue (rowCount () > 0);	// no current row; Do this *after* fetching default values, however. Otherwise most values will *not* be read, as the element is disabled
190 }
191 
serializeList(const QStringList & list)192 QString serializeList (const QStringList &list) {
193 	QString ret;
194 	for (int i = 0; i < list.size (); ++i) {
195 		if (i > 0) ret.append ('\t');
196 		ret.append (RKCommonFunctions::escape (list[i]));
197 	}
198 	return ret;
199 }
200 
unserializeList(const QString & serial)201 QStringList unserializeList  (const QString &serial) {
202 	QStringList ret = serial.split ('\t', QString::KeepEmptyParts);
203 	for (int i = 0; i < ret.size (); ++i) {
204 		ret[i] = RKCommonFunctions::unescape (ret[i]);
205 	}
206 	return ret;
207 }
208 
serializeMap(const RKComponent::PropertyValueMap & map)209 QString serializeMap (const RKComponent::PropertyValueMap &map) {
210 	QString ret;
211 
212 	RKComponent::PropertyValueMap::const_iterator it;
213 	for (it = map.constBegin (); it != map.constEnd (); ++it) {
214 		if (!ret.isEmpty ()) ret.append ('\t');
215 		ret.append (RKCommonFunctions::escape (it.key () + '=' + it.value ()));
216 	}
217 	return ret;
218 }
219 
unserializeMap(const QString & serial)220 RKComponent::PropertyValueMap unserializeMap (const QString &serial) {
221 	RKComponent::PropertyValueMap ret;
222 	QStringList l = serial.split ('\t', QString::KeepEmptyParts);
223 	for (int i = 0; i < l.size (); ++i) {
224 		QString &line = l[i];
225 		int sep = line.indexOf ('=');
226 		ret.insert (RKCommonFunctions::unescape (line.left (sep)), RKCommonFunctions::unescape (line.mid (sep+1)));
227 	}
228 	return ret;
229 }
230 
fetchPropertyValuesRecursive(PropertyValueMap * list,bool include_top_level,const QString & prefix,bool include_inactive_elements) const231 void RKOptionSet::fetchPropertyValuesRecursive (PropertyValueMap *list, bool include_top_level, const QString &prefix, bool include_inactive_elements) const {
232 	RK_TRACE (PLUGIN);
233 	RK_ASSERT (include_top_level);
234 	RK_ASSERT (!include_inactive_elements);
235 
236 	QString serialization;
237 
238 	if (keycolumn) {
239 		serialization.append ("keys=" + serializeList (old_keys));
240 	}
241 
242 	for (int r = 0; r < rows.size (); ++r) {
243 		if (!serialization.isEmpty ()) serialization.append ("\n");
244 		if (r == active_row) {
245 			PropertyValueMap map;	// current row may have changes which have not yet been stored to the state map
246 			contents_container->fetchPropertyValuesRecursive (&map);
247 			serialization.append ("_row=" + serializeMap (map));
248 		} else {
249 			serialization.append ("_row=" + serializeMap (rows[r].full_row_map));
250 		}
251 	}
252 
253 	list->insert (prefix + "serialized", serialization);
254 }
255 
serializationPropertyChanged(RKComponentPropertyBase * property)256 void RKOptionSet::serializationPropertyChanged (RKComponentPropertyBase* property) {
257 	if (updating) return;
258 	updating = true;
259 	if (model) model->layoutAboutToBeChanged ();
260 
261 	RK_TRACE (PLUGIN);
262 	RK_ASSERT (property == serialization_of_set);
263 /* What happens when deserializing a plugin, with a driven optionset, and
264  * the property connected to the keycolumn is restored *before* the optionset itself has been de-serialized?
265  *
266  * We have to special-case this: If we go into setPropertyValues, and
267  * the key column has already been touched, we have to
268  * - apply property values from the serialization
269  * - trigger handleKeycolumnUpdate(), delayed
270  *
271  */
272 	bool update_key_col = false;
273 	if (keycolumn && (keycolumn->value () != KEYCOLUMN_UNINITIALIZED_VALUE)) {
274 		update_key_col = true;
275 	} else {
276 		RK_ASSERT (rows.isEmpty ());
277 	}
278 
279 	QList<RowInfo> new_rows;
280 	int row = 0;
281 	QStringList items = fetchStringValue (property).split ('\n');
282 	bool keys_missing = (keycolumn != 0);
283 	for (int i = 0; i < items.size (); ++i) {
284 		const QString &item = items[i];
285 		int sep = item.indexOf ('=');
286 		if (sep < 0) {
287 			RK_DEBUG (PLUGIN, DL_WARNING, "Bad format while trying to de-serialize optionset, line %d", i);
288 			continue;
289 		}
290 		QString tag = item.left (sep);
291 		QString value = item.mid (sep + 1);
292 
293 		if (tag == QLatin1String ("keys")) {
294 			if (!keys_missing) {
295 				RK_DEBUG (PLUGIN, DL_WARNING, "Unexpected specification of keys while trying to de-serialize optionset, line %d", i);
296 				continue;
297 			}
298 			old_keys = unserializeList (value);
299 			keys_missing = false;
300 		} else if (tag == QLatin1String ("_row")) {
301 			new_rows.append (RowInfo (unserializeMap (value)));
302 			++row;
303 		} else {
304 			RK_DEBUG (PLUGIN, DL_WARNING, "Unexpected tag %s while trying to de-serialize optionset, line %d", qPrintable (tag), i);
305 			continue;
306 		}
307 	}
308 
309 	if (keycolumn) {
310 		RK_ASSERT (new_rows.size () == old_keys.size ());
311 		RK_ASSERT (!keys_missing);
312 	}
313 
314 	// reset all non-external columns to default values
315 	QMap<RKComponentPropertyStringList*, ColumnInfo>::const_iterator it;
316 	for (it = column_map.constBegin (); it != column_map.constEnd (); ++it) {
317 		const ColumnInfo &col = it.value ();
318 		if (col.external) continue;
319 		QStringList def;
320 		for (int i = 0; i < row; ++i) {
321 			def.append (getDefaultValue (col, i));
322 		}
323 		it.key ()->setValueList (def);
324 	}
325 
326 	rows = new_rows;
327 	n_unfinished_rows = n_invalid_rows = row;
328 	row_count->setIntValue (row);
329 	serialization_of_set->setValue (QString ());	// free some mem, and don't make users think, this can actually be queried in real-time
330 	updating = false;
331 	if (update_key_col) handleKeycolumnUpdate ();
332 
333 	active_row = -1;
334 	current_row->setIntValue (qMin (0, row - 1));
335 
336 	if (model) model->layoutChanged ();
337 	changed ();
338 }
339 
slotUpdateUnfinishedRows()340 void RKOptionSet::slotUpdateUnfinishedRows () {
341 	updateUnfinishedRows ();
342 }
343 
updateUnfinishedRows()344 void RKOptionSet::updateUnfinishedRows () {
345 	if (updating) return;
346 	if ((active_row >= 0) && (active_row < rows.size ())) {
347 		if (!rows[active_row].finished) {
348 			return;	// if the current row is unfinished: let's wait for this one, first.
349 		}
350 	}
351 
352 	RK_TRACE (PLUGIN);
353 
354 	if (!n_unfinished_rows) {	// done
355 		if (switcher->currentWidget () != updating_notice) return;
356 		current_row->setIntValue (return_to_row);
357 		switcher->setCurrentWidget (accordion);
358 		return;
359 	}
360 
361 	if (switcher->currentWidget () != updating_notice) {
362 		switcher->setCurrentWidget (updating_notice);
363 		return_to_row = active_row;
364 	}
365 	for (int i = 0; i < rows.size (); ++i) {
366 		if (!rows[i].finished) {
367 			current_row->setIntValue (i);
368 			return;
369 		}
370 	}
371 
372 	RK_ASSERT (false);	// This would mean, we did not find any unfinished row, even though we tested for n_unfinished_rows, above.
373 }
374 
addRow(int row)375 void RKOptionSet::addRow (int row) {
376 	RK_TRACE (PLUGIN);
377 
378 	storeRowSerialization (active_row);
379 
380 	int nrows = rowCount ();
381 	if (row < 0) row = nrows;
382 	RK_ASSERT (!keycolumn);
383 
384 	model->beginInsertRows (QModelIndex (), row, row);
385 	// adjust values
386 	updating = true;
387 	QMap<RKComponentPropertyStringList *, ColumnInfo>::iterator it = column_map.begin ();
388 	for (; it != column_map.end (); ++it) {
389 		RKComponentPropertyStringList* col = it.key ();
390 		ColumnInfo &column = it.value ();
391 		QStringList values = col->values ();
392 		values.insert (row, getDefaultValue (column, row));
393 		col->setValueList (values);
394 	}
395 	updating = false;
396 
397 	// adjust status info
398 	RowInfo ri (default_row_state);
399 	ri.valid = false;
400 	ri.finished = false;
401 	rows.insert (row, ri);
402 	++n_unfinished_rows;
403 	++n_invalid_rows;
404 	row_count->setIntValue (nrows + 1);
405 	current_row->setIntValue (active_row = row);
406 	setContentsForRow (active_row);
407 	model->endInsertRows ();
408 
409 	current_row->setIntValue (row);  // Setting this _again_, as the view might have messed with it following endInsertRows ()
410 
411 	changed ();
412 }
413 
removeRow(int row)414 void RKOptionSet::removeRow (int row) {
415 	RK_TRACE (PLUGIN);
416 
417 	int nrows = rowCount ();
418 	if (row < 0) {
419 		RK_ASSERT (false);
420 		return;
421 	}
422 	RK_ASSERT (!keycolumn);
423 
424 	model->beginRemoveRows (QModelIndex (), row, row);
425 	updating = true;
426 	// adjust values
427 	QMap<RKComponentPropertyStringList *, ColumnInfo>::iterator it = column_map.begin ();
428 	for (; it != column_map.end (); ++it) {
429 		RKComponentPropertyStringList* col = it.key ();
430 		QStringList values = col->values ();
431 		values.removeAt (row);
432 		col->setValueList (values);
433 	}
434 	updating = false;
435 
436 	// adjust status info
437 	if (!rows[row].valid) --n_invalid_rows;
438 	if (!rows[row].finished) --n_unfinished_rows;
439 	rows.removeAt (row);
440 
441 	--row;
442 	if ((row < 0) && (nrows > 1)) row = 0;
443 	row_count->setIntValue (nrows - 1);
444 	current_row->setIntValue (active_row = row);
445 	setContentsForRow (row);
446 	model->endRemoveRows ();
447 
448 	current_row->setIntValue (row);  // Setting this _again_, as the view might have messed with it following endRemoveRows ()
449 
450 	changed ();
451 }
452 
moveRow(int old_index,int new_index)453 void RKOptionSet::moveRow (int old_index, int new_index) {
454 	RK_TRACE (PLUGIN);
455 
456 	int nrows = rowCount ();
457 	if (old_index < 0 || old_index >= nrows) {
458 		RK_ASSERT (false);
459 		return;
460 	}
461 
462 	if (new_index < 0 || new_index > nrows) {
463 		new_index = nrows;
464 	}
465 
466 	storeRowSerialization (active_row);
467 	PropertyValueMap backup = rows[old_index].full_row_map;
468 	removeRow (old_index);
469 	if (new_index > old_index) new_index -= 1;
470 	addRow (new_index);
471 	rows[new_index].full_row_map = backup;
472 	setContentsForRow (new_index);
473 
474 	changed ();
475 }
476 
getDefaultValue(const RKOptionSet::ColumnInfo & ci,int row)477 QString getDefaultValue (const RKOptionSet::ColumnInfo& ci, int row) {
478 	// let's not trace this simple helper fun
479 	Q_UNUSED (row);
480 // TODO: should also allow scripted defaults?
481 	return ci.default_value;
482 }
483 
setRowState(int row,bool finished,bool valid)484 void RKOptionSet::setRowState (int row, bool finished, bool valid) {
485 	bool changed = false;
486 	RK_ASSERT (row < rows.size ());
487 	if (rows[row].finished != finished) {
488 		rows[row].finished = finished;
489 		finished ? --n_unfinished_rows : ++n_unfinished_rows;
490 		changed = true;
491 	}
492 	if (rows[row].valid != valid) {
493 		rows[row].valid = valid;
494 		valid ? --n_invalid_rows : ++n_invalid_rows;
495 		changed = true;
496 	}
497 	if (changed && model) model->dataChanged (model->index (row, 0), model->index (row, model->columnCount () - 1));
498 }
499 
changed()500 void RKOptionSet::changed () {
501 	int row = active_row;
502 
503 	if (row >= 0) {
504 		ComponentStatus cs = contents_container->recursiveStatus ();
505 		setRowState (row, cs != Processing, cs == Satisfied);
506 	}
507 
508 	update_timer.start ();
509 
510 	ComponentStatus s = recursiveStatus ();
511 	if (s != last_known_status) {
512 		last_known_status = s;
513 		if (model) model->headerDataChanged (Qt::Horizontal, 0, model->columnCount () - 1);
514 	}
515 
516 	RKComponent::changed ();
517 }
518 
519 // This function is called when a property of the current row of the optionset changes
governingPropertyChanged(RKComponentPropertyBase * property)520 void RKOptionSet::governingPropertyChanged (RKComponentPropertyBase *property) {
521 	RK_TRACE (PLUGIN);
522 
523 	int row = active_row;
524 	if (row < 0) return;
525 	if (updating) return;
526 	updating = true;
527 
528 	QList<RKComponentPropertyStringList *> cols = columns_to_update.values (property);
529 	for (int i = 0; i < cols.size (); ++i) {
530 		RKComponentPropertyStringList *target = cols.at (i);
531 		ColumnInfo &inf = column_map[target];
532 		QString value = fetchStringValue (property, inf.governor_modifier);
533 		target->setValueAt (row, value);
534 
535 		if (model && (inf.display_index >= 0)) {
536 			model->dataChanged (model->index (inf.display_index, row), model->index (inf.display_index, row));
537 		}
538 	}
539 
540 	updating = false;
541 }
542 
543 // This function is called, when a column of the set is changed, typically from external logic
columnPropertyChanged(RKComponentPropertyBase * property)544 void RKOptionSet::columnPropertyChanged (RKComponentPropertyBase *property) {
545 	RK_TRACE (PLUGIN);
546 
547 	if (updating) return;
548 
549 	RKComponentPropertyStringList *target = static_cast<RKComponentPropertyStringList *> (property);
550 	RK_ASSERT (column_map.contains (target));
551 	ColumnInfo& ci = column_map[target];
552 	if (!ci.external) {
553 		RK_DEBUG (PLUGIN, DL_ERROR, "Column %s was touched externally, although it is not marked as external", qPrintable (ci.column_name));
554 		return;
555 	}
556 
557 	if (target == keycolumn) handleKeycolumnUpdate ();
558 	else {
559 		if (model) model->dataChanged (model->index (ci.display_index, 0), model->index (ci.display_index, model->rowCount ()));
560 		applyContentsFromExternalColumn (target, active_row);
561 	}
562 }
563 
handleKeycolumnUpdate()564 void RKOptionSet::handleKeycolumnUpdate () {
565 	RK_TRACE (PLUGIN);
566 
567 	int activate_row = active_row;
568 	QStringList new_keys = keycolumn->values ();
569 	QMap<int, int> position_changes;
570 	QSet<int> found_rows;
571 
572 	for (int pos = 0; pos < new_keys.size (); ++pos) {
573 		QString key = new_keys[pos];
574 		if (old_keys.value (pos) != key) {	// NOTE: old_keys could be shorter than new_keys!
575 			int old_pos = old_keys.indexOf (key);	// NOTE: -1 for new keys
576 			if (old_pos == active_row) activate_row = pos;
577 			position_changes.insert (pos, old_pos);
578 			if (old_pos >= 0) found_rows.insert (old_pos);
579 		} else found_rows.insert (pos);
580 	}
581 
582 	if (position_changes.isEmpty () && (old_keys.size () == new_keys.size ())) {
583 		return;	// no change
584 	}
585 
586 	// get state of current row (which may subsequently be moved or even deleted
587 	storeRowSerialization (active_row);
588 	updating = true;
589 
590 	// as a first step, take a backup of any rows that have been removed.
591 	for (int i = old_keys.size () - 1; i >= 0; --i) {
592 		if (!found_rows.contains (i)) {
593 			former_row_states.insert (old_keys[i], rows[i].full_row_map);
594 		}
595 	}
596 
597 	// update all columns
598 	QMap<RKComponentPropertyStringList *, ColumnInfo>::iterator it = column_map.begin ();
599 	for (; it != column_map.end (); ++it) {
600 		RKComponentPropertyStringList* col = it.key ();
601 		ColumnInfo &column = it.value ();
602 		if (column.external) continue;
603 
604 		// Ok, we'll have to adjust this column. We start by copying the old values, and padding to the
605 		// new length (if that is greater than the old).
606 		QStringList old_values = col->values ();
607 		QStringList new_values = old_values;
608 		for (int i = (new_keys.size () - new_values.size ()); i > 0; --i) new_values.append (QString ());
609 
610 		// adjust all positions that have changed
611 		for (int pos = 0; pos < new_keys.size (); ++pos) {
612 			QMap<int, int>::const_iterator pit = position_changes.find (pos);
613 			if (pit != position_changes.constEnd ()) {	// some change
614 				int old_pos = pit.value ();
615 				if (old_pos < 0) {	// a new key
616 					new_values[pos] = getDefaultValue (column, pos);
617 					activate_row = pos;
618 				} else {	// old key changed position
619 					new_values[pos] = old_values[old_pos];
620 				} // NOTE: not visible: old key is gone without replacement
621 			}
622 		}
623 
624 		// strip excess length (if any), and apply
625 		new_values = new_values.mid (0, new_keys.size ());
626 		col->setValueList (new_values);
627 	}
628 
629 	// update status info
630 	QList<RowInfo> new_row_info = rows;
631 	for (int i = (new_keys.size () - new_row_info.size ()); i > 0; --i) new_row_info.append (RowInfo (default_row_state));
632 	for (int pos = 0; pos < new_keys.size (); ++pos) {
633 		QMap<int, int>::const_iterator pit = position_changes.find (pos);
634 		if (pit != position_changes.constEnd ()) {	// some change
635 			int old_pos = pit.value ();
636 			if (old_pos < 0) {	// a new key (but it might have been known, formerly)
637 				new_row_info[pos] = RowInfo (former_row_states.value (new_keys[pos], default_row_state));
638 			} else {	// old key changed position
639 				new_row_info[pos] = rows[old_pos];
640 			} // NOTE: not visible: old key is gone without replacement
641 		}
642 	}
643 	rows = new_row_info.mid (0, new_keys.size ());
644 	n_invalid_rows = n_unfinished_rows = 0;
645 	for (int i = 0; i < rows.size (); ++i) {
646 		if (!rows[i].finished) ++n_unfinished_rows;
647 		if (!rows[i].valid) ++n_invalid_rows;
648 	}
649 
650 	old_keys = new_keys;
651 
652 	int nrows = new_keys.size ();
653 	row_count->setIntValue (nrows);
654 	if (model) model->triggerReset ();
655 	updating = false;
656 	activate_row = qMin (nrows - 1, activate_row);
657 	setContentsForRow (active_row = activate_row);
658 	current_row->setIntValue (active_row);
659 	changed ();
660 }
661 
applyContentsFromExternalColumn(RKComponentPropertyStringList * column,int row)662 void RKOptionSet::applyContentsFromExternalColumn (RKComponentPropertyStringList* column, int row) {
663 	RK_TRACE (PLUGIN);
664 
665 	const ColumnInfo &ci = column_map[column];
666 	if (!ci.external) return;
667 	if (ci.governor.isEmpty ()) return;
668 
669 	updating = true;
670 	QString dummy;
671 	RKComponentBase *governor = contents_container->lookupComponent (ci.governor, &dummy);
672 	if (governor && governor->isProperty ()) {
673 		RK_ASSERT (dummy.isEmpty ());
674 
675 		QString value;
676 		if (row >= 0) value = column->valueAt (row);
677 		else value = getDefaultValue (ci, row);
678 
679 		static_cast<RKComponentPropertyBase*> (governor)->setValue (value);
680 	} else {
681 		RK_DEBUG (PLUGIN, DL_WARNING, "Lookup error while trying to restore row %d of optionset: %s. Remainder: %s", row, qPrintable (ci.governor), qPrintable (dummy));
682 		RK_ASSERT (false);
683 	}
684 	updating = false;
685 }
686 
setContentsForRow(int row)687 void RKOptionSet::setContentsForRow (int row) {
688 	RK_TRACE (PLUGIN);
689 
690 	RK_ASSERT (rows.size () > row);
691 	if (row >= 0) {
692 		const PropertyValueMap *map = &(rows[row].full_row_map);
693 		// If some elements are disabled, these will *not* be contained in the serialization of a row (unless that is still at the default).
694 		// They *are* contained in the default_row_state, however, and thus we will apply any properties for which we do not have a value
695 		// from the default_row_state, instead.
696 		for (PropertyValueMap::const_iterator it = default_row_state.constBegin (); it != default_row_state.constEnd (); ++it) {
697 			if (!map->contains (it.key ())) {
698 				RKComponentPropertyBase *prop = contents_container->lookupProperty (it.key (), 0, true);
699 				if (prop) {		// found a property
700 					RK_ASSERT (!prop->isInternal ());
701 					prop->setValue (it.value ());
702 				}
703 			}
704 		}
705 		contents_container->setPropertyValues (map, false);
706 	} else {
707 		contents_container->setPropertyValues (&default_row_state, false);
708 	}
709 	QMap<RKComponentPropertyStringList *, ColumnInfo>::const_iterator it = column_map.constBegin ();
710 	for (; it != column_map.constEnd (); ++it) {
711 		RKComponentPropertyStringList* col = it.key ();
712 		applyContentsFromExternalColumn (col, row);
713 	}
714 	contents_container->enablednessProperty ()->setBoolValue (row >= 0);
715 }
716 
storeRowSerialization(int row)717 void RKOptionSet::storeRowSerialization (int row) {
718 	RK_TRACE (PLUGIN);
719 
720 	if (row < 0) return;	// No row was active
721 	RK_ASSERT (rows.size () > row);
722 	rows[row].full_row_map.clear ();
723 	contents_container->fetchPropertyValuesRecursive (&(rows[row].full_row_map));
724 }
725 
updateCurrentRowInDisplay()726 void RKOptionSet::updateCurrentRowInDisplay () {
727 	if (!(accordion && model)) return;	// can happen during initialization
728 
729 	if (active_row < 0) accordion->collapseAll ();
730 	else accordion->activateRow (active_row);
731 }
732 
currentRowChanged(int row)733 void RKOptionSet::currentRowChanged (int row) {
734 	RK_TRACE (PLUGIN);
735 
736 	if (active_row != row) current_row->setIntValue (row);
737 	// --> currentRowPropertyChanged ()
738 }
739 
currentRowPropertyChanged(RKComponentPropertyBase * property)740 void RKOptionSet::currentRowPropertyChanged (RKComponentPropertyBase *property) {
741 	RK_TRACE (PLUGIN);
742 
743 	RK_ASSERT (property == current_row);
744 	int row = qMin (row_count->intValue () - 1, current_row->intValue ());
745 	if (row != active_row) {	// May or may not be the case. True, e.g. if a row was removed
746 		storeRowSerialization (active_row);
747 		active_row = row;
748 		setContentsForRow (active_row);
749 	}
750 
751 	updateCurrentRowInDisplay ();	// Doing this, even if the current row _seeems_ unchanged, helps fixing up selection problems
752 }
753 
754 
755 /** reimplemented from RKComponent */
recursiveStatus()756 RKComponent::ComponentStatus RKOptionSet::recursiveStatus () {
757 	RK_TRACE (PLUGIN);
758 
759 	ComponentStatus s = RKComponent::recursiveStatus ();
760 	if (s == Dead) return s;
761 	if (n_unfinished_rows > 0) return Processing;
762 	return s;
763 }
764 
isValid()765 bool RKOptionSet::isValid () {
766 	if (n_invalid_rows > n_unfinished_rows) return false;
767 	int count = row_count->intValue ();
768 	if (count < min_rows) return false;
769 	if ((count > 0) && (count < min_rows_if_any)) return false;
770 	if (count > max_rows) return false;
771 	return true;
772 }
773 
774 
775 
776 
RKOptionSetDisplayModel(RKOptionSet * parent)777 RKOptionSetDisplayModel::RKOptionSetDisplayModel (RKOptionSet* parent) : QAbstractTableModel (parent) {
778 	RK_TRACE (PLUGIN);
779 	set = parent;
780 	connect (&reset_timer, &QTimer::timeout, this, &RKOptionSetDisplayModel::doResetNow);
781 	reset_timer.setInterval (0);
782 	reset_timer.setSingleShot (true);
783 }
784 
~RKOptionSetDisplayModel()785 RKOptionSetDisplayModel::~RKOptionSetDisplayModel () {
786 	RK_TRACE (PLUGIN);
787 }
788 
columnCount(const QModelIndex & parent) const789 int RKOptionSetDisplayModel::columnCount (const QModelIndex& parent) const {
790 	if (parent.isValid ()) return 0;
791 	return column_labels.size ();
792 }
793 
rowCount(const QModelIndex & parent) const794 int RKOptionSetDisplayModel::rowCount (const QModelIndex& parent) const {
795 	if (parent.isValid ()) return 0;
796 	return set->rowCount ();
797 }
798 
data(const QModelIndex & index,int role) const799 QVariant RKOptionSetDisplayModel::data (const QModelIndex& index, int role) const {
800 	if (!index.isValid ()) return QVariant ();
801 	int row = index.row ();
802 	int column = index.column ();
803 
804 	if (role == Qt::DisplayRole) {
805 		RKComponentPropertyStringList *p = set->visible_columns.value (column);
806 		if (p) {
807 			return QVariant (p->valueAt (row));
808 		} else {
809 			RK_ASSERT (false);
810 		}
811 	} else if (role == Qt::BackgroundRole) {
812 		const RKOptionSet::RowInfo &ri = set->rows[row];
813 		if (!ri.finished) return QColor (Qt::yellow);
814 		if (!ri.valid) return QColor (Qt::red);
815 	} else if ((role == Qt::ToolTipRole) || role == (Qt::StatusTipRole)) {
816 		const RKOptionSet::RowInfo &ri = set->rows[row];
817 		if (!ri.finished) return i18n ("This row has not yet been processed.");
818 		if (!ri.valid) return i18n ("This row contains invalid settings.");
819 	}
820 
821 	return QVariant ();
822 }
823 
doResetNow()824 void RKOptionSetDisplayModel::doResetNow () {
825 	RK_TRACE (PLUGIN);
826 	emit (layoutChanged ());
827 	set->updateCurrentRowInDisplay ();
828 }
829 
headerData(int section,Qt::Orientation orientation,int role) const830 QVariant RKOptionSetDisplayModel::headerData (int section, Qt::Orientation orientation, int role) const {
831 	if (orientation == Qt::Horizontal) {
832 		if (role == Qt::DisplayRole) return (column_labels.value (section));
833 		if (role == Qt::BackgroundRole) {
834 			if (set->n_unfinished_rows > 0) return QColor (Qt::yellow);
835 			if (!set->isValid ()) return QColor (Qt::red);
836 		}
837 		if ((role == Qt::ToolTipRole) || role == (Qt::StatusTipRole)) {
838 			if (set->n_unfinished_rows > 0) return i18n ("Please wait while settings are being processed");
839 			if (!set->isValid ()) {
840 				QStringList probs;
841 				if (set->n_invalid_rows > set->n_unfinished_rows) probs.append (i18n ("One or more rows contain invalid settings."));
842 				if ((set->rowCount () > 0) && (set->rowCount () < set->min_rows_if_any)) probs.append (i18n ("At least %1 rows have to be defined (if any)", set->min_rows_if_any));
843 				if (set->rowCount () < set->min_rows) probs.append (i18n ("At least %1 rows have to be defined", set->min_rows));
844 				if (set->rowCount () > set->max_rows) probs.append (i18n ("At most %1 rows may be defined", set->max_rows));
845 				return (QString ("<p>%1</p><ul><li>%2</li></ul>").arg (i18n ("This element is not valid for the following reason(s):")).arg (probs.join ("</li>\n<li>")));
846 			}
847 		}
848 	}
849 	return QVariant ();
850 }
851 
triggerReset()852 void RKOptionSetDisplayModel::triggerReset() {
853 	RK_TRACE (PLUGIN);
854 	if (!reset_timer.isActive ()) {
855 		emit (layoutAboutToBeChanged ());
856 		reset_timer.start ();
857 	}
858 }
859 
860 QString optionsetdisplaymodel_mt ("application/x-rkaccordiontableitem");
mimeTypes() const861 QStringList RKOptionSetDisplayModel::mimeTypes () const {
862 	return QStringList (optionsetdisplaymodel_mt);
863 }
864 
mimeData(const QModelIndexList & indexes) const865 QMimeData* RKOptionSetDisplayModel::mimeData (const QModelIndexList& indexes) const {
866 	RK_ASSERT (indexes.length () >= 1);
867 	QMimeData *ret = new QMimeData ();
868 	ret->setData (optionsetdisplaymodel_mt, QByteArray (QString::number (indexes.first ().row ()).toLatin1 ()));
869 	return (ret);
870 }
871 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)872 bool RKOptionSetDisplayModel::dropMimeData (const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) {
873 	Q_UNUSED (column);
874 	if (action == Qt::IgnoreAction) return true;
875 	if (action == Qt::MoveAction) {
876 		if (parent.isValid ()) return false;
877 		int srow = QString::fromLatin1 (data->data (optionsetdisplaymodel_mt)).toInt ();
878 		set->moveRow (srow, row);
879 	}
880 	return false;
881 }
882 
flags(const QModelIndex & index) const883 Qt::ItemFlags RKOptionSetDisplayModel::flags (const QModelIndex& index) const {
884 	return QAbstractItemModel::flags (index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
885 }
886 
supportedDropActions() const887 Qt::DropActions RKOptionSetDisplayModel::supportedDropActions () const {
888     return Qt::MoveAction;
889 }
890 
supportedDragActions() const891 Qt::DropActions RKOptionSetDisplayModel::supportedDragActions() const {
892 	return Qt::MoveAction;
893 }
894