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 mTabWidget->setDocumentMode(true);
209 connect(sender: mTabWidget, signal: &QTabWidget::currentChanged, context: this, slot: &KPageTabbedView::currentPageChanged);
210
211 layout->addWidget(mTabWidget);
212}
213
214KPageTabbedView::~KPageTabbedView()
215{
216 if (model()) {
217 for (int i = 0; i < mTabWidget->count(); ++i) {
218 QWidget *page = qvariant_cast<QWidget *>(v: model()->data(index: model()->index(row: i, column: 0), role: KPageModel::WidgetRole));
219
220 if (page) {
221 page->setVisible(false);
222 page->setParent(nullptr); // reparent our children before they are deleted
223 }
224 }
225 }
226}
227
228void KPageTabbedView::setModel(QAbstractItemModel *model)
229{
230 QAbstractItemView::setModel(model);
231
232 connect(sender: model, signal: &QAbstractItemModel::layoutChanged, context: this, slot: &KPageTabbedView::layoutChanged);
233
234 layoutChanged();
235}
236
237QModelIndex KPageTabbedView::indexAt(const QPoint &) const
238{
239 if (model()) {
240 return model()->index(row: 0, column: 0);
241 } else {
242 return QModelIndex();
243 }
244}
245
246void KPageTabbedView::scrollTo(const QModelIndex &index, ScrollHint)
247{
248 if (!index.isValid()) {
249 return;
250 }
251
252 mTabWidget->setCurrentIndex(index.row());
253}
254
255QRect KPageTabbedView::visualRect(const QModelIndex &) const
256{
257 return QRect();
258}
259
260QSize KPageTabbedView::minimumSizeHint() const
261{
262 return mTabWidget->minimumSizeHint();
263}
264
265QModelIndex KPageTabbedView::moveCursor(QAbstractItemView::CursorAction, Qt::KeyboardModifiers)
266{
267 return QModelIndex();
268}
269
270int KPageTabbedView::horizontalOffset() const
271{
272 return 0;
273}
274
275int KPageTabbedView::verticalOffset() const
276{
277 return 0;
278}
279
280bool KPageTabbedView::isIndexHidden(const QModelIndex &index) const
281{
282 return (mTabWidget->currentIndex() != index.row());
283}
284
285void KPageTabbedView::setSelection(const QRect &, QFlags<QItemSelectionModel::SelectionFlag>)
286{
287}
288
289QRegion KPageTabbedView::visualRegionForSelection(const QItemSelection &) const
290{
291 return QRegion();
292}
293
294void KPageTabbedView::currentPageChanged(int index)
295{
296 if (!model()) {
297 return;
298 }
299
300 QModelIndex modelIndex = model()->index(row: index, column: 0);
301
302 selectionModel()->setCurrentIndex(index: modelIndex, command: QItemSelectionModel::ClearAndSelect);
303}
304
305void KPageTabbedView::layoutChanged()
306{
307 // save old position
308 int pos = mTabWidget->currentIndex();
309
310 // clear tab bar
311 int count = mTabWidget->count();
312 for (int i = 0; i < count; ++i) {
313 mTabWidget->removeTab(index: 0);
314 }
315
316 if (!model()) {
317 return;
318 }
319
320 // add new tabs
321 for (int i = 0; i < model()->rowCount(); ++i) {
322 const QString title = model()->data(index: model()->index(row: i, column: 0)).toString();
323 const QIcon icon = model()->data(index: model()->index(row: i, column: 0), role: Qt::DecorationRole).value<QIcon>();
324 QWidget *page = qvariant_cast<QWidget *>(v: model()->data(index: model()->index(row: i, column: 0), role: KPageModel::WidgetRole));
325 if (page) {
326 QWidget *widget = new QWidget(this);
327 QVBoxLayout *layout = new QVBoxLayout(widget);
328 layout->setContentsMargins({});
329 layout->addWidget(page);
330 page->setVisible(true);
331 mTabWidget->addTab(widget, icon, label: title);
332 }
333 }
334
335 mTabWidget->setCurrentIndex(pos);
336}
337
338void KPageTabbedView::dataChanged(const QModelIndex &index, const QModelIndex &, const QList<int> &roles)
339{
340 if (!index.isValid()) {
341 return;
342 }
343
344 if (index.row() < 0 || index.row() >= mTabWidget->count()) {
345 return;
346 }
347
348 if (roles.isEmpty() || roles.contains(t: Qt::DisplayRole) || roles.contains(t: Qt::DecorationRole)) {
349 const QString title = model()->data(index).toString();
350 const QIcon icon = model()->data(index, role: Qt::DecorationRole).value<QIcon>();
351
352 mTabWidget->setTabText(index: index.row(), text: title);
353 mTabWidget->setTabIcon(index: index.row(), icon);
354 }
355}
356
357// KPageListViewDelegate
358
359KPageListViewDelegate::KPageListViewDelegate(QObject *parent)
360 : QAbstractItemDelegate(parent)
361{
362}
363
364static int layoutText(QTextLayout *layout, int maxWidth)
365{
366 qreal height = 0;
367 int textWidth = 0;
368 layout->beginLayout();
369 while (true) {
370 QTextLine line = layout->createLine();
371 if (!line.isValid()) {
372 break;
373 }
374 line.setLineWidth(maxWidth);
375 line.setPosition(QPointF(0, height));
376 height += line.height();
377 textWidth = qMax(a: textWidth, b: qRound(d: line.naturalTextWidth() + 0.5));
378 }
379 layout->endLayout();
380 return textWidth;
381}
382
383void KPageListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
384{
385 if (!index.isValid()) {
386 return;
387 }
388
389 QStyleOptionViewItem opt(option);
390 opt.showDecorationSelected = true;
391 QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
392
393 const QIcon::Mode iconMode = (option.state & QStyle::State_Selected) && (option.state & QStyle::State_Active) ? QIcon::Selected : QIcon::Normal;
394 int iconSize = style->pixelMetric(metric: QStyle::PM_IconViewIconSize);
395 const QString text = index.model()->data(index, role: Qt::DisplayRole).toString();
396 const QIcon icon = index.model()->data(index, role: Qt::DecorationRole).value<QIcon>();
397 const QPixmap pixmap = icon.pixmap(w: iconSize, h: iconSize, mode: iconMode);
398
399 QFontMetrics fm = painter->fontMetrics();
400 int wp = pixmap.width() / pixmap.devicePixelRatio();
401 int hp = pixmap.height() / pixmap.devicePixelRatio();
402
403 QTextLayout iconTextLayout(text, option.font);
404 QTextOption textOption(Qt::AlignHCenter);
405 iconTextLayout.setTextOption(textOption);
406 int maxWidth = qMax(a: 3 * wp, b: 8 * fm.height());
407 layoutText(layout: &iconTextLayout, maxWidth);
408
409 QPen pen = painter->pen();
410 QPalette::ColorGroup cg = option.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled;
411 if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) {
412 cg = QPalette::Inactive;
413 }
414
415 style->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &opt, p: painter, w: opt.widget);
416 if (option.state & QStyle::State_Selected) {
417 painter->setPen(option.palette.color(cg, cr: QPalette::HighlightedText));
418 } else {
419 painter->setPen(option.palette.color(cg, cr: QPalette::Text));
420 }
421
422 painter->drawPixmap(x: option.rect.x() + (option.rect.width() / 2) - (wp / 2), y: option.rect.y() + 5, pm: pixmap);
423 if (!text.isEmpty()) {
424 iconTextLayout.draw(p: painter, pos: QPoint(option.rect.x() + (option.rect.width() / 2) - (maxWidth / 2), option.rect.y() + hp + 7));
425 }
426
427 painter->setPen(pen);
428
429 drawFocus(painter, option, option.rect);
430}
431
432QSize KPageListViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
433{
434 if (!index.isValid()) {
435 return QSize(0, 0);
436 }
437
438 QStyleOptionViewItem opt(option);
439 opt.showDecorationSelected = true;
440 QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
441
442 int iconSize = style->pixelMetric(metric: QStyle::PM_IconViewIconSize);
443 const QString text = index.model()->data(index, role: Qt::DisplayRole).toString();
444 const QIcon icon = index.model()->data(index, role: Qt::DecorationRole).value<QIcon>();
445 const QPixmap pixmap = icon.pixmap(w: iconSize, h: iconSize);
446
447 QFontMetrics fm = option.fontMetrics;
448 int gap = fm.height();
449 int wp = pixmap.width() / pixmap.devicePixelRatio();
450 int hp = pixmap.height() / pixmap.devicePixelRatio();
451
452 if (hp == 0) {
453 // No pixmap loaded yet, we'll use the default icon size in this case.
454 hp = iconSize;
455 wp = iconSize;
456 }
457
458 QTextLayout iconTextLayout(text, option.font);
459 int wt = layoutText(layout: &iconTextLayout, maxWidth: qMax(a: 3 * wp, b: 8 * fm.height()));
460 int ht = iconTextLayout.boundingRect().height();
461
462 int width;
463 int height;
464 if (text.isEmpty()) {
465 height = hp;
466 } else {
467 height = hp + ht + 10;
468 }
469
470 width = qMax(a: wt, b: wp) + gap;
471
472 return QSize(width, height);
473}
474
475void KPageListViewDelegate::drawFocus(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) const
476{
477 if (option.state & QStyle::State_HasFocus) {
478 QStyleOptionFocusRect o;
479 o.QStyleOption::operator=(other: option);
480 o.rect = rect;
481 o.state |= QStyle::State_KeyboardFocusChange;
482 QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled;
483 o.backgroundColor = option.palette.color(cg, cr: (option.state & QStyle::State_Selected) ? QPalette::Highlight : QPalette::Window);
484
485 QStyle *style = option.widget ? option.widget->style() : QApplication::style();
486 style->drawPrimitive(pe: QStyle::PE_FrameFocusRect, opt: &o, p: painter, w: option.widget);
487 }
488}
489
490// KPageListViewProxy
491
492KPageListViewProxy::KPageListViewProxy(QObject *parent)
493 : QAbstractProxyModel(parent)
494{
495}
496
497KPageListViewProxy::~KPageListViewProxy()
498{
499}
500
501int KPageListViewProxy::rowCount(const QModelIndex &) const
502{
503 return mList.count();
504}
505
506int KPageListViewProxy::columnCount(const QModelIndex &) const
507{
508 return 1;
509}
510
511QModelIndex KPageListViewProxy::index(int row, int column, const QModelIndex &) const
512{
513 if (column > 1 || row >= mList.count()) {
514 return QModelIndex();
515 } else {
516 return createIndex(arow: row, acolumn: column, adata: mList[row].internalPointer());
517 }
518}
519
520QModelIndex KPageListViewProxy::parent(const QModelIndex &) const
521{
522 return QModelIndex();
523}
524
525QVariant KPageListViewProxy::data(const QModelIndex &index, int role) const
526{
527 if (!index.isValid()) {
528 return QVariant();
529 }
530
531 if (index.row() >= mList.count()) {
532 return QVariant();
533 }
534
535 return sourceModel()->data(index: mList[index.row()], role);
536}
537
538QModelIndex KPageListViewProxy::mapFromSource(const QModelIndex &index) const
539{
540 if (!index.isValid()) {
541 return QModelIndex();
542 }
543
544 for (int i = 0; i < mList.count(); ++i) {
545 if (mList[i] == index) {
546 return createIndex(arow: i, acolumn: 0, adata: index.internalPointer());
547 }
548 }
549
550 return QModelIndex();
551}
552
553QModelIndex KPageListViewProxy::mapToSource(const QModelIndex &index) const
554{
555 if (!index.isValid()) {
556 return QModelIndex();
557 }
558
559 return mList[index.row()];
560}
561
562void KPageListViewProxy::rebuildMap()
563{
564 mList.clear();
565
566 const QAbstractItemModel *model = sourceModel();
567 if (!model) {
568 return;
569 }
570
571 for (int i = 0; i < model->rowCount(); ++i) {
572 addMapEntry(model->index(row: i, column: 0));
573 }
574
575 for (int i = 0; i < mList.count(); ++i) {
576 qCDebug(KWidgetsAddonsLog, "%d:0 -> %d:%d", i, mList[i].row(), mList[i].column());
577 }
578
579 Q_EMIT layoutChanged();
580}
581
582void KPageListViewProxy::addMapEntry(const QModelIndex &index)
583{
584 if (sourceModel()->rowCount(parent: index) == 0) {
585 mList.append(t: index);
586 } else {
587 const int count = sourceModel()->rowCount(parent: index);
588 for (int i = 0; i < count; ++i) {
589 addMapEntry(index: sourceModel()->index(row: i, column: 0, parent: index));
590 }
591 }
592}
593
594SelectionModel::SelectionModel(QAbstractItemModel *model, QObject *parent)
595 : QItemSelectionModel(model, parent)
596{
597}
598
599void SelectionModel::clear()
600{
601 // Don't allow the current selection to be cleared
602}
603
604void SelectionModel::select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command)
605{
606 // Don't allow the current selection to be cleared
607 if (!index.isValid() && (command & QItemSelectionModel::Clear)) {
608 return;
609 }
610 QItemSelectionModel::select(index, command);
611}
612
613void SelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command)
614{
615 // Don't allow the current selection to be cleared
616 if (!selection.count() && (command & QItemSelectionModel::Clear)) {
617 return;
618 }
619 QItemSelectionModel::select(selection, command);
620}
621
622#include "moc_kpageview_p.cpp"
623

source code of kwidgetsaddons/src/kpageview_p.cpp