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 | mTabWidget->setDocumentMode(true); |
209 | connect(sender: mTabWidget, signal: &QTabWidget::currentChanged, context: this, slot: &KPageTabbedView::currentPageChanged); |
210 | |
211 | layout->addWidget(mTabWidget); |
212 | } |
213 | |
214 | KPageTabbedView::~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 | |
228 | void 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 | |
237 | QModelIndex 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 | |
246 | void KPageTabbedView::scrollTo(const QModelIndex &index, ScrollHint) |
247 | { |
248 | if (!index.isValid()) { |
249 | return; |
250 | } |
251 | |
252 | mTabWidget->setCurrentIndex(index.row()); |
253 | } |
254 | |
255 | QRect KPageTabbedView::visualRect(const QModelIndex &) const |
256 | { |
257 | return QRect(); |
258 | } |
259 | |
260 | QSize KPageTabbedView::minimumSizeHint() const |
261 | { |
262 | return mTabWidget->minimumSizeHint(); |
263 | } |
264 | |
265 | QModelIndex KPageTabbedView::moveCursor(QAbstractItemView::CursorAction, Qt::KeyboardModifiers) |
266 | { |
267 | return QModelIndex(); |
268 | } |
269 | |
270 | int KPageTabbedView::horizontalOffset() const |
271 | { |
272 | return 0; |
273 | } |
274 | |
275 | int KPageTabbedView::verticalOffset() const |
276 | { |
277 | return 0; |
278 | } |
279 | |
280 | bool KPageTabbedView::isIndexHidden(const QModelIndex &index) const |
281 | { |
282 | return (mTabWidget->currentIndex() != index.row()); |
283 | } |
284 | |
285 | void KPageTabbedView::setSelection(const QRect &, QFlags<QItemSelectionModel::SelectionFlag>) |
286 | { |
287 | } |
288 | |
289 | QRegion KPageTabbedView::visualRegionForSelection(const QItemSelection &) const |
290 | { |
291 | return QRegion(); |
292 | } |
293 | |
294 | void 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 | |
305 | void 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 | |
338 | void 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 | |
359 | KPageListViewDelegate::KPageListViewDelegate(QObject *parent) |
360 | : QAbstractItemDelegate(parent) |
361 | { |
362 | } |
363 | |
364 | static 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 | |
383 | void 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 | |
432 | QSize 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 | |
475 | void 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 | |
492 | KPageListViewProxy::KPageListViewProxy(QObject *parent) |
493 | : QAbstractProxyModel(parent) |
494 | { |
495 | } |
496 | |
497 | KPageListViewProxy::~KPageListViewProxy() |
498 | { |
499 | } |
500 | |
501 | int KPageListViewProxy::rowCount(const QModelIndex &) const |
502 | { |
503 | return mList.count(); |
504 | } |
505 | |
506 | int KPageListViewProxy::columnCount(const QModelIndex &) const |
507 | { |
508 | return 1; |
509 | } |
510 | |
511 | QModelIndex 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 | |
520 | QModelIndex KPageListViewProxy::parent(const QModelIndex &) const |
521 | { |
522 | return QModelIndex(); |
523 | } |
524 | |
525 | QVariant 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 | |
538 | QModelIndex 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 | |
553 | QModelIndex KPageListViewProxy::mapToSource(const QModelIndex &index) const |
554 | { |
555 | if (!index.isValid()) { |
556 | return QModelIndex(); |
557 | } |
558 | |
559 | return mList[index.row()]; |
560 | } |
561 | |
562 | void 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 | |
582 | void 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 | |
594 | SelectionModel::SelectionModel(QAbstractItemModel *model, QObject *parent) |
595 | : QItemSelectionModel(model, parent) |
596 | { |
597 | } |
598 | |
599 | void SelectionModel::clear() |
600 | { |
601 | // Don't allow the current selection to be cleared |
602 | } |
603 | |
604 | void 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 | |
613 | void 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 | |