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

source code of kwidgetsaddons/src/kpageview_p.cpp