1 /***************************************************************************
2                           rkmatrixinput  -  description
3                              -------------------
4     begin                : Tue Oct 09 2012
5     copyright            : (C) 2012-2016 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 "rkmatrixinput.h"
19 
20 #include <QVBoxLayout>
21 #include <QLabel>
22 #include <QHeaderView>
23 
24 #include "../misc/rktableview.h"
25 #include "../dataeditor/rktextmatrix.h"
26 #include "kstandardaction.h"
27 #include "KLocalizedString"
28 
29 #include "../misc/xmlhelper.h"
30 
31 #include "../debug.h"
32 
RKMatrixInput(const QDomElement & element,RKComponent * parent_component,QWidget * parent_widget)33 RKMatrixInput::RKMatrixInput (const QDomElement& element, RKComponent* parent_component, QWidget* parent_widget) : RKComponent (parent_component, parent_widget) {
34 	RK_TRACE (PLUGIN);
35 
36 	is_valid = true;
37 
38 	// get xml-helper
39 	XMLHelper *xml = parent_component->xmlHelper ();
40 
41 	// create layout
42 	QVBoxLayout *vbox = new QVBoxLayout (this);
43 	vbox->setContentsMargins (0, 0, 0, 0);
44 
45 	QLabel *label = new QLabel (xml->i18nStringAttribute (element, "label", i18n ("Enter data:"), DL_INFO), this);
46 	vbox->addWidget (label);
47 
48 	display = new RKTableView (this);
49 	vbox->addWidget (display);
50 
51 	mode = static_cast<Mode> (xml->getMultiChoiceAttribute (element, "mode", "integer;real;string", 1, DL_WARNING));
52 	if (mode == Integer) {
53 		min = xml->getIntAttribute (element, "min", INT_MIN, DL_INFO) - .1;	// we'll only allow ints anyway. Make sure not to run into floating point precision issues.
54 		max = xml->getIntAttribute (element, "max", INT_MAX, DL_INFO) + .1;
55 	} else if (mode == Real) {
56 		min = xml->getDoubleAttribute (element, "min", -FLT_MAX, DL_INFO);
57 		max = xml->getDoubleAttribute (element, "max", FLT_MAX, DL_INFO);
58 	} else {
59 		min = -FLT_MAX;
60 		max = FLT_MAX;
61 	}
62 
63 	min_rows = xml->getIntAttribute (element, "min_rows", 0, DL_INFO);
64 	min_columns = xml->getIntAttribute (element, "min_columns", 0, DL_INFO);
65 
66 	// Note: string type matrix allows missings, implicitly (treating them as empty strings)
67 	allow_missings = xml->getBoolAttribute (element, "allow_missings", false, DL_INFO);
68 	if (mode == String) allow_missings = true;
69 	allow_user_resize_columns = xml->getBoolAttribute (element, "allow_user_resize_columns", true, DL_INFO);
70 	allow_user_resize_rows = xml->getBoolAttribute (element, "allow_user_resize_rows", true, DL_INFO);
71 	trailing_rows = allow_user_resize_rows ? 1 : 0;
72 	trailing_columns = allow_user_resize_columns ? 1 : 0;
73 
74 	row_count = new RKComponentPropertyInt (this, false, xml->getIntAttribute (element, "rows", qMax (2, min_rows), DL_INFO));
75 	column_count = new RKComponentPropertyInt (this, false, xml->getIntAttribute (element, "columns", qMax (2, min_columns), DL_INFO));
76 	tsv_data = new RKComponentPropertyBase (this, false);
77 	row_count->setInternal (true);
78 	addChild ("rows", row_count);
79 	column_count->setInternal (true);
80 	addChild ("columns", column_count);
81 	addChild ("tsv", tsv_data);
82 	connect (row_count, &RKComponentPropertyBase::valueChanged, this, &RKMatrixInput::dimensionPropertyChanged);
83 	connect (column_count, &RKComponentPropertyBase::valueChanged, this, &RKMatrixInput::dimensionPropertyChanged);
84 	connect (tsv_data, &RKComponentPropertyBase::valueChanged, this, &RKMatrixInput::tsvPropertyChanged);
85 	updating_tsv_data = false;
86 
87 	model = new RKMatrixInputModel (this);
88 	QString headers = xml->getStringAttribute (element, "horiz_headers", QString (), DL_INFO);
89 	if (!headers.isEmpty ()) model->horiz_header = headers.split (';');
90 	else if (!headers.isNull ()) display->horizontalHeader ()->hide ();	// attribute explicitly set to ""
91 	headers = xml->getStringAttribute (element, "vert_headers", QString (), DL_INFO);
92 	if (!headers.isEmpty ()) model->vert_header = headers.split (';');
93 	else if (!headers.isNull ()) display->verticalHeader ()->hide ();
94 	updateAll ();
95 	display->setModel (model);
96 	display->setAlternatingRowColors (true);
97 	if (xml->getBoolAttribute (element, "fixed_width", false, DL_INFO)) {
98 		display->horizontalHeader ()->setStretchLastSection (true);
99 	}
100 	if (xml->getBoolAttribute (element, "fixed_height", false, DL_INFO)) {
101 		int max_row = row_count->intValue () - 1;
102 		display->setHorizontalScrollBarPolicy (Qt::ScrollBarAlwaysOff);
103 		display->setFixedHeight (display->horizontalHeader ()->height () + display->rowViewportPosition (max_row) + display->rowHeight (max_row));
104 	}
105 
106 	// define standard actions
107 	QAction *cut = KStandardAction::cut (this, SLOT (cut()), this);
108 	display->addAction (cut);
109 	QAction *copy = KStandardAction::copy (this, SLOT (copy()), this);
110 	display->addAction (copy);
111 	QAction *paste = KStandardAction::paste (this, SLOT (paste()), this);
112 	display->addAction (paste);
113 	display->setContextMenuPolicy (Qt::ActionsContextMenu);
114 
115 	display->setRKItemDelegate (new RKItemDelegate (display, model, true));
116 	connect (display, &RKTableView::blankSelectionRequest, this, &RKMatrixInput::clearSelectedCells);
117 }
118 
~RKMatrixInput()119 RKMatrixInput::~RKMatrixInput () {
120 	RK_TRACE (PLUGIN);
121 }
122 
value(const QString & modifier)123 QVariant RKMatrixInput::value (const QString& modifier) {
124 	if (modifier.isEmpty () || (modifier == "cbind")) {
125 		QStringList ret;
126 		for (int i = 0; i < column_count->intValue (); ++i) {
127 			ret.append ("\tc (" + makeColumnString (i, ", ") + ')');
128 		}
129 		return QString ("cbind (\n" + ret.join (",\n") + "\n)");
130 	} else if (modifier.startsWith (QLatin1String ("row."))) {
131 		bool ok;
132 		int row = modifier.mid (4).toInt (&ok);
133 		if ((row >= 0) && ok) {
134 			return (rowStrings (row));
135 		}
136 	}
137 
138 	bool ok;
139 	int col = modifier.toInt (&ok);
140 	if ((col >= 0) && ok) {
141 		QStringList l;
142 		if (col < columns.size ()) l = columns[col].storage;
143 		while (l.size () < row_count->intValue ()) {
144 			l.append (QString ());
145 		}
146 		if (l.size () > row_count->intValue ()) l = l.mid (0, row_count->intValue ());
147 		return l;
148 	}
149 	return (QString ("Modifier '" + modifier + "' not recognized\n"));
150 }
151 
expandStorageForColumn(int column)152 bool RKMatrixInput::expandStorageForColumn (int column) {
153 	RK_TRACE (PLUGIN);
154 
155 	if ((column < 0) || (!allow_user_resize_columns && (column >= column_count->intValue ()))) {
156 		RK_ASSERT (false);
157 		return false;
158 	}
159 
160 	while (column >= columns.size ()) {
161 		Column col;
162 		col.last_valid_row = -1;
163 		columns.append (col);
164 	}
165 	return true;
166 }
167 
cellValue(int row,int column) const168 QString RKMatrixInput::cellValue (int row, int column) const {
169 	if ((column < 0) || (column >= columns.size ())) return (QString ());
170 	return columns[column].storage.value (row);
171 }
172 
173 
setCellValue(int row,int column,const QString & value)174 void RKMatrixInput::setCellValue (int row, int column, const QString& value) {
175 	RK_TRACE (PLUGIN);
176 
177 	if ((!expandStorageForColumn (column)) || (row < 0) || (!allow_user_resize_rows && (row >= row_count->intValue ()))) {
178 		RK_ASSERT (false);
179 		return;
180 	}
181 
182 	Column &col = columns[column];
183 	if (col.storage.value (row) == value) return;
184 
185 	while (row >= col.storage.size ()) {
186 		col.storage.append (QString ());
187 	}
188 	col.storage[row] = value;
189 	updateColumn (column);
190 	model->dataChanged (model->index (row, column), model->index (row, column));
191 }
192 
setColumnValue(int column,const QString & value)193 void RKMatrixInput::setColumnValue (int column, const QString& value) {
194 	RK_TRACE (PLUGIN);
195 
196 	if (!expandStorageForColumn (column)) return;
197 	columns[column].storage = value.split ('\t', QString::KeepEmptyParts);
198 	updateColumn (column);
199 	model->dataChanged (model->index (0, column), model->index (row_count->intValue () + trailing_rows, column));
200 }
201 
updateColumn(int column)202 void RKMatrixInput::updateColumn (int column) {
203 	RK_TRACE (PLUGIN);
204 	RK_ASSERT ((column >= 0) && (column < columns.size ()));
205 
206 	Column &col = columns[column];
207 
208 	// check for trailing empty rows:
209 	int last_row = col.storage.size ();
210 	while ((--last_row >= min_rows) && col.storage[last_row].isEmpty ()) {	// strip empty trailing strings
211 		col.storage.pop_back ();
212 	}
213 
214 	col.last_valid_row = -1;
215 	col.cached_tab_joined_string.clear (); // == no valid cache
216 
217 	updateAll ();
218 }
219 
pasteableValue(const QString & in,bool string)220 QString pasteableValue (const QString &in, bool string) {
221 	if (string) return (RObject::rQuote (in));
222 	else if (in.isEmpty ()) return ("NA");
223 	else return in;
224 }
225 
makeColumnString(int column,const QString & sep,bool r_pasteable)226 QString RKMatrixInput::makeColumnString (int column, const QString& sep, bool r_pasteable) {
227 	RK_TRACE (PLUGIN);
228 
229 	QStringList storage;
230 	if (column < columns.size ()) {
231 		storage = columns[column].storage;
232 	}
233 	QString ret;
234 	ret.reserve (3 * row_count->intValue ());	// a rather conservative estimate for most purposes
235 	for (int i = 0; i < row_count->intValue (); ++i) {
236 		if (i > 0) ret.append (sep);
237 		const QString val = storage.value (i);
238 		if (r_pasteable) ret.append (pasteableValue (val, mode == String));
239 		else ret.append (val);
240 	}
241 	return ret;
242 }
243 
rowStrings(int row)244 QStringList RKMatrixInput::rowStrings (int row) {
245 	RK_TRACE (PLUGIN);
246 
247 	QStringList ret;
248 	ret.reserve (column_count->intValue ());
249 	for (int i = 0; i < column_count->intValue (); ++i) {
250 		if (i < columns.size ()) ret.append (columns[i].storage.value (row));
251 		else ret.append (QString ());
252 	}
253 	return ret;
254 }
255 
updateAll()256 void RKMatrixInput::updateAll () {
257 	RK_TRACE (PLUGIN);
258 
259 	if (updating_tsv_data) return;
260 	updating_tsv_data = true;
261 
262 	int max_row = row_count->intValue () - 1;
263 	if (allow_user_resize_rows) {
264 		max_row = -1;
265 		for (int i = columns.size () - 1; i >= 0; --i) {
266 			max_row = qMax (max_row, columns[i].storage.size () - 1);
267 		}
268 	}
269 	max_row = qMax (min_rows - 1, max_row);
270 	if (max_row != row_count->intValue () - 1) {
271 		row_count->setIntValue (max_row + 1);
272 	}
273 
274 	int max_col = column_count->intValue () - 1;
275 	if (allow_user_resize_columns) {
276 		for (max_col = columns.size () - 1; max_col >= 0; --max_col) {
277 			if (!columns[max_col].storage.isEmpty ()) {
278 				break;
279 			}
280 		}
281 	}
282 	max_col = qMax (min_columns - 1, max_col);
283 	if (max_col != column_count->intValue () - 1) {
284 		column_count->setIntValue (max_col + 1);
285 	}
286 
287 	QStringList tsv;
288 	int i = 0;
289 	for (; i < columns.size (); ++i) {
290 		Column& col = columns[i];
291 		if (col.cached_tab_joined_string.isEmpty ()) {
292 			col.cached_tab_joined_string = makeColumnString (i, "\t", false);
293 		}
294 		tsv.append (col.cached_tab_joined_string);
295 	}
296 	for (; i < max_col; ++i) {
297 		tsv.append (QString (max_row, '\t'));
298 	}
299 	tsv_data->setValue (tsv.join ("\n"));
300 
301 	updating_tsv_data = false;
302 
303 	// finally, check whether table is valid, and signal change
304 	bool new_valid = true;
305 	for (int i = 0; i < column_count->intValue (); ++i) {
306 		if (!isColumnValid (i)) {
307 			new_valid = false;
308 			break;
309 		}
310 	}
311 	if (new_valid != is_valid) {
312 		is_valid = new_valid;
313 		model->headerDataChanged (Qt::Horizontal, 0, column_count->intValue () - 1);
314 	}
315 	changed ();
316 }
317 
dimensionPropertyChanged(RKComponentPropertyBase * property)318 void RKMatrixInput::dimensionPropertyChanged (RKComponentPropertyBase *property) {
319 	RK_TRACE (PLUGIN);
320 
321 	if (allow_user_resize_rows && (property == row_count)) {
322 		RK_ASSERT (updating_tsv_data);
323 	}
324 	if (allow_user_resize_columns && (property == column_count)) {
325 		RK_ASSERT (updating_tsv_data);
326 	}
327 
328 	if (property == row_count) {		// invalidates column caches
329 		for (int i = columns.size () - 1; i >= 0; --i) {
330 			columns[i].last_valid_row = qMin (columns[i].last_valid_row, row_count->intValue () - 1);
331 			columns[i].cached_tab_joined_string.clear ();
332 		}
333 	}
334 
335 	model->layoutAboutToBeChanged ();
336 	updateAll ();
337 	model->layoutChanged ();
338 }
339 
tsvPropertyChanged()340 void RKMatrixInput::tsvPropertyChanged () {
341 	if (updating_tsv_data) return;
342 	updating_tsv_data = true;
343 	RK_TRACE (PLUGIN);
344 
345 	columns.clear ();
346 	QStringList coldata = fetchStringValue (tsv_data).split ('\n', QString::KeepEmptyParts);
347 	for (int i = 0; i < coldata.size (); ++i) {
348 		setColumnValue (i, coldata[i]);
349 	}
350 
351 	updating_tsv_data = false;
352 	updateAll ();
353 }
354 
isValueValid(const QString & value) const355 bool RKMatrixInput::isValueValid (const QString& value) const {
356 	if (mode == String) return true;
357 	if (value.isEmpty ()) return allow_missings;
358 
359 	bool number_ok;
360 	double val;
361 	if (mode == Integer) {
362 		val = value.toInt (&number_ok);
363 	} else {
364 		val = value.toFloat (&number_ok);
365 	}
366 	if (!number_ok) return false;
367 	if (val < min) return false;
368 	return (val <= max);
369 }
370 
isColumnValid(int column)371 bool RKMatrixInput::isColumnValid (int column) {
372 	if (column < 0) {
373 		RK_ASSERT (false);
374 		return false;
375 	}
376 
377 	if (mode == String) return true;
378 
379 	if (column >= columns.size ()) return (allow_missings || (row_count->intValue () == 0));
380 
381 	Column &col = columns[column];
382 	if (col.last_valid_row >= (row_count->intValue () - 1)) {
383 		return true;
384 	} else if (allow_missings && (col.last_valid_row >= (col.storage.size () - 1))) {
385 		return true;
386 	}
387 
388 	int row = col.last_valid_row + 1;
389 	for (; row < col.storage.size (); ++row) {
390 		if (!isValueValid (col.storage[row])) {
391 			col.last_valid_row = row - 1;
392 			return false;
393 		}
394 	}
395 	col.last_valid_row = row - 1;
396 
397 	if (col.last_valid_row < (row_count->intValue () - 1)) {
398 		return allow_missings;
399 	}
400 	return true;
401 }
402 
clearSelectedCells()403 void RKMatrixInput::clearSelectedCells () {
404 	RK_TRACE (PLUGIN);
405 
406 	QItemSelectionRange range = display->getSelectionBoundaries ();
407 	if (!range.isValid ()) return;
408 
409 	updating_tsv_data = true;
410 	for (int col = range.left (); col <= range.right (); ++col) {
411 		for (int row = range.top (); row <= range.bottom (); ++row) {
412 			setCellValue (row, col, QString ());
413 		}
414 	}
415 	updating_tsv_data = false;
416 	updateAll ();
417 }
418 
copy()419 void RKMatrixInput::copy () {
420 	RK_TRACE (PLUGIN);
421 
422 	QItemSelectionRange range = display->getSelectionBoundaries ();
423 	if (!range.isValid ()) return;
424 
425 	RKTextMatrix ret;
426 	for (int col = range.left (); col <= range.right (); ++col) {
427 		for (int row = range.top (); row <= range.bottom (); ++row) {
428 			ret.setText (row - range.top (), col - range.left (), cellValue (row, col));
429 		}
430 	}
431 	ret.copyToClipboard ();
432 }
433 
cut()434 void RKMatrixInput::cut () {
435 	RK_TRACE (PLUGIN);
436 
437 	copy ();
438 	clearSelectedCells ();
439 }
440 
paste()441 void RKMatrixInput::paste () {
442 	RK_TRACE (PLUGIN);
443 
444 	int left = 0;
445 	int top = 0;
446 	QItemSelectionRange range = display->getSelectionBoundaries ();
447 	if (range.isValid ()) {
448 		left = range.left ();
449 		top = range.top ();
450 	}
451 
452 	RKTextMatrix pasted = RKTextMatrix::matrixFromClipboard ();
453 	int height = allow_user_resize_rows ? pasted.numRows () : qMin (pasted.numRows (), row_count->intValue () - top);
454 	int width = allow_user_resize_columns ? pasted.numColumns () : qMin (pasted.numColumns (), column_count->intValue () - left);
455 
456 	updating_tsv_data = true;
457 	for (int c = 0; c < width; ++c) {
458 		for (int r = 0; r < height; ++r) {
459 			setCellValue (r + top, c + left, pasted.getText (r, c));
460 		}
461 	}
462 	updating_tsv_data = false;
463 	updateAll ();
464 }
465 
466 
467 
468 
469 ///////////////////////////////////////////////////////////////////////////////////////
470 
RKMatrixInputModel(RKMatrixInput * _matrix)471 RKMatrixInputModel::RKMatrixInputModel (RKMatrixInput* _matrix) : QAbstractTableModel (_matrix) {
472 	RK_TRACE (PLUGIN);
473 
474 	matrix = _matrix;
475 }
476 
~RKMatrixInputModel()477 RKMatrixInputModel::~RKMatrixInputModel () {
478 	RK_TRACE (PLUGIN);
479 }
480 
columnCount(const QModelIndex & parent) const481 int RKMatrixInputModel::columnCount (const QModelIndex& parent) const {
482 	if (parent.isValid ()) return 0;
483 	return matrix->column_count->intValue () + matrix->trailing_columns;
484 }
485 
rowCount(const QModelIndex & parent) const486 int RKMatrixInputModel::rowCount (const QModelIndex& parent) const {
487 	if (parent.isValid ()) return 0;
488 	return matrix->row_count->intValue () + matrix->trailing_rows;
489 }
490 
data(const QModelIndex & index,int role) const491 QVariant RKMatrixInputModel::data (const QModelIndex& index, int role) const {
492 	if (!index.isValid ()) return QVariant ();
493 
494 	int row = index.row ();
495 	int column = index.column ();
496 
497 	// handle trailing rows / cols in user expandable tables
498 	bool trailing = false;
499 	if (row >= matrix->row_count->intValue ()) {
500 		if ((!matrix->allow_user_resize_rows) || (row >= matrix->row_count->intValue () + matrix->trailing_rows)) {
501 			RK_ASSERT (false);
502 			return QVariant ();
503 		}
504 		trailing = true;
505 	}
506 	if (column >= matrix->column_count->intValue ()) {
507 		if ((!matrix->allow_user_resize_columns) || (column >= matrix->column_count->intValue () + matrix->trailing_columns)) {
508 			RK_ASSERT (false);
509 			return QVariant ();
510 		}
511 		trailing = true;
512 	};
513 	if (trailing) {
514 		if (role == Qt::BackgroundRole) return QVariant (QBrush (Qt::gray));
515 		if (role == Qt::ToolTipRole || role == Qt::StatusTipRole) return QVariant (i18n ("Type on these cells to expand the table"));
516 		return QVariant ();
517 	}
518 
519 	// regular cells
520 	QString value = matrix->cellValue (row, column);
521 	if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) {
522 		return QVariant (value);
523 	} else if (role == Qt::BackgroundRole) {
524 		if (!matrix->is_valid && !matrix->isValueValid (value)) return QVariant (QColor (Qt::red));
525 	} else if ((role == Qt::ToolTipRole) || (role == Qt::StatusTipRole)) {
526 		if (!matrix->is_valid && (value.isEmpty () && !matrix->allow_missings)) return QVariant (i18n ("Empty values are not allowed"));
527 		if (!matrix->is_valid && !matrix->isValueValid (value)) return QVariant (i18n ("This value is not allowed, here"));
528 	}
529 	return QVariant ();
530 }
531 
flags(const QModelIndex & index) const532 Qt::ItemFlags RKMatrixInputModel::flags (const QModelIndex& index) const {
533 	// handle trailing rows / cols in user expandable tables
534 	if ((index.row () > matrix->row_count->intValue ()) || (index.column () > matrix->column_count->intValue ())) {
535 		return (Qt::ItemIsEditable | Qt::ItemIsEnabled);
536 	}
537 	return (Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
538 }
539 
headerData(int section,Qt::Orientation orientation,int role) const540 QVariant RKMatrixInputModel::headerData (int section, Qt::Orientation orientation, int role) const {
541 	if (role == Qt::DisplayRole) {
542 		const QStringList* list;
543 		if (orientation == Qt::Horizontal) list = &horiz_header;
544 		else list = &vert_header;
545 		if (section < list->size ()) return QVariant ((*list)[section]);
546 		return QVariant (QString::number (section + 1));
547 	} else if (orientation == Qt::Horizontal) {
548 		if (section < matrix->column_count->intValue ()) {
549 			if ((role == Qt::BackgroundRole) && !matrix->isColumnValid (section)) return QVariant (QColor (Qt::red));
550 			if (((role == Qt::ToolTipRole) || (role == Qt::StatusTipRole)) && !matrix->isColumnValid (section)) return QVariant (i18n ("This column contains illegal values in some of its cells"));
551 		}
552 	}
553 	return QVariant ();
554 }
555 
setData(const QModelIndex & index,const QVariant & value,int role)556 bool RKMatrixInputModel::setData (const QModelIndex& index, const QVariant& value, int role) {
557 	RK_TRACE (PLUGIN);
558 
559 	if (!index.isValid ()) return false;
560 	if (role != Qt::EditRole) return false;
561 	matrix->setCellValue (index.row (), index.column (), value.toString ());
562 	return true;
563 }
564 
565