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 |
38 | class NoPaddingToolBarProxyStyle : public QProxyStyle |
39 | { |
40 | public: |
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 |
51 | class SearchMatchOverlay : public QWidget |
52 | { |
53 | public: |
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 | |
71 | private: |
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 | |
117 | void 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 | |
237 | void 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 | |
255 | void 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 | |
277 | QList<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 | |
296 | KPageView::FaceType KPageViewPrivate::effectiveFaceType() const |
297 | { |
298 | if (faceType == KPageView::Auto) { |
299 | return detectAutoFace(); |
300 | } |
301 | |
302 | return faceType; |
303 | } |
304 | |
305 | KPageView::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 | |
332 | void 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 | |
360 | void 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 | |
399 | void KPageViewPrivate::updateTitleWidget(const QModelIndex &index) |
400 | { |
401 | Q_Q(KPageView); |
402 | |
403 | const bool = model->data(index, role: KPageModel::HeaderVisibleRole).toBool(); |
404 | if (!headerVisible) { |
405 | titleWidget->setVisible(false); |
406 | return; |
407 | } |
408 | QString = 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 | |
418 | void 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 | |
440 | void 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 | |
456 | KPageViewPrivate::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 | |
469 | void 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 | |
528 | static 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 | |
543 | template<typename WidgetType> |
544 | static 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 | |
556 | template<> |
557 | QList<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 | |
569 | template<typename...> |
570 | struct FindChildrenHelper { |
571 | static QList<QWidget *> hasMatchingTextForTypes(const QString &, QWidget *) |
572 | { |
573 | return {}; |
574 | } |
575 | }; |
576 | |
577 | template<typename First, typename... Rest> |
578 | struct 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 | |
585 | static 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 | |
605 | bool 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 | |
611 | void 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 |
714 | KPageView::KPageView(QWidget *parent) |
715 | : KPageView(*new KPageViewPrivate(this), parent) |
716 | { |
717 | } |
718 | |
719 | KPageView::KPageView(KPageViewPrivate &dd, QWidget *parent) |
720 | : QWidget(parent) |
721 | , d_ptr(&dd) |
722 | { |
723 | d_ptr->init(); |
724 | } |
725 | |
726 | KPageView::~KPageView() = default; |
727 | |
728 | void 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 | |
756 | QAbstractItemModel *KPageView::model() const |
757 | { |
758 | Q_D(const KPageView); |
759 | return d->model; |
760 | } |
761 | |
762 | void KPageView::setFaceType(FaceType faceType) |
763 | { |
764 | Q_D(KPageView); |
765 | d->faceType = faceType; |
766 | |
767 | d->rebuildGui(); |
768 | } |
769 | |
770 | KPageView::FaceType KPageView::faceType() const |
771 | { |
772 | Q_D(const KPageView); |
773 | return d->faceType; |
774 | } |
775 | |
776 | void 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 | |
786 | QModelIndex 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 | |
796 | void KPageView::setItemDelegate(QAbstractItemDelegate *delegate) |
797 | { |
798 | Q_D(KPageView); |
799 | if (d->view) { |
800 | d->view->setItemDelegate(delegate); |
801 | } |
802 | } |
803 | |
804 | QAbstractItemDelegate *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 | |
814 | void 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 | |
835 | void KPageView::(QWidget *) |
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 | |
862 | QWidget *KPageView::() const |
863 | { |
864 | Q_D(const KPageView); |
865 | if (!d->pageHeader) { |
866 | return d->titleWidget; |
867 | } |
868 | return d->pageHeader; |
869 | } |
870 | |
871 | void KPageView::(QWidget *) |
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 | |
890 | QWidget *KPageView::() const |
891 | { |
892 | Q_D(const KPageView); |
893 | return d->pageFooter; |
894 | } |
895 | |
896 | QAbstractItemView *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 | |
923 | bool KPageView::() 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 | |
935 | Qt::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 | |