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