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 SPDX-FileCopyrightText: 2024 g10 Code GmbH
6 SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
7
8 SPDX-License-Identifier: LGPL-2.0-or-later
9*/
10
11#include "kpageview.h"
12#include "kpageview_p.h"
13
14#include "common_helpers_p.h"
15#include "kpagemodel.h"
16#include "kpagewidgetmodel.h"
17#include "loggingcategory.h"
18
19#include <ktitlewidget.h>
20
21#include <QAbstractButton>
22#include <QAbstractItemView>
23#include <QApplication>
24#include <QCheckBox>
25#include <QComboBox>
26#include <QEvent>
27#include <QGridLayout>
28#include <QLabel>
29#include <QPaintEvent>
30#include <QPainter>
31#include <QProxyStyle>
32#include <QSize>
33#include <QTimer>
34#include <QToolButton>
35#include <QWidgetAction>
36
37// Remove the additional margin of the toolbar
38class NoPaddingToolBarProxyStyle : public QProxyStyle
39{
40public:
41 int pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const override
42 {
43 if (metric == QStyle::PM_ToolBarItemMargin) {
44 return 0;
45 }
46 return QProxyStyle::pixelMetric(metric, option, widget);
47 }
48};
49
50// Helper class that draws a rect over a matched widget
51class SearchMatchOverlay : public QWidget
52{
53public:
54 SearchMatchOverlay(QWidget *parent, int tabIdx = -1)
55 : QWidget(parent)
56 , m_tabIdx(tabIdx)
57 {
58 setAttribute(Qt::WA_TransparentForMouseEvents, on: true);
59 resize_impl();
60 parent->installEventFilter(filterObj: this);
61
62 show();
63 raise();
64 }
65
66 int tabIndex() const
67 {
68 return m_tabIdx;
69 }
70
71private:
72 void doResize()
73 {
74 QMetaObject::invokeMethod(object: this, function: &SearchMatchOverlay::resize_impl, type: Qt::QueuedConnection);
75 }
76
77 void resize_impl()
78 {
79 if (m_tabIdx >= 0) {
80 auto tabBar = qobject_cast<QTabBar *>(object: parentWidget());
81 if (!tabBar) {
82 setVisible(false);
83 return;
84 }
85 const QRect r = tabBar->tabRect(index: m_tabIdx);
86 if (geometry() != r) {
87 setGeometry(r);
88 }
89 return;
90 }
91
92 if (parentWidget() && size() != parentWidget()->size()) {
93 resize(parentWidget()->size());
94 }
95 }
96
97 bool eventFilter(QObject *o, QEvent *e) override
98 {
99 if (parentWidget() && o == parentWidget() && (e->type() == QEvent::Resize || e->type() == QEvent::Show)) {
100 doResize();
101 }
102 return QWidget::eventFilter(watched: o, event: e);
103 }
104
105 void paintEvent(QPaintEvent *e) override
106 {
107 QPainter p(this);
108 p.setClipRegion(e->region());
109 QColor c = palette().brush(cg: QPalette::Active, cr: QPalette::Highlight).color();
110 c.setAlpha(110);
111 p.fillRect(rect(), color: c);
112 }
113
114 int m_tabIdx = -1;
115};
116
117void KPageViewPrivate::rebuildGui()
118{
119 // clean up old view
120 Q_Q(KPageView);
121
122 QModelIndex currentLastIndex;
123 if (view && view->selectionModel()) {
124 QObject::disconnect(m_selectionChangedConnection);
125 currentLastIndex = view->selectionModel()->currentIndex();
126 }
127
128 delete view;
129 view = q->createView();
130
131 Q_ASSERT(view);
132
133 view->setSelectionBehavior(QAbstractItemView::SelectItems);
134 view->setSelectionMode(QAbstractItemView::SingleSelection);
135
136 if (model) {
137 view->setModel(model);
138 }
139
140 // setup new view
141 if (view->selectionModel()) {
142 m_selectionChangedConnection = QObject::connect(sender: view->selectionModel(),
143 signal: &QItemSelectionModel::selectionChanged,
144 context: q,
145 slot: [this](const QItemSelection &selected, const QItemSelection &deselected) {
146 pageSelected(selected, deselected);
147 });
148
149 if (currentLastIndex.isValid()) {
150 view->selectionModel()->setCurrentIndex(index: currentLastIndex, command: QItemSelectionModel::Select);
151 } else if (model) {
152 view->selectionModel()->setCurrentIndex(index: model->index(row: 0, column: 0), command: QItemSelectionModel::Select);
153 }
154 }
155
156 if (faceType == KPageView::Tabbed) {
157 stack->setVisible(false);
158 layout->removeWidget(w: stack);
159 } else {
160 layout->addWidget(stack, row: 3, column: 1, rowSpan: 1, columnSpan: 2);
161 stack->setVisible(true);
162 }
163
164 titleWidget->setPalette(qApp->palette(titleWidget));
165
166 if (!hasSearchableView()) {
167 layout->removeWidget(w: searchLineEditContainer);
168 searchLineEditContainer->setVisible(false);
169 searchLineEdit->setEnabled(false);
170 titleWidget->setAutoFillBackground(false);
171 layout->setSpacing(0);
172 separatorLine->setVisible(false);
173 titleWidget->setObjectName("KPageView::TitleWidgetNonSearchable");
174 titleWidget->setContentsMargins(left: q_ptr->style()->pixelMetric(metric: QStyle::PM_LayoutLeftMargin),
175 top: q_ptr->style()->pixelMetric(metric: QStyle::PM_LayoutTopMargin),
176 right: q_ptr->style()->pixelMetric(metric: QStyle::PM_LayoutRightMargin),
177 bottom: q_ptr->style()->pixelMetric(metric: QStyle::PM_LayoutBottomMargin));
178 } else {
179 titleWidget->setObjectName("KPageView::TitleWidget");
180 searchLineEdit->setEnabled(true);
181 searchLineEditContainer->setVisible(true);
182 searchLineEditContainer->setSizePolicy(hor: QSizePolicy::Ignored, ver: QSizePolicy::Fixed);
183 separatorLine->setVisible(true);
184
185 // Adjust margins for a better alignment
186 searchLineEditContainer->setContentsMargins(left: 4, top: 3, right: 4, bottom: 3);
187 titleWidget->setContentsMargins(left: 5, top: 4, right: 4, bottom: 2);
188
189 // Adjust the search + title background so that it merges into the titlebar
190 layout->setSpacing(0);
191 layout->setContentsMargins({});
192 }
193
194 layout->removeWidget(w: titleWidget);
195 layout->removeWidget(w: actionsToolBar);
196
197 actionsToolBar->setVisible(q->showPageHeader());
198 if (pageHeader) {
199 layout->removeWidget(w: pageHeader);
200 pageHeader->setVisible(q->showPageHeader());
201 titleWidget->setVisible(false);
202
203 if (faceType == KPageView::Tabbed) {
204 layout->addWidget(pageHeader, row: 1, column: 1);
205 } else {
206 layout->addWidget(pageHeader, row: 1, column: 1);
207 layout->addWidget(actionsToolBar, row: 1, column: 2);
208 }
209 } else {
210 titleWidget->setVisible(q->showPageHeader());
211 if (faceType == KPageView::Tabbed) {
212 layout->addWidget(titleWidget, row: 1, column: 1);
213 } else {
214 layout->addWidget(titleWidget, row: 1, column: 1);
215 layout->addWidget(actionsToolBar, row: 1, column: 2);
216 }
217 }
218
219 Qt::Alignment alignment = q->viewPosition();
220 if (alignment & Qt::AlignTop) {
221 layout->addWidget(view, row: 2, column: 1);
222 } else if (alignment & Qt::AlignRight) {
223 // search line
224 layout->addWidget(searchLineEditContainer, row: 1, column: 2, Qt::AlignVCenter);
225 // item view below the search line
226 layout->addWidget(view, row: 3, column: 2, rowSpan: 3, columnSpan: 1);
227 } else if (alignment & Qt::AlignBottom) {
228 layout->addWidget(view, row: 4, column: 1);
229 } else if (alignment & Qt::AlignLeft) {
230 // search line
231 layout->addWidget(searchLineEditContainer, row: 1, column: 0, Qt::AlignVCenter);
232 // item view below the search line
233 layout->addWidget(view, row: 3, column: 0, rowSpan: 3, columnSpan: 1);
234 }
235}
236
237void KPageViewPrivate::updateSelection()
238{
239 // Select the first item in the view if not done yet.
240
241 if (!model) {
242 return;
243 }
244
245 if (!view || !view->selectionModel()) {
246 return;
247 }
248
249 const QModelIndex index = view->selectionModel()->currentIndex();
250 if (!index.isValid()) {
251 view->selectionModel()->setCurrentIndex(index: model->index(row: 0, column: 0), command: QItemSelectionModel::Select);
252 }
253}
254
255void KPageViewPrivate::cleanupPages()
256{
257 // Remove all orphan pages from the stacked widget.
258
259 const QList<QWidget *> widgets = collectPages();
260
261 for (int i = 0; i < stack->count(); ++i) {
262 QWidget *page = stack->widget(i);
263
264 bool found = false;
265 for (int j = 0; j < widgets.count(); ++j) {
266 if (widgets[j] == page) {
267 found = true;
268 }
269 }
270
271 if (!found) {
272 stack->removeWidget(w: page);
273 }
274 }
275}
276
277QList<QWidget *> KPageViewPrivate::collectPages(const QModelIndex &parentIndex)
278{
279 // Traverse through the model recursive and collect all widgets in
280 // a list.
281 QList<QWidget *> retval;
282
283 int rows = model->rowCount(parent: parentIndex);
284 for (int j = 0; j < rows; ++j) {
285 const QModelIndex index = model->index(row: j, column: 0, parent: parentIndex);
286 retval.append(t: qvariant_cast<QWidget *>(v: model->data(index, role: KPageModel::WidgetRole)));
287
288 if (model->rowCount(parent: index) > 0) {
289 retval += collectPages(parentIndex: index);
290 }
291 }
292
293 return retval;
294}
295
296KPageView::FaceType KPageViewPrivate::effectiveFaceType() const
297{
298 if (faceType == KPageView::Auto) {
299 return detectAutoFace();
300 }
301
302 return faceType;
303}
304
305KPageView::FaceType KPageViewPrivate::detectAutoFace() const
306{
307 if (!model) {
308 return KPageView::Plain;
309 }
310
311 // Check whether the model has sub pages.
312 bool hasSubPages = false;
313 const int count = model->rowCount();
314 for (int i = 0; i < count; ++i) {
315 if (model->rowCount(parent: model->index(row: i, column: 0)) > 0) {
316 hasSubPages = true;
317 break;
318 }
319 }
320
321 if (hasSubPages) {
322 return KPageView::Tree;
323 }
324
325 if (model->rowCount() > 1) {
326 return KPageView::List;
327 }
328
329 return KPageView::Plain;
330}
331
332void KPageViewPrivate::modelChanged()
333{
334 if (!model) {
335 return;
336 }
337
338 // If the face type is Auto, we rebuild the GUI whenever the layout
339 // of the model changes.
340 if (faceType == KPageView::Auto) {
341 rebuildGui();
342 // If you discover some crashes use the line below instead...
343 // QTimer::singleShot(0, q, SLOT(rebuildGui()));
344 }
345
346 // Set the stack to the minimum size of the largest widget.
347 QSize size = stack->size();
348 const QList<QWidget *> widgets = collectPages();
349 for (int i = 0; i < widgets.count(); ++i) {
350 const QWidget *widget = widgets[i];
351 if (widget) {
352 size = size.expandedTo(otherSize: widget->minimumSizeHint());
353 }
354 }
355 stack->setMinimumSize(size);
356
357 updateSelection();
358}
359
360void KPageViewPrivate::pageSelected(const QItemSelection &index, const QItemSelection &previous)
361{
362 if (!model) {
363 return;
364 }
365
366 // Return if the current Index is not valid
367 if (index.indexes().size() != 1) {
368 return;
369 }
370 QModelIndex currentIndex = index.indexes().first();
371
372 QModelIndex previousIndex;
373 // The previous index can be invalid
374 if (previous.indexes().size() == 1) {
375 previousIndex = previous.indexes().first();
376 }
377
378 if (faceType != KPageView::Tabbed) {
379 QWidget *widget = qvariant_cast<QWidget *>(v: model->data(index: currentIndex, role: KPageModel::WidgetRole));
380
381 if (widget) {
382 if (stack->indexOf(widget) == -1) { // not included yet
383 stack->addWidget(w: widget);
384 }
385
386 stack->setCurrentWidget(widget);
387 } else {
388 stack->setCurrentWidget(defaultWidget);
389 }
390
391 updateTitleWidget(index: currentIndex);
392 updateActionsLayout(index: currentIndex, previous: previousIndex);
393 }
394
395 Q_Q(KPageView);
396 Q_EMIT q->currentPageChanged(current: currentIndex, previous: previousIndex);
397}
398
399void KPageViewPrivate::updateTitleWidget(const QModelIndex &index)
400{
401 Q_Q(KPageView);
402
403 const bool headerVisible = model->data(index, role: KPageModel::HeaderVisibleRole).toBool();
404 if (!headerVisible) {
405 titleWidget->setVisible(false);
406 return;
407 }
408 QString header = model->data(index, role: KPageModel::HeaderRole).toString();
409 if (header.isEmpty()) {
410 header = model->data(index, role: Qt::DisplayRole).toString();
411 }
412
413 titleWidget->setText(text: header);
414
415 titleWidget->setVisible(q->showPageHeader());
416}
417
418void KPageViewPrivate::updateActionsLayout(const QModelIndex &index, const QModelIndex &previous)
419{
420 Q_Q(KPageView);
421
422 if (previous.isValid()) {
423 const auto previousActions = qvariant_cast<QList<QAction *>>(v: model->data(index, role: KPageModel::ActionsRole));
424 for (const auto action : previousActions) {
425 actionsToolBar->removeAction(action);
426 }
427 }
428
429 const auto actions = qvariant_cast<QList<QAction *>>(v: model->data(index, role: KPageModel::ActionsRole));
430 if (actions.isEmpty()) {
431 actionsToolBar->hide();
432 } else {
433 actionsToolBar->show();
434 for (const auto action : actions) {
435 actionsToolBar->addAction(action);
436 }
437 }
438}
439
440void KPageViewPrivate::dataChanged(const QModelIndex &, const QModelIndex &)
441{
442 // When data has changed we update the header and icon for the currently selected
443 // page.
444 if (!view) {
445 return;
446 }
447
448 QModelIndex index = view->selectionModel()->currentIndex();
449 if (!index.isValid()) {
450 return;
451 }
452
453 updateTitleWidget(index);
454}
455
456KPageViewPrivate::KPageViewPrivate(KPageView *_parent)
457 : q_ptr(_parent)
458 , model(nullptr)
459 , faceType(KPageView::Auto)
460 , layout(nullptr)
461 , stack(nullptr)
462 , titleWidget(nullptr)
463 , searchLineEditContainer(nullptr)
464 , searchLineEdit(nullptr)
465 , view(nullptr)
466{
467}
468
469void KPageViewPrivate::init()
470{
471 Q_Q(KPageView);
472 layout = new QGridLayout(q);
473 stack = new KPageStackedWidget(q);
474
475 titleWidget = new KTitleWidget(q);
476 titleWidget->setObjectName("KPageView::TitleWidget");
477 titleWidget->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Expanding);
478
479 separatorLine = new QFrame(q);
480 separatorLine->setFrameShape(QFrame::HLine);
481 separatorLine->setFixedHeight(1);
482 separatorLine->setFrameShadow(QFrame::Sunken);
483
484 actionsToolBar = new QToolBar(q);
485 actionsToolBar->setObjectName(QLatin1String("KPageView::TitleWidget"));
486 actionsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
487 actionsToolBar->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Expanding);
488 actionsToolBar->setStyle(new NoPaddingToolBarProxyStyle);
489 actionsToolBar->show();
490
491 // list view under it to the left
492 layout->addWidget(titleWidget, row: 1, column: 1);
493 layout->addWidget(actionsToolBar, row: 1, column: 1);
494 // separator
495 layout->addWidget(separatorLine, row: 2, column: 0, rowSpan: 1, columnSpan: 3);
496 // and then the actual page on the right
497 layout->addWidget(stack, row: 3, column: 1, rowSpan: 1, columnSpan: 2);
498
499 defaultWidget = new QWidget(q);
500 stack->addWidget(w: defaultWidget);
501
502 // stack should use most space
503 layout->setColumnStretch(column: 1, stretch: 1);
504 layout->setRowStretch(row: 3, stretch: 1);
505
506 searchLineEdit = new QLineEdit(defaultWidget);
507 searchTimer.setInterval(400);
508 searchTimer.setSingleShot(true);
509 searchTimer.callOnTimeout(args: q, args: [this] {
510 onSearchTextChanged();
511 });
512 q->setFocusProxy(searchLineEdit);
513 searchLineEdit->setPlaceholderText(KPageView::tr(s: "Search…", c: "@info:placeholder"));
514 searchLineEdit->setClearButtonEnabled(true);
515 auto a = new QAction(q);
516 a->setIcon(QIcon::fromTheme(QStringLiteral("search")));
517 searchLineEdit->addAction(action: a, position: QLineEdit::LeadingPosition);
518 q->connect(sender: searchLineEdit, signal: &QLineEdit::textChanged, context: &searchTimer, slot: QOverload<>::of(ptr: &QTimer::start));
519
520 searchLineEditContainer = new QWidget(q);
521 auto containerLayout = new QVBoxLayout(searchLineEditContainer);
522 containerLayout->setContentsMargins({});
523 containerLayout->setSpacing(0);
524 containerLayout->addWidget(searchLineEdit);
525 searchLineEditContainer->setObjectName("KPageView::Search");
526}
527
528static QList<KPageWidgetItem *> getAllPages(KPageWidgetModel *model, const QModelIndex &parent)
529{
530 const int rc = model->rowCount(parent);
531 QList<KPageWidgetItem *> ret;
532 for (int i = 0; i < rc; ++i) {
533 auto child = model->index(row: i, column: 0, parent);
534 auto item = model->item(index: child);
535 if (child.isValid() && item) {
536 ret << item;
537 ret << getAllPages(model, parent: child);
538 }
539 }
540 return ret;
541}
542
543template<typename WidgetType>
544static QList<QWidget *> hasMatchingText(const QString &text, QWidget *page)
545{
546 QList<QWidget *> ret;
547 const auto widgets = page->findChildren<WidgetType *>();
548 for (auto label : widgets) {
549 if (removeAcceleratorMarker(label->text()).contains(text, Qt::CaseInsensitive)) {
550 ret << label;
551 }
552 }
553 return ret;
554}
555
556template<>
557QList<QWidget *> hasMatchingText<QComboBox>(const QString &text, QWidget *page)
558{
559 QList<QWidget *> ret;
560 const auto comboxBoxes = page->findChildren<QComboBox *>();
561 for (auto cb : comboxBoxes) {
562 if (cb->findText(text, flags: Qt::MatchFlag::MatchContains) != -1) {
563 ret << cb;
564 }
565 }
566 return ret;
567}
568
569template<typename...>
570struct FindChildrenHelper {
571 static QList<QWidget *> hasMatchingTextForTypes(const QString &, QWidget *)
572 {
573 return {};
574 }
575};
576
577template<typename First, typename... Rest>
578struct FindChildrenHelper<First, Rest...> {
579 static QList<QWidget *> hasMatchingTextForTypes(const QString &text, QWidget *page)
580 {
581 return hasMatchingText<First>(text, page) << FindChildrenHelper<Rest...>::hasMatchingTextForTypes(text, page);
582 }
583};
584
585static QModelIndex walkTreeAndHideItems(QTreeView *tree, const QString &searchText, const QSet<QString> &pagesToHide, const QModelIndex &parent)
586{
587 QModelIndex current;
588 auto model = tree->model();
589 const int rows = model->rowCount(parent);
590 for (int i = 0; i < rows; ++i) {
591 const auto index = model->index(row: i, column: 0, parent);
592 const auto itemName = index.data().toString();
593 tree->setRowHidden(row: i, parent, hide: pagesToHide.contains(value: itemName) && !itemName.contains(s: searchText, cs: Qt::CaseInsensitive));
594 if (!searchText.isEmpty() && !tree->isRowHidden(row: i, parent) && !current.isValid()) {
595 current = model->index(row: i, column: 0, parent);
596 }
597 auto curr = walkTreeAndHideItems(tree, searchText, pagesToHide, parent: index);
598 if (!current.isValid()) {
599 current = curr;
600 }
601 }
602 return current;
603}
604
605bool KPageViewPrivate::hasSearchableView() const
606{
607 // We support search for only these two types as they can hide rows
608 return qobject_cast<KDEPrivate::KPageListView *>(object: view) || qobject_cast<KDEPrivate::KPageTreeView *>(object: view);
609}
610
611void KPageViewPrivate::onSearchTextChanged()
612{
613 if (!hasSearchableView()) {
614 return;
615 }
616
617 const QString text = searchLineEdit->text();
618 QSet<QString> pagesToHide;
619 std::vector<QWidget *> matchedWidgets;
620 if (!text.isEmpty()) {
621 const auto pages = getAllPages(model: static_cast<KPageWidgetModel *>(model), parent: {});
622 for (auto item : pages) {
623 const auto matchingWidgets = FindChildrenHelper<QLabel, QAbstractButton, QComboBox>::hasMatchingTextForTypes(text, page: item->widget());
624 if (matchingWidgets.isEmpty()) {
625 pagesToHide << item->name();
626 }
627 matchedWidgets.insert(position: matchedWidgets.end(), first: matchingWidgets.begin(), last: matchingWidgets.end());
628 }
629 }
630
631 if (model) {
632 QModelIndex current;
633 if (auto list = qobject_cast<QListView *>(object: view)) {
634 for (int i = 0; i < model->rowCount(); ++i) {
635 const auto itemName = model->index(row: i, column: 0).data().toString();
636 list->setRowHidden(row: i, hide: pagesToHide.contains(value: itemName) && !itemName.contains(s: text, cs: Qt::CaseInsensitive));
637 if (!text.isEmpty() && !list->isRowHidden(row: i) && !current.isValid()) {
638 current = model->index(row: i, column: 0);
639 }
640 }
641 } else if (auto tree = qobject_cast<QTreeView *>(object: view)) {
642 current = walkTreeAndHideItems(tree, searchText: text, pagesToHide, parent: {});
643 auto parent = current.parent();
644 while (parent.isValid()) {
645 tree->setRowHidden(row: parent.row(), parent: parent.parent(), hide: false);
646 parent = parent.parent();
647 }
648 } else {
649 qWarning() << "Unreacheable, unknown view:" << view;
650 Q_UNREACHABLE();
651 }
652
653 if (current.isValid()) {
654 view->setCurrentIndex(current);
655 }
656 }
657
658 qDeleteAll(c: m_searchMatchOverlays);
659 m_searchMatchOverlays.clear();
660
661 using TabWidgetAndPage = QPair<QTabWidget *, QWidget *>;
662 auto tabWidgetParent = [](QWidget *w) {
663 // Finds if @p w is in a QTabWidget and returns
664 // The QTabWidget + the widget in the stack where
665 // @p w lives
666 auto parent = w->parentWidget();
667 TabWidgetAndPage p = {nullptr, nullptr};
668 QVarLengthArray<QWidget *, 8> parentChain;
669 parentChain << parent;
670 while (parent) {
671 if (auto tw = qobject_cast<QTabWidget *>(object: parent)) {
672 if (parentChain.size() >= 3) {
673 // last == QTabWidget
674 // second last == QStackedWidget of QTabWidget
675 // third last => the widget we want
676 p.second = parentChain.value(i: (parentChain.size() - 1) - 2);
677 }
678 p.first = tw;
679 break;
680 }
681 parent = parent->parentWidget();
682 parentChain << parent;
683 }
684 return p;
685 };
686
687 for (auto w : matchedWidgets) {
688 if (w) {
689 m_searchMatchOverlays << new SearchMatchOverlay(w);
690 if (!w->isVisible()) {
691 const auto [tabWidget, page] = tabWidgetParent(w);
692 if (!tabWidget && !page) {
693 continue;
694 }
695 const int idx = tabWidget->indexOf(widget: page);
696 if (idx < 0) {
697 // qDebug() << page << tabWidget << "not found" << w;
698 continue;
699 }
700
701 const bool alreadyOverlayed =
702 std::any_of(first: m_searchMatchOverlays.cbegin(), last: m_searchMatchOverlays.cend(), pred: [tabbar = tabWidget->tabBar(), idx](SearchMatchOverlay *overlay) {
703 return idx == overlay->tabIndex() && tabbar == overlay->parentWidget();
704 });
705 if (!alreadyOverlayed) {
706 m_searchMatchOverlays << new SearchMatchOverlay(tabWidget->tabBar(), idx);
707 }
708 }
709 }
710 }
711}
712
713// KPageView Implementation
714KPageView::KPageView(QWidget *parent)
715 : KPageView(*new KPageViewPrivate(this), parent)
716{
717}
718
719KPageView::KPageView(KPageViewPrivate &dd, QWidget *parent)
720 : QWidget(parent)
721 , d_ptr(&dd)
722{
723 d_ptr->init();
724}
725
726KPageView::~KPageView() = default;
727
728void KPageView::setModel(QAbstractItemModel *model)
729{
730 Q_D(KPageView);
731 // clean up old model
732 if (d->model) {
733 disconnect(d->m_layoutChangedConnection);
734 disconnect(d->m_dataChangedConnection);
735 }
736
737 d->model = model;
738
739 if (d->model) {
740 d->m_layoutChangedConnection = connect(sender: d->model, signal: &QAbstractItemModel::layoutChanged, context: this, slot: [d]() {
741 d->modelChanged();
742 });
743 d->m_dataChangedConnection = connect(sender: d->model, signal: &QAbstractItemModel::dataChanged, context: this, slot: [d](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
744 d->dataChanged(topLeft, bottomRight);
745 });
746
747 // set new model in navigation view
748 if (d->view) {
749 d->view->setModel(model);
750 }
751 }
752
753 d->rebuildGui();
754}
755
756QAbstractItemModel *KPageView::model() const
757{
758 Q_D(const KPageView);
759 return d->model;
760}
761
762void KPageView::setFaceType(FaceType faceType)
763{
764 Q_D(KPageView);
765 d->faceType = faceType;
766
767 d->rebuildGui();
768}
769
770KPageView::FaceType KPageView::faceType() const
771{
772 Q_D(const KPageView);
773 return d->faceType;
774}
775
776void KPageView::setCurrentPage(const QModelIndex &index)
777{
778 Q_D(KPageView);
779 if (!d->view || !d->view->selectionModel()) {
780 return;
781 }
782
783 d->view->selectionModel()->setCurrentIndex(index, command: QItemSelectionModel::SelectCurrent);
784}
785
786QModelIndex KPageView::currentPage() const
787{
788 Q_D(const KPageView);
789 if (!d->view || !d->view->selectionModel()) {
790 return QModelIndex();
791 }
792
793 return d->view->selectionModel()->currentIndex();
794}
795
796void KPageView::setItemDelegate(QAbstractItemDelegate *delegate)
797{
798 Q_D(KPageView);
799 if (d->view) {
800 d->view->setItemDelegate(delegate);
801 }
802}
803
804QAbstractItemDelegate *KPageView::itemDelegate() const
805{
806 Q_D(const KPageView);
807 if (d->view) {
808 return d->view->itemDelegate();
809 } else {
810 return nullptr;
811 }
812}
813
814void KPageView::setDefaultWidget(QWidget *widget)
815{
816 Q_D(KPageView);
817
818 Q_ASSERT(widget);
819
820 bool isCurrent = (d->stack->currentIndex() == d->stack->indexOf(d->defaultWidget));
821
822 // remove old default widget
823 d->stack->removeWidget(w: d->defaultWidget);
824 delete d->defaultWidget;
825
826 // add new default widget
827 d->defaultWidget = widget;
828 d->stack->addWidget(w: d->defaultWidget);
829
830 if (isCurrent) {
831 d->stack->setCurrentWidget(d->defaultWidget);
832 }
833}
834
835void KPageView::setPageHeader(QWidget *header)
836{
837 Q_D(KPageView);
838 if (d->pageHeader == header) {
839 return;
840 }
841
842 if (d->pageHeader) {
843 d->layout->removeWidget(w: d->pageHeader);
844 }
845 d->layout->removeWidget(w: d->titleWidget);
846 d->layout->removeWidget(w: d->actionsToolBar);
847
848 d->pageHeader = header;
849
850 // Give it a colSpan of 2 to add a margin to the right
851 if (d->pageHeader) {
852 d->layout->addWidget(d->pageHeader, row: 1, column: 1, rowSpan: 1, columnSpan: 1);
853 d->layout->addWidget(d->actionsToolBar, row: 1, column: 2);
854 d->pageHeader->setVisible(showPageHeader());
855 } else {
856 d->layout->addWidget(d->titleWidget, row: 1, column: 1, rowSpan: 1, columnSpan: 1);
857 d->layout->addWidget(d->actionsToolBar, row: 1, column: 2);
858 d->titleWidget->setVisible(showPageHeader());
859 }
860}
861
862QWidget *KPageView::pageHeader() const
863{
864 Q_D(const KPageView);
865 if (!d->pageHeader) {
866 return d->titleWidget;
867 }
868 return d->pageHeader;
869}
870
871void KPageView::setPageFooter(QWidget *footer)
872{
873 Q_D(KPageView);
874 if (d->pageFooter == footer) {
875 return;
876 }
877
878 if (d->pageFooter) {
879 d->layout->removeWidget(w: d->pageFooter);
880 }
881
882 d->pageFooter = footer;
883
884 if (footer) {
885 d->pageFooter->setContentsMargins(left: 4, top: 4, right: 4, bottom: 4);
886 d->layout->addWidget(d->pageFooter, row: 4, column: 1, rowSpan: 1, columnSpan: 2);
887 }
888}
889
890QWidget *KPageView::pageFooter() const
891{
892 Q_D(const KPageView);
893 return d->pageFooter;
894}
895
896QAbstractItemView *KPageView::createView()
897{
898 Q_D(KPageView);
899 const FaceType faceType = d->effectiveFaceType();
900
901 if (faceType == Plain) {
902 return new KDEPrivate::KPagePlainView(this);
903 }
904 if (faceType == FlatList) {
905 return new KDEPrivate::KPageListView(this);
906 }
907 if (faceType == List) {
908 auto view = new KDEPrivate::KPageListView(this);
909 view->setItemDelegate(new KDEPrivate::KPageListViewDelegate(this));
910 view->setFlexibleWidth(true);
911 return view;
912 }
913 if (faceType == Tree) {
914 return new KDEPrivate::KPageTreeView(this);
915 }
916 if (faceType == Tabbed) {
917 return new KDEPrivate::KPageTabbedView(this);
918 }
919
920 return nullptr;
921}
922
923bool KPageView::showPageHeader() const
924{
925 Q_D(const KPageView);
926 const FaceType faceType = d->effectiveFaceType();
927
928 if (faceType == Tabbed) {
929 return false;
930 } else {
931 return d->pageHeader || !d->titleWidget->text().isEmpty();
932 }
933}
934
935Qt::Alignment KPageView::viewPosition() const
936{
937 Q_D(const KPageView);
938 const FaceType faceType = d->effectiveFaceType();
939
940 if (faceType == Plain || faceType == Tabbed) {
941 return Qt::AlignTop;
942 } else {
943 return Qt::AlignLeft;
944 }
945}
946
947#include "moc_kpageview.cpp"
948

source code of kwidgetsaddons/src/kpageview.cpp