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 | |
21 | constexpr const auto viewWidth = 300; |
22 | |
23 | using namespace KDEPrivate; |
24 | |
25 | // KPagePlainView |
26 | |
27 | KPagePlainView::KPagePlainView(QWidget *parent) |
28 | : QAbstractItemView(parent) |
29 | { |
30 | hide(); |
31 | } |
32 | |
33 | QModelIndex KPagePlainView::indexAt(const QPoint &) const |
34 | { |
35 | return QModelIndex(); |
36 | } |
37 | |
38 | void KPagePlainView::scrollTo(const QModelIndex &, ScrollHint) |
39 | { |
40 | } |
41 | |
42 | QRect KPagePlainView::visualRect(const QModelIndex &) const |
43 | { |
44 | return QRect(); |
45 | } |
46 | |
47 | QModelIndex KPagePlainView::moveCursor(QAbstractItemView::CursorAction, Qt::KeyboardModifiers) |
48 | { |
49 | return QModelIndex(); |
50 | } |
51 | |
52 | int KPagePlainView::horizontalOffset() const |
53 | { |
54 | return 0; |
55 | } |
56 | |
57 | int KPagePlainView::verticalOffset() const |
58 | { |
59 | return 0; |
60 | } |
61 | |
62 | bool KPagePlainView::isIndexHidden(const QModelIndex &) const |
63 | { |
64 | return false; |
65 | } |
66 | |
67 | void KPagePlainView::setSelection(const QRect &, QFlags<QItemSelectionModel::SelectionFlag>) |
68 | { |
69 | } |
70 | |
71 | QRegion KPagePlainView::visualRegionForSelection(const QItemSelection &) const |
72 | { |
73 | return QRegion(); |
74 | } |
75 | |
76 | // KPageListView |
77 | |
78 | KPageListView::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 | |
95 | KPageListView::~KPageListView() |
96 | { |
97 | } |
98 | |
99 | void 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 | |
111 | void KPageListView::changeEvent(QEvent *event) |
112 | { |
113 | QListView::changeEvent(event); |
114 | |
115 | if (event->type() == QEvent::FontChange) { |
116 | updateWidth(); |
117 | } |
118 | } |
119 | |
120 | void KPageListView::setFlexibleWidth(bool flexibleWidth) |
121 | { |
122 | m_flexibleWidth = flexibleWidth; |
123 | } |
124 | |
125 | void 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 | |
139 | KPageTreeView::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 | |
150 | void 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 | |
162 | void 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 | |
181 | void 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 | |
193 | KPageTabbedView::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 | |
216 | KPageTabbedView::~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 | |
230 | void 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 | |
239 | QModelIndex 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 | |
248 | void KPageTabbedView::scrollTo(const QModelIndex &index, ScrollHint) |
249 | { |
250 | if (!index.isValid()) { |
251 | return; |
252 | } |
253 | |
254 | mTabWidget->setCurrentIndex(index.row()); |
255 | } |
256 | |
257 | QRect KPageTabbedView::visualRect(const QModelIndex &) const |
258 | { |
259 | return QRect(); |
260 | } |
261 | |
262 | QSize KPageTabbedView::minimumSizeHint() const |
263 | { |
264 | return mTabWidget->minimumSizeHint(); |
265 | } |
266 | |
267 | QModelIndex KPageTabbedView::moveCursor(QAbstractItemView::CursorAction, Qt::KeyboardModifiers) |
268 | { |
269 | return QModelIndex(); |
270 | } |
271 | |
272 | int KPageTabbedView::horizontalOffset() const |
273 | { |
274 | return 0; |
275 | } |
276 | |
277 | int KPageTabbedView::verticalOffset() const |
278 | { |
279 | return 0; |
280 | } |
281 | |
282 | bool KPageTabbedView::isIndexHidden(const QModelIndex &index) const |
283 | { |
284 | return (mTabWidget->currentIndex() != index.row()); |
285 | } |
286 | |
287 | void KPageTabbedView::setSelection(const QRect &, QFlags<QItemSelectionModel::SelectionFlag>) |
288 | { |
289 | } |
290 | |
291 | QRegion KPageTabbedView::visualRegionForSelection(const QItemSelection &) const |
292 | { |
293 | return QRegion(); |
294 | } |
295 | |
296 | void 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 | |
307 | void 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 | |
340 | void 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 | |
361 | KPageListViewDelegate::KPageListViewDelegate(QObject *parent) |
362 | : QAbstractItemDelegate(parent) |
363 | { |
364 | } |
365 | |
366 | static 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 | |
385 | void 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 | |
434 | QSize 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 | |
477 | void 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 | |
494 | KPageListViewProxy::KPageListViewProxy(QObject *parent) |
495 | : QAbstractProxyModel(parent) |
496 | { |
497 | } |
498 | |
499 | KPageListViewProxy::~KPageListViewProxy() |
500 | { |
501 | } |
502 | |
503 | int KPageListViewProxy::rowCount(const QModelIndex &) const |
504 | { |
505 | return mList.count(); |
506 | } |
507 | |
508 | int KPageListViewProxy::columnCount(const QModelIndex &) const |
509 | { |
510 | return 1; |
511 | } |
512 | |
513 | QModelIndex 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 | |
522 | QModelIndex KPageListViewProxy::parent(const QModelIndex &) const |
523 | { |
524 | return QModelIndex(); |
525 | } |
526 | |
527 | QVariant 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 | |
540 | QModelIndex 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 | |
555 | QModelIndex KPageListViewProxy::mapToSource(const QModelIndex &index) const |
556 | { |
557 | if (!index.isValid()) { |
558 | return QModelIndex(); |
559 | } |
560 | |
561 | return mList[index.row()]; |
562 | } |
563 | |
564 | void 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 | |
584 | void 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 | |
596 | SelectionModel::SelectionModel(QAbstractItemModel *model, QObject *parent) |
597 | : QItemSelectionModel(model, parent) |
598 | { |
599 | } |
600 | |
601 | void SelectionModel::clear() |
602 | { |
603 | // Don't allow the current selection to be cleared |
604 | } |
605 | |
606 | void 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 | |
615 | void 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 | |