1 /***************************************************************************
2 rkaccordiontable - description
3 -------------------
4 begin : Fri Oct 24 2015
5 copyright : (C) 2015-2018 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 "rkaccordiontable.h"
19
20 #include <QPointer>
21 #include <QTimer>
22 #include <QVBoxLayout>
23 #include <QAbstractProxyModel>
24 #include <QToolButton>
25 #include <QHBoxLayout>
26 #include <QLabel>
27
28 #include <KLocalizedString>
29
30 #include "rkcommonfunctions.h"
31 #include "rkstandardicons.h"
32
33 #include "../debug.h"
34
35 /** Maps from the Optionset data model to the model used internally in the RKAccordionTable
36 * (a dummy child item is inserted for each actual row). This item can _not_ actually be accessed
37 * in a meaningful way. The only purpose is to provide a placeholder to expand / collapse in the view. */
38 class RKAccordionDummyModel : public QAbstractProxyModel {
39 Q_OBJECT
40 public:
RKAccordionDummyModel(QObject * parent)41 RKAccordionDummyModel (QObject *parent) : QAbstractProxyModel (parent) {
42 add_trailing_columns = 1;
43 add_trailing_rows = 1;
44 };
45
mapFromSource(const QModelIndex & sindex) const46 QModelIndex mapFromSource (const QModelIndex& sindex) const override {
47 if (!sindex.isValid ()) return QModelIndex ();
48 // we're using Source row as "Internal ID", here. This _would_ fall on our feet when removing rows, _if_ we'd actually
49 // have to be able to map the dummy rows back to their real parents.
50 return (createIndex (sindex.row (), mapColumnFromSource (sindex.column ()), real_item_id));
51 }
52
mapColumnFromSource(int column) const53 inline int mapColumnFromSource (int column) const {
54 return qMax (0, column);
55 }
56
mapColumnToSource(int column) const57 inline int mapColumnToSource (int column) const {
58 return qMin (sourceModel ()->columnCount () - 1, column);
59 }
60
isFakeColumn(int column) const61 inline bool isFakeColumn (int column) const {
62 return (column >= mapColumnFromSource (sourceModel ()->columnCount ()));
63 }
64
mapToSource(const QModelIndex & pindex) const65 QModelIndex mapToSource (const QModelIndex& pindex) const override {
66 if (!pindex.isValid ()) return QModelIndex ();
67 if (pindex.internalId () == real_item_id) {
68 return sourceModel ()->index (pindex.row (), mapColumnToSource (pindex.column ()));
69 } else if (pindex.internalId () == trailing_item_id) {
70 return QModelIndex ();
71 } else {
72 return sourceModel ()->index (pindex.internalId (), 0);
73 }
74 }
75
flags(const QModelIndex & index) const76 Qt::ItemFlags flags (const QModelIndex& index) const override {
77 if (isFake (index)) {
78 if (index.internalId () == trailing_item_id) return (Qt::ItemIsEnabled);
79 return (Qt::NoItemFlags);
80 }
81 return (QAbstractProxyModel::flags (index));
82 }
83
rowCount(const QModelIndex & parent=QModelIndex ()) const84 int rowCount (const QModelIndex& parent = QModelIndex ()) const override {
85 if (isFake (parent)) return 0;
86 if (parent.isValid ()) return 1;
87 return sourceModel ()->rowCount (mapToSource (parent)) + add_trailing_rows;
88 }
89
data(const QModelIndex & proxyIndex,int role=Qt::DisplayRole) const90 QVariant data (const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const override {
91 if (isFake (proxyIndex)) {
92 if (proxyIndex.internalId () == trailing_item_id) {
93 if (role == Qt::DisplayRole) {
94 return i18n ("Click to add new row");
95 } else if (role == Qt::FontRole) {
96 QFont font;
97 font.setItalic (true);
98 return font;
99 } else if (role == Qt::DecorationRole) {
100 return RKStandardIcons::getIcon (RKStandardIcons::ActionInsertRow);
101 }
102 }
103 return QVariant ();
104 }
105 if (isFakeColumn (proxyIndex.column ()) && (role == Qt::DisplayRole)) return QVariant ();
106 return QAbstractProxyModel::data (proxyIndex, role);
107 }
108
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)109 bool dropMimeData (const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override {
110 // Ok, I don't understand why exactly, but something goes wrong while mapping this back to the source model. So we help it a bit:
111 Q_UNUSED (column);
112
113 if (isFake (parent)) {
114 RK_ASSERT (false);
115 return false;
116 }
117 if (parent.isValid ()) row = parent.row ();
118 return sourceModel ()->dropMimeData (data, action, row, 0, QModelIndex ());
119 }
120
headerData(int section,Qt::Orientation orientation,int role=Qt::DisplayRole) const121 QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override {
122 if ((orientation == Qt::Horizontal) && isFakeColumn (section) && (role == Qt::DisplayRole)) return QVariant ();
123 return QAbstractProxyModel::headerData (section, orientation, role);
124 }
125
hasChildren(const QModelIndex & parent) const126 bool hasChildren (const QModelIndex& parent) const override {
127 return (!isFake (parent));
128 }
129
columnCount(const QModelIndex & parent=QModelIndex ()) const130 int columnCount (const QModelIndex& parent = QModelIndex ()) const override {
131 if (isFake (parent)) return 1;
132 return mapColumnFromSource (sourceModel ()->columnCount (mapToSource (parent))) + add_trailing_columns;
133 }
134
index(int row,int column,const QModelIndex & parent=QModelIndex ()) const135 QModelIndex index (int row, int column, const QModelIndex& parent = QModelIndex ()) const override {
136 if (!parent.isValid ()) {
137 if (row == sourceModel ()->rowCount ()) return createIndex (row, column, trailing_item_id);
138 return createIndex (row, column, real_item_id);
139 }
140 RK_ASSERT (parent.internalId () >= trailing_item_id);
141 return createIndex (row, column, parent.row ());
142 }
143
parent(const QModelIndex & child) const144 QModelIndex parent (const QModelIndex& child) const override {
145 if (child.internalId () == real_item_id) return QModelIndex ();
146 else if (child.internalId () == trailing_item_id) return QModelIndex ();
147 return createIndex (child.internalId (), 0, real_item_id);
148 }
149
setSourceModel(QAbstractItemModel * source_model)150 void setSourceModel (QAbstractItemModel* source_model) override {
151 /* More than these would be needed for a proper proxy of any model, but in our case, we only have to support the RKOptionsetDisplayModel */
152 connect (source_model, &QAbstractItemModel::rowsInserted, this, &RKAccordionDummyModel::r_rowsInserted);
153 connect (source_model, &QAbstractItemModel::rowsRemoved, this, &RKAccordionDummyModel::r_rowsRemoved);
154 connect (source_model, &QAbstractItemModel::dataChanged, this, &RKAccordionDummyModel::r_dataChanged);
155 connect (source_model, &QAbstractItemModel::headerDataChanged, this, &RKAccordionDummyModel::r_headerDataChanged);
156 connect (source_model, &QAbstractItemModel::layoutChanged, this, &RKAccordionDummyModel::r_layoutChanged);
157 QAbstractProxyModel::setSourceModel (source_model);
158 }
159
isFake(const QModelIndex & index) const160 bool isFake (const QModelIndex& index) const {
161 return (index.isValid () && (index.internalId () != real_item_id));
162 }
163
164 static const quint32 real_item_id = 0xFFFFFFFF;
165 static const quint32 trailing_item_id = 0xFFFFFFFE;
166 int add_trailing_columns;
167 int add_trailing_rows;
168 public slots:
r_rowsInserted(const QModelIndex & parent,int start,int end)169 void r_rowsInserted (const QModelIndex& parent, int start, int end) {
170 RK_TRACE (MISC);
171 RK_ASSERT (!parent.isValid ());
172
173 beginInsertRows (mapFromSource (parent), start, end);
174 endInsertRows ();
175 }
r_rowsRemoved(const QModelIndex & parent,int start,int end)176 void r_rowsRemoved (const QModelIndex& parent, int start, int end) {
177 RK_TRACE (MISC);
178 RK_ASSERT (!parent.isValid ());
179
180 beginRemoveRows (mapFromSource (parent), start, end);
181 endRemoveRows ();
182 }
r_dataChanged(const QModelIndex & from,const QModelIndex & to)183 void r_dataChanged (const QModelIndex& from, const QModelIndex& to) {
184 emit (dataChanged (mapFromSource (from), mapFromSource (to)));
185 }
r_headerDataChanged(Qt::Orientation o,int from,int to)186 void r_headerDataChanged(Qt::Orientation o,int from,int to) {
187 emit (headerDataChanged (o, from, to));
188 }
r_layoutChanged()189 void r_layoutChanged () {
190 emit (layoutChanged());
191 }
192 };
193
194 /** Protects the given child widget from deletion */
195 class RKWidgetGuard : public QWidget {
196 public:
RKWidgetGuard(QWidget * parent,QWidget * widget_to_guard,QWidget * fallback_parent)197 RKWidgetGuard (QWidget *parent, QWidget *widget_to_guard, QWidget *fallback_parent) : QWidget (parent) {
198 RK_TRACE (MISC);
199 RK_ASSERT (widget_to_guard);
200
201 guarded = widget_to_guard;
202 RKWidgetGuard::fallback_parent = fallback_parent;
203
204 QVBoxLayout *layout = new QVBoxLayout (this);
205 layout->setContentsMargins (0, 0, 0, 0);
206 guarded->setParent (this);
207 layout->addWidget (guarded);
208 }
209
~RKWidgetGuard()210 ~RKWidgetGuard () {
211 RK_TRACE (MISC);
212 if ((!guarded.isNull ()) && guarded->parent () == this) {
213 guarded->setParent (fallback_parent);
214 }
215 }
216 private:
217 QPointer<QWidget> guarded;
218 QWidget *fallback_parent;
219 };
220
221 #include <QStyledItemDelegate>
222 #include <QPainter>
223 /** Responsible for drawing expand / collapse indicators in first column */
224 class RKAccordionDelegate : public QStyledItemDelegate {
225 public:
RKAccordionDelegate(RKAccordionTable * parent)226 RKAccordionDelegate (RKAccordionTable* parent) : QStyledItemDelegate (parent) {
227 table = parent;
228 expanded = RKStandardIcons::getIcon (RKStandardIcons::ActionCollapseUp);
229 collapsed = RKStandardIcons::getIcon (RKStandardIcons::ActionExpandDown);
230 }
initStyleOption(QStyleOptionViewItem * option,const QModelIndex & index) const231 void initStyleOption (QStyleOptionViewItem* option, const QModelIndex& index) const override {
232 QStyledItemDelegate::initStyleOption (option, index);
233 if (!pmodel->isFake (index)) {
234 option->icon = table->isExpanded (index) ? expanded : collapsed;
235 option->features |= QStyleOptionViewItem::HasDecoration;
236 }
237 }
238 RKAccordionDummyModel *pmodel;
239 RKAccordionTable* table;
240 QIcon expanded;
241 QIcon collapsed;
242 };
243
244 #include <QScrollBar>
245 #include <QHeaderView>
RKAccordionTable(QWidget * parent)246 RKAccordionTable::RKAccordionTable (QWidget* parent) : QTreeView (parent) {
247 RK_TRACE (MISC);
248
249 show_add_remove_buttons = false;
250 handling_a_click = false;
251
252 // This may seem like excessive wrapping. The point is to be able to manipulate the editor_widget_container's sizeHint(), while
253 // keeping the editor_widget's sizeHint() intact.
254 editor_widget_container = new QWidget ();
255 QHBoxLayout *layout = new QHBoxLayout (editor_widget_container);
256 layout->setContentsMargins (0, 0, 0, 0);
257 editor_widget = new QWidget (editor_widget_container);
258 new QVBoxLayout (editor_widget);
259 layout->addWidget (editor_widget);
260
261 setSelectionMode (SingleSelection);
262 setDragEnabled (true);
263 setAcceptDrops (true);
264 setDragDropMode (InternalMove);
265 setDropIndicatorShown (false);
266 setDragDropOverwriteMode (false);
267 setIndentation (0);
268 setRootIsDecorated (false);
269 setAlternatingRowColors (true);
270 setExpandsOnDoubleClick (false); // we expand on single click, instead
271 setItemsExpandable (false); // custom handling
272 setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
273
274 pmodel = new RKAccordionDummyModel (0);
275 RKAccordionDelegate* delegate = new RKAccordionDelegate (this);
276 delegate->pmodel = pmodel;
277 setItemDelegateForColumn (0, delegate);
278
279 connect (this, &QTreeView::expanded, this, &RKAccordionTable::rowExpanded);
280 connect (this, &QTreeView::clicked, this, &RKAccordionTable::rowClicked);
281 }
282
~RKAccordionTable()283 RKAccordionTable::~RKAccordionTable () {
284 RK_TRACE (MISC);
285
286 // Qt 4.8.6: The model must _not_ be a child of this view, and must _not_ be deleted along with it, or else there will be a crash
287 // on destruction _if_ (and only if) there are any trailing dummy rows. (Inside QAbstractItemModelPrivate::removePersistentIndexData())
288 // No, I do not understand this, yes, this is worrysome, but no idea, what could be the actual cause.
289 pmodel->deleteLater ();
290 delete editor_widget_container;
291 }
292
drawRow(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const293 void RKAccordionTable::drawRow (QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
294 if (index.parent ().isValid ()) { // must be the editor widget
295 painter->fillRect (option.rect, palette ().background ()); // fill to paper over any padding around the widget (whereever it comes from)
296 QTreeView::drawRow (painter, option, index);
297 painter->drawLine (option.rect.bottomLeft (), option.rect.bottomRight ());
298 } else {
299 QTreeView::drawRow (painter, option, index);
300 if (isExpanded (index)) {
301 painter->drawLine (option.rect.topLeft (), option.rect.topRight ());
302 }
303 }
304 }
305
setShowAddRemoveButtons(bool show)306 void RKAccordionTable::setShowAddRemoveButtons (bool show) {
307 RK_TRACE (MISC);
308 show_add_remove_buttons = show;
309 pmodel->add_trailing_columns = show;
310 pmodel->add_trailing_rows = show;
311 }
312
sizeHintWithoutEditor() const313 QSize RKAccordionTable::sizeHintWithoutEditor () const {
314 RK_TRACE (MISC);
315
316 // NOTE: This is not totally correct, but seems to be, roughly. We can't use sizeHintForRow(0) for height calculation, as the model may be empty
317 // (for "driven" optionsets.
318 return (QSize (minimumSizeHint ().width (), header ()->sizeHint().height () + horizontalScrollBar ()->sizeHint ().height () + QFontMetrics (QFont ()).lineSpacing () * 4));
319 }
320
sizeHint() const321 QSize RKAccordionTable::sizeHint () const {
322 RK_TRACE (MISC);
323
324 QSize swoe = sizeHintWithoutEditor ();
325 QSize min = editor_widget->sizeHint ();
326 min.setHeight (min.height () + swoe.height ());
327 min.setWidth (qMax (min.width (), swoe.width ()));
328 return min;
329 }
330
resizeEvent(QResizeEvent * event)331 void RKAccordionTable::resizeEvent (QResizeEvent* event) {
332 RK_TRACE (MISC);
333
334 QSize esh = editor_widget->sizeHint ();
335 int available_height = height () - sizeHintWithoutEditor ().height ();
336 int extra_height = available_height - esh.height ();
337 editor_widget_container->setMinimumHeight (esh.height () + qMax (0, 2 * (extra_height / 3)));
338 editor_widget_container->setMaximumWidth (width ());
339
340 QTreeView::resizeEvent (event);
341
342 // NOTE: For Qt 4.8.6, an expanded editor row will _not_ be updated, automatically.
343 // We have to force this by hiding / unhiding it.
344 QModelIndex expanded;
345 for (int i = 0; i < model ()->rowCount (); ++i) {
346 if (isExpanded (model ()->index (i, 0))) {
347 expanded = model ()->index (i, 0);
348 break;
349 }
350 }
351 if (expanded.isValid ()) {
352 setUpdatesEnabled (false);
353 setRowHidden (0, expanded, true);
354 setRowHidden (0, expanded, false);
355 setUpdatesEnabled (true);
356 }
357 }
358
activateRow(int row)359 void RKAccordionTable::activateRow (int row) {
360 RK_TRACE (MISC);
361
362 setExpanded (model ()->index (row, 0), true);
363 }
364
365 // Gaaah! currentIndexChanged() (on click) seems to be called _before_ rowClick ().
366 // But we _have to_ expand items activated via currentChanged() (could have been keyboard, too), and we _have to_
367 // expand items on click, if they are not expanded (as it could be the already-current-item), but we _must _not_
368 // collapse the item we have just expanded from currentChanged().
369 // To make things worse, rowClicked () is called with a delay (probably when Qt is sure it wasn't a double click),
370 // so a simple "swallow any click in this event cycle" is not enough.
371 // Solution, if mouse was clicked, prevent currentChanged() from handling anything.
mousePressEvent(QMouseEvent * event)372 void RKAccordionTable::mousePressEvent (QMouseEvent* event) {
373 handling_a_click = true;
374 QTreeView::mousePressEvent (event);
375 handling_a_click = false;
376 }
377
rowClicked(QModelIndex row)378 void RKAccordionTable::rowClicked (QModelIndex row) {
379 RK_TRACE (MISC);
380
381 row = model ()->index (row.row (), 0, row.parent ()); // Fix up index to point to column 0, or isExpanded() will always return false
382 if (isExpanded (row) && currentIndex ().row () == row.row ()) {
383 setExpanded (row, false);
384 } else {
385 if (!pmodel->isFake (row)) {
386 if (currentIndex ().row () == row.row ()) {
387 setExpanded (row, true);
388 }
389 // Expanding of rows, when current has changed is handled in currenChanged(), only.
390 }
391 }
392 if (!row.parent ().isValid ()) {
393 if (row.row () >= pmodel->rowCount () - pmodel->add_trailing_rows) {
394 emit (addRow (row.row ()));
395 }
396 }
397 }
398
currentChanged(const QModelIndex & current,const QModelIndex & previous)399 void RKAccordionTable::currentChanged (const QModelIndex& current, const QModelIndex& previous) {
400 RK_TRACE (MISC);
401 Q_UNUSED (previous);
402
403 if (handling_a_click) return;
404 if (!pmodel->isFake (current)) {
405 setExpanded (current, true);
406 emit (activated (current.row ()));
407 }
408 }
409
rowExpanded(QModelIndex row)410 void RKAccordionTable::rowExpanded (QModelIndex row) {
411 RK_TRACE (MISC);
412
413 for (int i = 0; i < pmodel->rowCount () - pmodel->add_trailing_rows; ++i) {
414 QModelIndex _row = model ()->index (i, 0);
415 if (i != row.row ()) {
416 setIndexWidget (model ()->index (0, 0, _row), 0);
417 setExpanded (_row, false);
418 }
419 }
420 setFirstColumnSpanned (0, row, true);
421 setIndexWidget (model ()->index (0, 0, row), new RKWidgetGuard (0, editor_widget_container, this));
422 setCurrentIndex (row);
423 scrollTo (row, EnsureVisible); // yes, we want both scrolls: We want the header row above the widget, if possible at all,
424 scrollTo (model ()->index (0, 0, row), EnsureVisible); // but of course, having the header row visible without the widget is not enough...
425 }
426
updateWidget()427 void RKAccordionTable::updateWidget () {
428 RK_TRACE (MISC);
429
430 bool seen_expanded = false;
431 for (int i = 0; i < pmodel->rowCount () - pmodel->add_trailing_rows; ++i) {
432 QModelIndex row = model ()->index (i, 0);
433 if (isExpanded (row) && !seen_expanded) {
434 rowExpanded (row);
435 seen_expanded = true;
436 }
437
438 if (show_add_remove_buttons) {
439 QModelIndex button_index = model ()->index (i, model ()->columnCount () - 1);
440 if (!indexWidget (button_index)) {
441 QWidget *display_buttons = new QWidget;
442 QHBoxLayout *layout = new QHBoxLayout (display_buttons);
443 layout->setContentsMargins (0, 0, 0, 0);
444 layout->setSpacing (0);
445
446 QToolButton *remove_button = new QToolButton (display_buttons);
447 remove_button->setAutoRaise (true);
448 connect (remove_button, &QToolButton::clicked, this, &RKAccordionTable::removeClicked);
449 remove_button->setIcon (RKStandardIcons::getIcon (RKStandardIcons::ActionDeleteRow));
450 RKCommonFunctions::setTips (i18n ("Remove this row / element"), remove_button);
451 layout->addWidget (remove_button);
452
453 setIndexWidget (button_index, display_buttons);
454
455 if (i == 0) {
456 header ()->setStretchLastSection (false); // we stretch the second to last, instead
457 header ()->resizeSection (button_index.column (), rowHeight (row));
458 header ()->setSectionResizeMode (button_index.column (), QHeaderView::Fixed);
459 header ()->setSectionResizeMode (button_index.column () - 1, QHeaderView::Stretch);
460 }
461 }
462 }
463 }
464
465 if (pmodel->add_trailing_rows) {
466 setFirstColumnSpanned (pmodel->rowCount () - pmodel->add_trailing_rows, QModelIndex (), true);
467 }
468 }
469
rowOfButton(QObject * button) const470 int RKAccordionTable::rowOfButton (QObject* button) const {
471 RK_TRACE (MISC);
472
473 if (!button) return -1;
474
475 // we rely on the fact that the buttons in use, here, are encapsulaped in a parent widget, which is set as indexWidget()
476 QObject* button_parent = button->parent ();
477 for (int i = model ()->rowCount () - 1; i >= 0; --i) {
478 QModelIndex row = model ()->index (i, model ()->columnCount () - 1);
479 if (button_parent == indexWidget (row)) {
480 return i;
481 }
482 }
483 RK_ASSERT (false);
484 return -1;
485 }
486
removeClicked()487 void RKAccordionTable::removeClicked () {
488 RK_TRACE (MISC);
489
490 int row = rowOfButton (sender ());
491 if (row < 0) {
492 RK_ASSERT (row >= 0);
493 return;
494 }
495 emit (removeRow (row));
496 }
497
setModel(QAbstractItemModel * model)498 void RKAccordionTable::setModel (QAbstractItemModel* model) {
499 RK_TRACE (MISC);
500
501 pmodel->setSourceModel (model);
502 QTreeView::setModel (pmodel);
503 connect (pmodel, &QAbstractItemModel::layoutChanged, this, &RKAccordionTable::updateWidget);
504 connect (pmodel, &QAbstractItemModel::rowsInserted, this, &RKAccordionTable::updateWidget);
505 connect (pmodel, &QAbstractItemModel::rowsRemoved, this, &RKAccordionTable::updateWidget);
506
507 if (pmodel->rowCount () > 0) expand (pmodel->index (0, 0));
508
509 updateWidget ();
510 updateGeometry (); // TODO: Not so clean to call this, here. But at this point we know the display_widget has been constructed, too
511 }
512
513 #include "rkaccordiontable.moc"
514