1 /*
2     This file is part of the KDE Libraries
3     SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>
4     SPDX-FileCopyrightText: 2007 Rafael Fernández López <ereslibre@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "kpageview.h"
10 #include "kpageview_p.h"
11 
12 #include "kpagemodel.h"
13 #include "loggingcategory.h"
14 
15 #include <ktitlewidget.h>
16 
17 #include <QAbstractItemView>
18 #include <QGridLayout>
19 #include <QSize>
20 #include <QTimer>
21 
rebuildGui()22 void KPageViewPrivate::rebuildGui()
23 {
24     // clean up old view
25     Q_Q(KPageView);
26 
27     QModelIndex currentLastIndex;
28     if (view && view->selectionModel()) {
29         QObject::disconnect(m_selectionChangedConnection);
30         currentLastIndex = view->selectionModel()->currentIndex();
31     }
32 
33     delete view;
34     view = q->createView();
35 
36     Q_ASSERT(view);
37 
38     view->setSelectionBehavior(QAbstractItemView::SelectItems);
39     view->setSelectionMode(QAbstractItemView::SingleSelection);
40 
41     if (model) {
42         view->setModel(model);
43     }
44 
45     // setup new view
46     if (view->selectionModel()) {
47         m_selectionChangedConnection = QObject::connect(view->selectionModel(),
48                                                         &QItemSelectionModel::selectionChanged,
49                                                         q,
50                                                         [this](const QItemSelection &selected, const QItemSelection &deselected) {
51                                                             pageSelected(selected, deselected);
52                                                         });
53 
54         if (currentLastIndex.isValid()) {
55             view->selectionModel()->setCurrentIndex(currentLastIndex, QItemSelectionModel::Select);
56         } else if (model) {
57             view->selectionModel()->setCurrentIndex(model->index(0, 0), QItemSelectionModel::Select);
58         }
59     }
60 
61     if (faceType == KPageView::Tabbed) {
62         stack->setVisible(false);
63         layout->removeWidget(stack);
64     } else {
65         layout->addWidget(stack, 2, 1);
66         stack->setVisible(true);
67     }
68 
69     layout->removeWidget(titleWidget);
70 
71     if (pageHeader) {
72         layout->removeWidget(pageHeader);
73         pageHeader->setVisible(q->showPageHeader());
74         titleWidget->setVisible(false);
75 
76         if (faceType == KPageView::Tabbed) {
77             layout->addWidget(pageHeader, 1, 1);
78         } else {
79             layout->addWidget(pageHeader, 1, 1, 1, 2);
80         }
81     } else {
82         titleWidget->setVisible(q->showPageHeader());
83         if (faceType == KPageView::Tabbed) {
84             layout->addWidget(titleWidget, 1, 1);
85         } else {
86             layout->addWidget(titleWidget, 1, 1, 1, 2);
87         }
88     }
89 
90     Qt::Alignment alignment = q->viewPosition();
91     if (alignment & Qt::AlignTop) {
92         layout->addWidget(view, 2, 1);
93     } else if (alignment & Qt::AlignRight) {
94         layout->addWidget(view, 1, 2, 4, 1);
95     } else if (alignment & Qt::AlignBottom) {
96         layout->addWidget(view, 4, 1);
97     } else if (alignment & Qt::AlignLeft) {
98         layout->addWidget(view, 1, 0, 4, 1);
99     }
100 }
101 
updateSelection()102 void KPageViewPrivate::updateSelection()
103 {
104     // Select the first item in the view if not done yet.
105 
106     if (!model) {
107         return;
108     }
109 
110     if (!view || !view->selectionModel()) {
111         return;
112     }
113 
114     const QModelIndex index = view->selectionModel()->currentIndex();
115     if (!index.isValid()) {
116         view->selectionModel()->setCurrentIndex(model->index(0, 0), QItemSelectionModel::Select);
117     }
118 }
119 
cleanupPages()120 void KPageViewPrivate::cleanupPages()
121 {
122     // Remove all orphan pages from the stacked widget.
123 
124     const QList<QWidget *> widgets = collectPages();
125 
126     for (int i = 0; i < stack->count(); ++i) {
127         QWidget *page = stack->widget(i);
128 
129         bool found = false;
130         for (int j = 0; j < widgets.count(); ++j) {
131             if (widgets[j] == page) {
132                 found = true;
133             }
134         }
135 
136         if (!found) {
137             stack->removeWidget(page);
138         }
139     }
140 }
141 
collectPages(const QModelIndex & parentIndex)142 QList<QWidget *> KPageViewPrivate::collectPages(const QModelIndex &parentIndex)
143 {
144     // Traverse through the model recursive and collect all widgets in
145     // a list.
146     QList<QWidget *> retval;
147 
148     int rows = model->rowCount(parentIndex);
149     for (int j = 0; j < rows; ++j) {
150         const QModelIndex index = model->index(j, 0, parentIndex);
151         retval.append(qvariant_cast<QWidget *>(model->data(index, KPageModel::WidgetRole)));
152 
153         if (model->rowCount(index) > 0) {
154             retval += collectPages(index);
155         }
156     }
157 
158     return retval;
159 }
160 
effectiveFaceType() const161 KPageView::FaceType KPageViewPrivate::effectiveFaceType() const
162 {
163     if (faceType == KPageView::Auto) {
164         return detectAutoFace();
165     }
166 
167     return faceType;
168 }
169 
detectAutoFace() const170 KPageView::FaceType KPageViewPrivate::detectAutoFace() const
171 {
172     if (!model) {
173         return KPageView::Plain;
174     }
175 
176     // Check whether the model has sub pages.
177     bool hasSubPages = false;
178     const int count = model->rowCount();
179     for (int i = 0; i < count; ++i) {
180         if (model->rowCount(model->index(i, 0)) > 0) {
181             hasSubPages = true;
182             break;
183         }
184     }
185 
186     if (hasSubPages) {
187         return KPageView::Tree;
188     }
189 
190     if (model->rowCount() > 1) {
191         return KPageView::List;
192     }
193 
194     return KPageView::Plain;
195 }
196 
modelChanged()197 void KPageViewPrivate::modelChanged()
198 {
199     if (!model) {
200         return;
201     }
202 
203     // If the face type is Auto, we rebuild the GUI whenever the layout
204     // of the model changes.
205     if (faceType == KPageView::Auto) {
206         rebuildGui();
207         // If you discover some crashes use the line below instead...
208         // QTimer::singleShot(0, q, SLOT(rebuildGui()));
209     }
210 
211     // Set the stack to the minimum size of the largest widget.
212     QSize size = stack->size();
213     const QList<QWidget *> widgets = collectPages();
214     for (int i = 0; i < widgets.count(); ++i) {
215         const QWidget *widget = widgets[i];
216         if (widget) {
217             size = size.expandedTo(widget->minimumSizeHint());
218         }
219     }
220     stack->setMinimumSize(size);
221 
222     updateSelection();
223 }
224 
pageSelected(const QItemSelection & index,const QItemSelection & previous)225 void KPageViewPrivate::pageSelected(const QItemSelection &index, const QItemSelection &previous)
226 {
227     if (!model) {
228         return;
229     }
230 
231     // Return if the current Index is not valid
232     if (index.indexes().size() != 1) {
233         return;
234     }
235     QModelIndex currentIndex = index.indexes().first();
236 
237     QModelIndex previousIndex;
238     // The previous index can be invalid
239     if (previous.indexes().size() == 1) {
240         previousIndex = previous.indexes().first();
241     }
242 
243     if (faceType != KPageView::Tabbed) {
244         QWidget *widget = qvariant_cast<QWidget *>(model->data(currentIndex, KPageModel::WidgetRole));
245 
246         if (widget) {
247             if (stack->indexOf(widget) == -1) { // not included yet
248                 stack->addWidget(widget);
249             }
250 
251             stack->setCurrentWidget(widget);
252         } else {
253             stack->setCurrentWidget(defaultWidget);
254         }
255 
256         updateTitleWidget(currentIndex);
257     }
258 
259     Q_Q(KPageView);
260     Q_EMIT q->currentPageChanged(currentIndex, previousIndex);
261 }
262 
updateTitleWidget(const QModelIndex & index)263 void KPageViewPrivate::updateTitleWidget(const QModelIndex &index)
264 {
265     Q_Q(KPageView);
266 
267     const bool headerVisible = model->data(index, KPageModel::HeaderVisibleRole).toBool();
268     if (!headerVisible) {
269         titleWidget->setVisible(false);
270         return;
271     }
272     QString header = model->data(index, KPageModel::HeaderRole).toString();
273     if (header.isNull()) { // TODO KF6 remove that ugly logic, see also doxy-comments in KPageWidgetItem::setHeader()
274         header = model->data(index, Qt::DisplayRole).toString();
275     }
276 
277     titleWidget->setText(header);
278 
279     titleWidget->setVisible(q->showPageHeader());
280 }
281 
dataChanged(const QModelIndex &,const QModelIndex &)282 void KPageViewPrivate::dataChanged(const QModelIndex &, const QModelIndex &)
283 {
284     // When data has changed we update the header and icon for the currently selected
285     // page.
286     if (!view) {
287         return;
288     }
289 
290     QModelIndex index = view->selectionModel()->currentIndex();
291     if (!index.isValid()) {
292         return;
293     }
294 
295     updateTitleWidget(index);
296 }
297 
KPageViewPrivate(KPageView * _parent)298 KPageViewPrivate::KPageViewPrivate(KPageView *_parent)
299     : q_ptr(_parent)
300     , model(nullptr)
301     , faceType(KPageView::Auto)
302     , layout(nullptr)
303     , stack(nullptr)
304     , titleWidget(nullptr)
305     , view(nullptr)
306 {
307 }
308 
init()309 void KPageViewPrivate::init()
310 {
311     Q_Q(KPageView);
312     layout = new QGridLayout(q);
313     stack = new KPageStackedWidget(q);
314     titleWidget = new KTitleWidget(q);
315     layout->addWidget(titleWidget, 1, 1, 1, 2);
316     layout->addWidget(stack, 2, 1);
317 
318     defaultWidget = new QWidget(q);
319     stack->addWidget(defaultWidget);
320 
321     // stack should use most space
322     layout->setColumnStretch(1, 1);
323     layout->setRowStretch(2, 1);
324 }
325 
326 // KPageView Implementation
KPageView(QWidget * parent)327 KPageView::KPageView(QWidget *parent)
328     : KPageView(*new KPageViewPrivate(this), parent)
329 {
330 }
331 
KPageView(KPageViewPrivate & dd,QWidget * parent)332 KPageView::KPageView(KPageViewPrivate &dd, QWidget *parent)
333     : QWidget(parent)
334     , d_ptr(&dd)
335 {
336     d_ptr->init();
337 }
338 
339 KPageView::~KPageView() = default;
340 
setModel(QAbstractItemModel * model)341 void KPageView::setModel(QAbstractItemModel *model)
342 {
343     Q_D(KPageView);
344     // clean up old model
345     if (d->model) {
346         disconnect(d->m_layoutChangedConnection);
347         disconnect(d->m_dataChangedConnection);
348     }
349 
350     d->model = model;
351 
352     if (d->model) {
353         d->m_layoutChangedConnection = connect(d->model, &QAbstractItemModel::layoutChanged, this, [d]() {
354             d->modelChanged();
355         });
356         d->m_dataChangedConnection = connect(d->model, &QAbstractItemModel::dataChanged, this, [d](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
357             d->dataChanged(topLeft, bottomRight);
358         });
359 
360         // set new model in navigation view
361         if (d->view) {
362             d->view->setModel(model);
363         }
364     }
365 
366     d->rebuildGui();
367 }
368 
model() const369 QAbstractItemModel *KPageView::model() const
370 {
371     Q_D(const KPageView);
372     return d->model;
373 }
374 
setFaceType(FaceType faceType)375 void KPageView::setFaceType(FaceType faceType)
376 {
377     Q_D(KPageView);
378     d->faceType = faceType;
379 
380     d->rebuildGui();
381 }
382 
faceType() const383 KPageView::FaceType KPageView::faceType() const
384 {
385     Q_D(const KPageView);
386     return d->faceType;
387 }
388 
setCurrentPage(const QModelIndex & index)389 void KPageView::setCurrentPage(const QModelIndex &index)
390 {
391     Q_D(KPageView);
392     if (!d->view || !d->view->selectionModel()) {
393         return;
394     }
395 
396     d->view->selectionModel()->setCurrentIndex(index, QItemSelectionModel::SelectCurrent);
397 }
398 
currentPage() const399 QModelIndex KPageView::currentPage() const
400 {
401     Q_D(const KPageView);
402     if (!d->view || !d->view->selectionModel()) {
403         return QModelIndex();
404     }
405 
406     return d->view->selectionModel()->currentIndex();
407 }
408 
setItemDelegate(QAbstractItemDelegate * delegate)409 void KPageView::setItemDelegate(QAbstractItemDelegate *delegate)
410 {
411     Q_D(KPageView);
412     if (d->view) {
413         d->view->setItemDelegate(delegate);
414     }
415 }
416 
itemDelegate() const417 QAbstractItemDelegate *KPageView::itemDelegate() const
418 {
419     Q_D(const KPageView);
420     if (d->view) {
421         return d->view->itemDelegate();
422     } else {
423         return nullptr;
424     }
425 }
426 
setDefaultWidget(QWidget * widget)427 void KPageView::setDefaultWidget(QWidget *widget)
428 {
429     Q_D(KPageView);
430 
431     Q_ASSERT(widget);
432 
433     bool isCurrent = (d->stack->currentIndex() == d->stack->indexOf(d->defaultWidget));
434 
435     // remove old default widget
436     d->stack->removeWidget(d->defaultWidget);
437     delete d->defaultWidget;
438 
439     // add new default widget
440     d->defaultWidget = widget;
441     d->stack->addWidget(d->defaultWidget);
442 
443     if (isCurrent) {
444         d->stack->setCurrentWidget(d->defaultWidget);
445     }
446 }
447 
setPageHeader(QWidget * header)448 void KPageView::setPageHeader(QWidget *header)
449 {
450     Q_D(KPageView);
451     if (d->pageHeader == header) {
452         return;
453     }
454 
455     if (d->pageHeader) {
456         d->layout->removeWidget(d->pageHeader);
457     }
458     d->layout->removeWidget(d->titleWidget);
459 
460     d->pageHeader = header;
461 
462     // Give it a colSpan of 2 to add a margin to the right
463     if (d->pageHeader) {
464         d->layout->addWidget(d->pageHeader, 1, 1, 1, 2);
465         d->pageHeader->setVisible(showPageHeader());
466     } else {
467         d->layout->addWidget(d->titleWidget, 1, 1, 1, 2);
468         d->titleWidget->setVisible(showPageHeader());
469     }
470 }
471 
pageHeader() const472 QWidget *KPageView::pageHeader() const
473 {
474     Q_D(const KPageView);
475     if (!d->pageHeader) {
476         return d->titleWidget;
477     }
478     return d->pageHeader;
479 }
480 
setPageFooter(QWidget * footer)481 void KPageView::setPageFooter(QWidget *footer)
482 {
483     Q_D(KPageView);
484     if (d->pageFooter == footer) {
485         return;
486     }
487 
488     if (d->pageFooter) {
489         d->layout->removeWidget(d->pageFooter);
490     }
491 
492     d->pageFooter = footer;
493 
494     if (footer) {
495         d->layout->addWidget(d->pageFooter, 3, 1);
496     }
497 }
498 
pageFooter() const499 QWidget *KPageView::pageFooter() const
500 {
501     Q_D(const KPageView);
502     return d->pageFooter;
503 }
504 
createView()505 QAbstractItemView *KPageView::createView()
506 {
507     Q_D(KPageView);
508     const FaceType faceType = d->effectiveFaceType();
509 
510     if (faceType == Plain) {
511         return new KDEPrivate::KPagePlainView(this);
512     }
513     if (faceType == List) {
514         return new KDEPrivate::KPageListView(this);
515     }
516     if (faceType == Tree) {
517         return new KDEPrivate::KPageTreeView(this);
518     }
519     if (faceType == Tabbed) {
520         return new KDEPrivate::KPageTabbedView(this);
521     }
522 
523     return nullptr;
524 }
525 
showPageHeader() const526 bool KPageView::showPageHeader() const
527 {
528     Q_D(const KPageView);
529     const FaceType faceType = d->effectiveFaceType();
530 
531     if (faceType == Tabbed) {
532         return false;
533     } else {
534         return d->pageHeader || !d->titleWidget->text().isEmpty();
535     }
536 }
537 
viewPosition() const538 Qt::Alignment KPageView::viewPosition() const
539 {
540     Q_D(const KPageView);
541     const FaceType faceType = d->effectiveFaceType();
542 
543     if (faceType == Plain || faceType == Tabbed) {
544         return Qt::AlignTop;
545     } else {
546         return Qt::AlignLeft;
547     }
548 }
549 
550 #include "moc_kpageview.cpp"
551