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

source code of kwidgetsaddons/src/kpageview.cpp