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