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_p.h"
10
11#include <QApplication>
12#include <QHeaderView>
13#include <QPainter>
14#include <QScrollBar>
15#include <QTextLayout>
16#include <QVBoxLayout>
17
18#include "highcontrasthelper_p.h"
19#include "kpagemodel.h"
20#include "loggingcategory.h"
21
22constexpr const auto viewWidth = 300;
23
24using namespace KDEPrivate;
25
26// KPagePlainView
27
28KPagePlainView::KPagePlainView(QWidget *parent)
29 : QAbstractItemView(parent)
30{
31 hide();
32}
33
34QModelIndex KPagePlainView::indexAt(const QPoint &) const
35{
36 return QModelIndex();
37}
38
39void KPagePlainView::scrollTo(const QModelIndex &, ScrollHint)
40{
41}
42
43QRect KPagePlainView::visualRect(const QModelIndex &) const
44{
45 return QRect();
46}
47
48QModelIndex KPagePlainView::moveCursor(QAbstractItemView::CursorAction, Qt::KeyboardModifiers)
49{
50 return QModelIndex();
51}
52
53int KPagePlainView::horizontalOffset() const
54{
55 return 0;
56}
57
58int KPagePlainView::verticalOffset() const
59{
60 return 0;
61}
62
63bool KPagePlainView::isIndexHidden(const QModelIndex &) const
64{
65 return false;
66}
67
68void KPagePlainView::setSelection(const QRect &, QFlags<QItemSelectionModel::SelectionFlag>)
69{
70}
71
72QRegion KPagePlainView::visualRegionForSelection(const QItemSelection &) const
73{
74 return QRegion();
75}
76
77// KPageListView
78
79KPageListView::KPageListView(QWidget *parent)
80 : QListView(parent)
81{
82 if (layoutDirection() == Qt::RightToLeft) {
83 setProperty(name: "_breeze_borders_sides", value: QVariant::fromValue(value: QFlags{Qt::LeftEdge}));
84 } else {
85 setProperty(name: "_breeze_borders_sides", value: QVariant::fromValue(value: QFlags{Qt::RightEdge}));
86 }
87 setViewMode(QListView::ListMode);
88 setMovement(QListView::Static);
89 setVerticalScrollMode(QListView::ScrollPerPixel);
90
91 QFont boldFont(font());
92 boldFont.setBold(true);
93 setFont(boldFont);
94}
95
96KPageListView::~KPageListView()
97{
98}
99
100void KPageListView::setModel(QAbstractItemModel *model)
101{
102 connect(sender: model, signal: &QAbstractItemModel::layoutChanged, context: this, slot: &KPageListView::updateWidth);
103
104 QListView::setModel(model);
105
106 // Set our own selection model, which won't allow our current selection to be cleared
107 setSelectionModel(new KDEPrivate::SelectionModel(model, this));
108
109 updateWidth();
110}
111
112void KPageListView::changeEvent(QEvent *event)
113{
114 QListView::changeEvent(event);
115
116 if (event->type() == QEvent::FontChange) {
117 updateWidth();
118 }
119}
120
121void KPageListView::setFlexibleWidth(bool flexibleWidth)
122{
123 m_flexibleWidth = flexibleWidth;
124}
125
126void KPageListView::updateWidth()
127{
128 if (!model()) {
129 return;
130 }
131 if (m_flexibleWidth) {
132 setFixedWidth(sizeHintForColumn(column: 0) + verticalScrollBar()->sizeHint().width() + 5);
133 } else {
134 setFixedWidth(viewWidth);
135 }
136}
137
138// KPageTreeView
139
140KPageTreeView::KPageTreeView(QWidget *parent)
141 : QTreeView(parent)
142{
143 if (layoutDirection() == Qt::RightToLeft) {
144 setProperty(name: "_breeze_borders_sides", value: QVariant::fromValue(value: QFlags{Qt::LeftEdge}));
145 } else {
146 setProperty(name: "_breeze_borders_sides", value: QVariant::fromValue(value: QFlags{Qt::RightEdge}));
147 }
148 header()->hide();
149}
150
151void KPageTreeView::setModel(QAbstractItemModel *model)
152{
153 connect(sender: model, signal: &QAbstractItemModel::layoutChanged, context: this, slot: &KPageTreeView::updateWidth);
154
155 QTreeView::setModel(model);
156
157 // Set our own selection model, which won't allow our current selection to be cleared
158 setSelectionModel(new KDEPrivate::SelectionModel(model, this));
159
160 updateWidth();
161}
162
163void KPageTreeView::updateWidth()
164{
165 if (!model()) {
166 return;
167 }
168
169 int columns = model()->columnCount();
170
171 expandItems();
172
173 int width = 0;
174 for (int i = 0; i < columns; ++i) {
175 resizeColumnToContents(column: i);
176 width = qMax(a: width, b: sizeHintForColumn(column: i));
177 }
178
179 setFixedWidth(qMax(a: viewWidth, b: width + 25));
180}
181
182void KPageTreeView::expandItems(const QModelIndex &index)
183{
184 setExpanded(index, expand: true);
185
186 const int count = model()->rowCount(parent: index);
187 for (int i = 0; i < count; ++i) {
188 expandItems(index: model()->index(row: i, column: 0, parent: index));
189 }
190}
191
192// KPageTabbedView
193
194KPageTabbedView::KPageTabbedView(QWidget *parent)
195 : QAbstractItemView(parent)
196{
197 // hide the viewport of the QAbstractScrollArea
198 const QList<QWidget *> list = findChildren<QWidget *>();
199 for (int i = 0; i < list.count(); ++i) {
200 list[i]->hide();
201 }
202
203 setFrameShape(NoFrame);
204
205 QVBoxLayout *layout = new QVBoxLayout(this);
206 layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
207
208 mTabWidget = new QTabWidget(this);
209 setFocusProxy(mTabWidget);
210 mTabWidget->setDocumentMode(true);
211 mTabWidget->tabBar()->setExpanding(true);
212 connect(sender: mTabWidget, signal: &QTabWidget::currentChanged, context: this, slot: &KPageTabbedView::currentPageChanged);
213
214 layout->addWidget(mTabWidget);
215}
216
217KPageTabbedView::~KPageTabbedView()
218{
219 if (model()) {
220 for (int i = 0; i < mTabWidget->count(); ++i) {
221 QWidget *page = qvariant_cast<QWidget *>(v: model()->data(index: model()->index(row: i, column: 0), role: KPageModel::WidgetRole));
222
223 if (page) {
224 page->setVisible(false);
225 page->setParent(nullptr); // reparent our children before they are deleted
226 }
227 }
228 }
229}
230
231void KPageTabbedView::setModel(QAbstractItemModel *model)
232{
233 QAbstractItemView::setModel(model);
234
235 connect(sender: model, signal: &QAbstractItemModel::layoutChanged, context: this, slot: &KPageTabbedView::layoutChanged);
236
237 layoutChanged();
238}
239
240QModelIndex KPageTabbedView::indexAt(const QPoint &) const
241{
242 if (model()) {
243 return model()->index(row: 0, column: 0);
244 } else {
245 return QModelIndex();
246 }
247}
248
249void KPageTabbedView::scrollTo(const QModelIndex &index, ScrollHint)
250{
251 if (!index.isValid()) {
252 return;
253 }
254
255 mTabWidget->setCurrentIndex(index.row());
256}
257
258QRect KPageTabbedView::visualRect(const QModelIndex &) const
259{
260 return QRect();
261}
262
263QSize KPageTabbedView::minimumSizeHint() const
264{
265 return mTabWidget->minimumSizeHint();
266}
267
268QModelIndex KPageTabbedView::moveCursor(QAbstractItemView::CursorAction, Qt::KeyboardModifiers)
269{
270 return QModelIndex();
271}
272
273int KPageTabbedView::horizontalOffset() const
274{
275 return 0;
276}
277
278int KPageTabbedView::verticalOffset() const
279{
280 return 0;
281}
282
283bool KPageTabbedView::isIndexHidden(const QModelIndex &index) const
284{
285 return (mTabWidget->currentIndex() != index.row());
286}
287
288void KPageTabbedView::setSelection(const QRect &, QFlags<QItemSelectionModel::SelectionFlag>)
289{
290}
291
292QRegion KPageTabbedView::visualRegionForSelection(const QItemSelection &) const
293{
294 return QRegion();
295}
296
297void KPageTabbedView::currentPageChanged(int index)
298{
299 if (!model()) {
300 return;
301 }
302
303 QModelIndex modelIndex = model()->index(row: index, column: 0);
304
305 selectionModel()->setCurrentIndex(index: modelIndex, command: QItemSelectionModel::ClearAndSelect);
306}
307
308void KPageTabbedView::layoutChanged()
309{
310 // save old position
311 int pos = mTabWidget->currentIndex();
312
313 // clear tab bar
314 int count = mTabWidget->count();
315 for (int i = 0; i < count; ++i) {
316 mTabWidget->removeTab(index: 0);
317 }
318
319 if (!model()) {
320 return;
321 }
322
323 // add new tabs
324 for (int i = 0; i < model()->rowCount(); ++i) {
325 const QString title = model()->data(index: model()->index(row: i, column: 0)).toString();
326 const QIcon icon = model()->data(index: model()->index(row: i, column: 0), role: Qt::DecorationRole).value<QIcon>();
327 QWidget *page = qvariant_cast<QWidget *>(v: model()->data(index: model()->index(row: i, column: 0), role: KPageModel::WidgetRole));
328 if (page) {
329 QWidget *widget = new QWidget(this);
330 QVBoxLayout *layout = new QVBoxLayout(widget);
331 layout->setContentsMargins({});
332 layout->addWidget(page);
333 page->setVisible(true);
334 mTabWidget->addTab(widget, icon, label: title);
335 }
336 }
337
338 mTabWidget->setCurrentIndex(pos);
339}
340
341void KPageTabbedView::dataChanged(const QModelIndex &index, const QModelIndex &, const QList<int> &roles)
342{
343 if (!index.isValid()) {
344 return;
345 }
346
347 if (index.row() < 0 || index.row() >= mTabWidget->count()) {
348 return;
349 }
350
351 if (roles.isEmpty() || roles.contains(t: Qt::DisplayRole) || roles.contains(t: Qt::DecorationRole)) {
352 const QString title = model()->data(index).toString();
353 const QIcon icon = model()->data(index, role: Qt::DecorationRole).value<QIcon>();
354
355 mTabWidget->setTabText(index: index.row(), text: title);
356 mTabWidget->setTabIcon(index: index.row(), icon);
357 }
358}
359
360// KPageListViewDelegate
361
362KPageListViewDelegate::KPageListViewDelegate(QObject *parent)
363 : QAbstractItemDelegate(parent)
364{
365}
366
367static int layoutText(QTextLayout *layout, int maxWidth)
368{
369 qreal height = 0;
370 int textWidth = 0;
371 layout->beginLayout();
372 while (true) {
373 QTextLine line = layout->createLine();
374 if (!line.isValid()) {
375 break;
376 }
377 line.setLineWidth(maxWidth);
378 line.setPosition(QPointF(0, height));
379 height += line.height();
380 textWidth = qMax(a: textWidth, b: qRound(d: line.naturalTextWidth() + 0.5));
381 }
382 layout->endLayout();
383 return textWidth;
384}
385
386void KPageListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
387{
388 if (!index.isValid()) {
389 return;
390 }
391
392 QStyleOptionViewItem opt(option);
393 opt.showDecorationSelected = true;
394 QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
395
396 const QIcon::Mode iconMode = (option.state & QStyle::State_Selected) && ((option.state & QStyle::State_Active) || isHighContrastColorSchemeInUse())
397 ? QIcon::Selected
398 : QIcon::Normal;
399 int iconSize = style->pixelMetric(metric: QStyle::PM_IconViewIconSize);
400 const QString text = index.model()->data(index, role: Qt::DisplayRole).toString();
401 const QIcon icon = index.model()->data(index, role: Qt::DecorationRole).value<QIcon>();
402 const QPixmap pixmap = icon.pixmap(w: iconSize, h: iconSize, mode: iconMode);
403
404 QFontMetrics fm = painter->fontMetrics();
405 int wp = pixmap.width() / pixmap.devicePixelRatio();
406 int hp = pixmap.height() / pixmap.devicePixelRatio();
407
408 QTextLayout iconTextLayout(text, option.font);
409 QTextOption textOption(Qt::AlignHCenter);
410 iconTextLayout.setTextOption(textOption);
411 int maxWidth = qMax(a: 3 * wp, b: 8 * fm.height());
412 layoutText(layout: &iconTextLayout, maxWidth);
413
414 QPen pen = painter->pen();
415 QPalette::ColorGroup cg = option.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled;
416 if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) {
417 cg = QPalette::Inactive;
418 }
419
420 style->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &opt, p: painter, w: opt.widget);
421 if (option.state & QStyle::State_Selected) {
422 painter->setPen(option.palette.color(cg, cr: QPalette::HighlightedText));
423 } else {
424 painter->setPen(option.palette.color(cg, cr: QPalette::Text));
425 }
426
427 painter->drawPixmap(x: option.rect.x() + (option.rect.width() / 2) - (wp / 2), y: option.rect.y() + 5, pm: pixmap);
428 if (!text.isEmpty()) {
429 iconTextLayout.draw(p: painter, pos: QPoint(option.rect.x() + (option.rect.width() / 2) - (maxWidth / 2), option.rect.y() + hp + 7));
430 }
431
432 painter->setPen(pen);
433
434 drawFocus(painter, option, option.rect);
435}
436
437QSize KPageListViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
438{
439 if (!index.isValid()) {
440 return QSize(0, 0);
441 }
442
443 QStyleOptionViewItem opt(option);
444 opt.showDecorationSelected = true;
445 QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
446
447 int iconSize = style->pixelMetric(metric: QStyle::PM_IconViewIconSize);
448 const QString text = index.model()->data(index, role: Qt::DisplayRole).toString();
449 const QIcon icon = index.model()->data(index, role: Qt::DecorationRole).value<QIcon>();
450 const QPixmap pixmap = icon.pixmap(w: iconSize, h: iconSize);
451
452 QFontMetrics fm = option.fontMetrics;
453 int gap = fm.height();
454 int wp = pixmap.width() / pixmap.devicePixelRatio();
455 int hp = pixmap.height() / pixmap.devicePixelRatio();
456
457 if (hp == 0) {
458 // No pixmap loaded yet, we'll use the default icon size in this case.
459 hp = iconSize;
460 wp = iconSize;
461 }
462
463 QTextLayout iconTextLayout(text, option.font);
464 int wt = layoutText(layout: &iconTextLayout, maxWidth: qMax(a: 3 * wp, b: 8 * fm.height()));
465 int ht = iconTextLayout.boundingRect().height();
466
467 int width;
468 int height;
469 if (text.isEmpty()) {
470 height = hp;
471 } else {
472 height = hp + ht + 10;
473 }
474
475 width = qMax(a: wt, b: wp) + gap;
476
477 return QSize(width, height);
478}
479
480void KPageListViewDelegate::drawFocus(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) const
481{
482 if (option.state & QStyle::State_HasFocus) {
483 QStyleOptionFocusRect o;
484 o.QStyleOption::operator=(other: option);
485 o.rect = rect;
486 o.state |= QStyle::State_KeyboardFocusChange;
487 QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled;
488 o.backgroundColor = option.palette.color(cg, cr: (option.state & QStyle::State_Selected) ? QPalette::Highlight : QPalette::Window);
489
490 QStyle *style = option.widget ? option.widget->style() : QApplication::style();
491 style->drawPrimitive(pe: QStyle::PE_FrameFocusRect, opt: &o, p: painter, w: option.widget);
492 }
493}
494
495// KPageListViewProxy
496
497KPageListViewProxy::KPageListViewProxy(QObject *parent)
498 : QAbstractProxyModel(parent)
499{
500}
501
502KPageListViewProxy::~KPageListViewProxy()
503{
504}
505
506int KPageListViewProxy::rowCount(const QModelIndex &) const
507{
508 return mList.count();
509}
510
511int KPageListViewProxy::columnCount(const QModelIndex &) const
512{
513 return 1;
514}
515
516QModelIndex KPageListViewProxy::index(int row, int column, const QModelIndex &) const
517{
518 if (column > 1 || row >= mList.count()) {
519 return QModelIndex();
520 } else {
521 return createIndex(arow: row, acolumn: column, adata: mList[row].internalPointer());
522 }
523}
524
525QModelIndex KPageListViewProxy::parent(const QModelIndex &) const
526{
527 return QModelIndex();
528}
529
530QVariant KPageListViewProxy::data(const QModelIndex &index, int role) const
531{
532 if (!index.isValid()) {
533 return QVariant();
534 }
535
536 if (index.row() >= mList.count()) {
537 return QVariant();
538 }
539
540 return sourceModel()->data(index: mList[index.row()], role);
541}
542
543QModelIndex KPageListViewProxy::mapFromSource(const QModelIndex &index) const
544{
545 if (!index.isValid()) {
546 return QModelIndex();
547 }
548
549 for (int i = 0; i < mList.count(); ++i) {
550 if (mList[i] == index) {
551 return createIndex(arow: i, acolumn: 0, adata: index.internalPointer());
552 }
553 }
554
555 return QModelIndex();
556}
557
558QModelIndex KPageListViewProxy::mapToSource(const QModelIndex &index) const
559{
560 if (!index.isValid()) {
561 return QModelIndex();
562 }
563
564 return mList[index.row()];
565}
566
567void KPageListViewProxy::rebuildMap()
568{
569 mList.clear();
570
571 const QAbstractItemModel *model = sourceModel();
572 if (!model) {
573 return;
574 }
575
576 for (int i = 0; i < model->rowCount(); ++i) {
577 addMapEntry(model->index(row: i, column: 0));
578 }
579
580 for (int i = 0; i < mList.count(); ++i) {
581 qCDebug(KWidgetsAddonsLog, "%d:0 -> %d:%d", i, mList[i].row(), mList[i].column());
582 }
583
584 Q_EMIT layoutChanged();
585}
586
587void KPageListViewProxy::addMapEntry(const QModelIndex &index)
588{
589 if (sourceModel()->rowCount(parent: index) == 0) {
590 mList.append(t: index);
591 } else {
592 const int count = sourceModel()->rowCount(parent: index);
593 for (int i = 0; i < count; ++i) {
594 addMapEntry(index: sourceModel()->index(row: i, column: 0, parent: index));
595 }
596 }
597}
598
599SelectionModel::SelectionModel(QAbstractItemModel *model, QObject *parent)
600 : QItemSelectionModel(model, parent)
601{
602}
603
604void SelectionModel::clear()
605{
606 // Don't allow the current selection to be cleared
607}
608
609void SelectionModel::select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command)
610{
611 // Don't allow the current selection to be cleared
612 if (!index.isValid() && (command & QItemSelectionModel::Clear)) {
613 return;
614 }
615 QItemSelectionModel::select(index, command);
616}
617
618void SelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command)
619{
620 // Don't allow the current selection to be cleared
621 if (!selection.count() && (command & QItemSelectionModel::Clear)) {
622 return;
623 }
624 QItemSelectionModel::select(selection, command);
625}
626
627#include "moc_kpageview_p.cpp"
628

source code of kwidgetsaddons/src/kpageview_p.cpp