1 /*
2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
3 *
4 * Based on the Itemviews NG project from Trolltech Labs
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 #include "kitemlistcontainer.h"
10
11 #include "kitemlistcontroller.h"
12 #include "kitemlistview.h"
13 #include "private/kitemlistsmoothscroller.h"
14
15 #include <QApplication>
16 #include <QFontMetrics>
17 #include <QGraphicsScene>
18 #include <QGraphicsView>
19 #include <QScrollBar>
20 #include <QScroller>
21 #include <QStyleOption>
22
23 /**
24 * Replaces the default viewport of KItemListContainer by a
25 * non-scrollable viewport. The scrolling is done in an optimized
26 * way by KItemListView internally.
27 */
28 class KItemListContainerViewport : public QGraphicsView
29 {
30 Q_OBJECT
31
32 public:
33 KItemListContainerViewport(QGraphicsScene* scene, QWidget* parent);
34 protected:
35 void wheelEvent(QWheelEvent* event) override;
36 };
37
KItemListContainerViewport(QGraphicsScene * scene,QWidget * parent)38 KItemListContainerViewport::KItemListContainerViewport(QGraphicsScene* scene, QWidget* parent) :
39 QGraphicsView(scene, parent)
40 {
41 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
42 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
43 setViewportMargins(0, 0, 0, 0);
44 setFrameShape(QFrame::NoFrame);
45 }
46
wheelEvent(QWheelEvent * event)47 void KItemListContainerViewport::wheelEvent(QWheelEvent* event)
48 {
49 // Assure that the wheel-event gets forwarded to the parent
50 // and not handled at all by QGraphicsView.
51 event->ignore();
52 }
53
KItemListContainer(KItemListController * controller,QWidget * parent)54 KItemListContainer::KItemListContainer(KItemListController* controller, QWidget* parent) :
55 QAbstractScrollArea(parent),
56 m_controller(controller),
57 m_horizontalSmoothScroller(nullptr),
58 m_verticalSmoothScroller(nullptr),
59 m_scroller(nullptr)
60 {
61 Q_ASSERT(controller);
62 controller->setParent(this);
63
64 QGraphicsView* graphicsView = new KItemListContainerViewport(new QGraphicsScene(this), this);
65 setViewport(graphicsView);
66
67 m_horizontalSmoothScroller = new KItemListSmoothScroller(horizontalScrollBar(), this);
68 m_verticalSmoothScroller = new KItemListSmoothScroller(verticalScrollBar(), this);
69
70 if (controller->model()) {
71 slotModelChanged(controller->model(), nullptr);
72 }
73 if (controller->view()) {
74 slotViewChanged(controller->view(), nullptr);
75 }
76
77 connect(controller, &KItemListController::modelChanged,
78 this, &KItemListContainer::slotModelChanged);
79 connect(controller, &KItemListController::viewChanged,
80 this, &KItemListContainer::slotViewChanged);
81
82 m_scroller = QScroller::scroller(viewport());
83 m_scroller->grabGesture(viewport());
84 connect(controller, &KItemListController::scrollerStop,
85 this, &KItemListContainer::stopScroller);
86 connect(m_scroller, &QScroller::stateChanged,
87 controller, &KItemListController::slotStateChanged);
88 }
89
~KItemListContainer()90 KItemListContainer::~KItemListContainer()
91 {
92 // Don't rely on the QObject-order to delete the controller, otherwise
93 // the QGraphicsScene might get deleted before the view.
94 delete m_controller;
95 m_controller = nullptr;
96 }
97
controller() const98 KItemListController* KItemListContainer::controller() const
99 {
100 return m_controller;
101 }
102
setEnabledFrame(bool enable)103 void KItemListContainer::setEnabledFrame(bool enable)
104 {
105 QGraphicsView* graphicsView = qobject_cast<QGraphicsView*>(viewport());
106 if (enable) {
107 setFrameShape(QFrame::StyledPanel);
108 graphicsView->setPalette(palette());
109 graphicsView->viewport()->setAutoFillBackground(true);
110 } else {
111 setFrameShape(QFrame::NoFrame);
112 // Make the background of the container transparent and apply the window-text color
113 // to the text color, so that enough contrast is given for all color
114 // schemes
115 QPalette p = graphicsView->palette();
116 p.setColor(QPalette::Active, QPalette::Text, p.color(QPalette::Active, QPalette::WindowText));
117 p.setColor(QPalette::Inactive, QPalette::Text, p.color(QPalette::Inactive, QPalette::WindowText));
118 p.setColor(QPalette::Disabled, QPalette::Text, p.color(QPalette::Disabled, QPalette::WindowText));
119 graphicsView->setPalette(p);
120 graphicsView->viewport()->setAutoFillBackground(false);
121 }
122 }
123
enabledFrame() const124 bool KItemListContainer::enabledFrame() const
125 {
126 const QGraphicsView* graphicsView = qobject_cast<QGraphicsView*>(viewport());
127 return graphicsView->autoFillBackground();
128 }
129
keyPressEvent(QKeyEvent * event)130 void KItemListContainer::keyPressEvent(QKeyEvent* event)
131 {
132 // TODO: We should find a better way to handle the key press events in the view.
133 // The reasons why we need this hack are:
134 // 1. Without reimplementing keyPressEvent() here, the event would not reach the QGraphicsView.
135 // 2. By default, the KItemListView does not have the keyboard focus in the QGraphicsScene, so
136 // simply sending the event to the QGraphicsView which is the KItemListContainer's viewport
137 // does not work.
138 KItemListView* view = m_controller->view();
139 if (view) {
140 QApplication::sendEvent(view, event);
141 }
142 }
143
showEvent(QShowEvent * event)144 void KItemListContainer::showEvent(QShowEvent* event)
145 {
146 QAbstractScrollArea::showEvent(event);
147 updateGeometries();
148 }
149
resizeEvent(QResizeEvent * event)150 void KItemListContainer::resizeEvent(QResizeEvent* event)
151 {
152 QAbstractScrollArea::resizeEvent(event);
153 updateGeometries();
154 }
155
scrollContentsBy(int dx,int dy)156 void KItemListContainer::scrollContentsBy(int dx, int dy)
157 {
158 m_horizontalSmoothScroller->scrollContentsBy(dx);
159 m_verticalSmoothScroller->scrollContentsBy(dy);
160 }
161
wheelEvent(QWheelEvent * event)162 void KItemListContainer::wheelEvent(QWheelEvent* event)
163 {
164 if (event->modifiers().testFlag(Qt::ControlModifier)) {
165 event->ignore();
166 return;
167 }
168
169 KItemListView* view = m_controller->view();
170 if (!view) {
171 event->ignore();
172 return;
173 }
174
175 const bool scrollHorizontally = (qAbs(event->angleDelta().y()) < qAbs(event->angleDelta().x())) ||
176 (!verticalScrollBar()->isVisible());
177 KItemListSmoothScroller* smoothScroller = scrollHorizontally ?
178 m_horizontalSmoothScroller : m_verticalSmoothScroller;
179
180 smoothScroller->handleWheelEvent(event);
181 }
182
slotScrollOrientationChanged(Qt::Orientation current,Qt::Orientation previous)183 void KItemListContainer::slotScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous)
184 {
185 Q_UNUSED(previous)
186 updateSmoothScrollers(current);
187 }
188
slotModelChanged(KItemModelBase * current,KItemModelBase * previous)189 void KItemListContainer::slotModelChanged(KItemModelBase* current, KItemModelBase* previous)
190 {
191 Q_UNUSED(current)
192 Q_UNUSED(previous)
193 }
194
slotViewChanged(KItemListView * current,KItemListView * previous)195 void KItemListContainer::slotViewChanged(KItemListView* current, KItemListView* previous)
196 {
197 QGraphicsScene* scene = static_cast<QGraphicsView*>(viewport())->scene();
198 if (previous) {
199 scene->removeItem(previous);
200 disconnect(previous, &KItemListView::scrollOrientationChanged,
201 this, &KItemListContainer::slotScrollOrientationChanged);
202 disconnect(previous, &KItemListView::scrollOffsetChanged,
203 this, &KItemListContainer::updateScrollOffsetScrollBar);
204 disconnect(previous, &KItemListView::maximumScrollOffsetChanged,
205 this, &KItemListContainer::updateScrollOffsetScrollBar);
206 disconnect(previous, &KItemListView::itemOffsetChanged,
207 this, &KItemListContainer::updateItemOffsetScrollBar);
208 disconnect(previous, &KItemListView::maximumItemOffsetChanged,
209 this, &KItemListContainer::updateItemOffsetScrollBar);
210 disconnect(previous, &KItemListView::scrollTo, this, &KItemListContainer::scrollTo);
211 disconnect(m_horizontalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, previous, &KItemListView::scrollingStopped);
212 disconnect(m_verticalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, previous, &KItemListView::scrollingStopped);
213 m_horizontalSmoothScroller->setTargetObject(nullptr);
214 m_verticalSmoothScroller->setTargetObject(nullptr);
215 }
216 if (current) {
217 scene->addItem(current);
218 connect(current, &KItemListView::scrollOrientationChanged,
219 this, &KItemListContainer::slotScrollOrientationChanged);
220 connect(current, &KItemListView::scrollOffsetChanged,
221 this, &KItemListContainer::updateScrollOffsetScrollBar);
222 connect(current, &KItemListView::maximumScrollOffsetChanged,
223 this, &KItemListContainer::updateScrollOffsetScrollBar);
224 connect(current, &KItemListView::itemOffsetChanged,
225 this, &KItemListContainer::updateItemOffsetScrollBar);
226 connect(current, &KItemListView::maximumItemOffsetChanged,
227 this, &KItemListContainer::updateItemOffsetScrollBar);
228 connect(current, &KItemListView::scrollTo, this, &KItemListContainer::scrollTo);
229 connect(m_horizontalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, current, &KItemListView::scrollingStopped);
230 connect(m_verticalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, current, &KItemListView::scrollingStopped);
231
232 m_horizontalSmoothScroller->setTargetObject(current);
233 m_verticalSmoothScroller->setTargetObject(current);
234 updateSmoothScrollers(current->scrollOrientation());
235 }
236 }
237
scrollTo(qreal offset)238 void KItemListContainer::scrollTo(qreal offset)
239 {
240 const KItemListView* view = m_controller->view();
241 if (view) {
242 if (view->scrollOrientation() == Qt::Vertical) {
243 m_verticalSmoothScroller->scrollTo(offset);
244 } else {
245 m_horizontalSmoothScroller->scrollTo(offset);
246 }
247 }
248 }
249
updateScrollOffsetScrollBar()250 void KItemListContainer::updateScrollOffsetScrollBar()
251 {
252 const KItemListView* view = m_controller->view();
253 if (!view) {
254 return;
255 }
256
257 KItemListSmoothScroller* smoothScroller = nullptr;
258 QScrollBar* scrollOffsetScrollBar = nullptr;
259 int singleStep = 0;
260 int pageStep = 0;
261 int maximum = 0;
262 if (view->scrollOrientation() == Qt::Vertical) {
263 smoothScroller = m_verticalSmoothScroller;
264 scrollOffsetScrollBar = verticalScrollBar();
265
266 // Don't scroll super fast when using a wheel mouse:
267 // We want to consider one "line" to be the text label which has a
268 // roughly fixed height rather than using the height of the icon which
269 // may be very tall
270 const QFontMetrics metrics(font());
271 singleStep = metrics.height() * QApplication::wheelScrollLines();
272
273 // We cannot use view->size().height() because this height might
274 // include the header widget, which is not part of the scrolled area.
275 pageStep = view->verticalPageStep();
276
277 // However, the total height of the view must be considered for the
278 // maximum value of the scroll bar. Note that the view's scrollOffset()
279 // refers to the offset of the top part of the view, which might be
280 // hidden behind the header.
281 maximum = qMax(0, int(view->maximumScrollOffset() - view->size().height()));
282 } else {
283 smoothScroller = m_horizontalSmoothScroller;
284 scrollOffsetScrollBar = horizontalScrollBar();
285 singleStep = view->itemSize().width();
286 pageStep = view->size().width();
287 maximum = qMax(0, int(view->maximumScrollOffset() - view->size().width()));
288 }
289
290 const int value = view->scrollOffset();
291 if (smoothScroller->requestScrollBarUpdate(maximum)) {
292 const bool updatePolicy = (scrollOffsetScrollBar->maximum() > 0 && maximum == 0)
293 || horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOn;
294
295 scrollOffsetScrollBar->setSingleStep(singleStep);
296 scrollOffsetScrollBar->setPageStep(pageStep);
297 scrollOffsetScrollBar->setMinimum(0);
298 scrollOffsetScrollBar->setMaximum(maximum);
299 scrollOffsetScrollBar->setValue(value);
300
301 if (updatePolicy) {
302 // Prevent a potential endless layout loop (see bug #293318).
303 updateScrollOffsetScrollBarPolicy();
304 }
305 }
306 }
307
updateItemOffsetScrollBar()308 void KItemListContainer::updateItemOffsetScrollBar()
309 {
310 const KItemListView* view = m_controller->view();
311 if (!view) {
312 return;
313 }
314
315 KItemListSmoothScroller* smoothScroller = nullptr;
316 QScrollBar* itemOffsetScrollBar = nullptr;
317 int singleStep = 0;
318 int pageStep = 0;
319 if (view->scrollOrientation() == Qt::Vertical) {
320 smoothScroller = m_horizontalSmoothScroller;
321 itemOffsetScrollBar = horizontalScrollBar();
322 singleStep = view->size().width() / 10;
323 pageStep = view->size().width();
324 } else {
325 smoothScroller = m_verticalSmoothScroller;
326 itemOffsetScrollBar = verticalScrollBar();
327 singleStep = view->size().height() / 10;
328 pageStep = view->size().height();
329 }
330
331 const int value = view->itemOffset();
332 const int maximum = qMax(0, int(view->maximumItemOffset()) - pageStep);
333 if (smoothScroller->requestScrollBarUpdate(maximum)) {
334 itemOffsetScrollBar->setSingleStep(singleStep);
335 itemOffsetScrollBar->setPageStep(pageStep);
336 itemOffsetScrollBar->setMinimum(0);
337 itemOffsetScrollBar->setMaximum(maximum);
338 itemOffsetScrollBar->setValue(value);
339 }
340 }
341
stopScroller()342 void KItemListContainer::stopScroller()
343 {
344 m_scroller->stop();
345 }
346
updateGeometries()347 void KItemListContainer::updateGeometries()
348 {
349 QRect rect = geometry();
350
351 int extra = frameWidth() * 2;
352 QStyleOption option;
353 option.initFrom(this);
354 int scrollbarSpacing = 0;
355 if (style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &option, this)) {
356 scrollbarSpacing = style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing, &option, this);
357 }
358
359 const int widthDec = verticalScrollBar()->isVisible()
360 ? extra + scrollbarSpacing + style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this)
361 : extra;
362
363 const int heightDec = horizontalScrollBar()->isVisible()
364 ? extra + scrollbarSpacing + style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this)
365 : extra;
366
367 const QRectF newGeometry(0, 0, rect.width() - widthDec,
368 rect.height() - heightDec);
369 if (m_controller->view()->geometry() != newGeometry) {
370 m_controller->view()->setGeometry(newGeometry);
371
372 // Get the real geometry of the view again since the scrollbars
373 // visibilities and the view geometry may have changed in re-layout.
374 static_cast<KItemListContainerViewport*>(viewport())->scene()->setSceneRect(m_controller->view()->geometry());
375 static_cast<KItemListContainerViewport*>(viewport())->viewport()->setGeometry(m_controller->view()->geometry().toRect());
376
377 updateScrollOffsetScrollBar();
378 updateItemOffsetScrollBar();
379 }
380 }
381
updateSmoothScrollers(Qt::Orientation orientation)382 void KItemListContainer::updateSmoothScrollers(Qt::Orientation orientation)
383 {
384 if (orientation == Qt::Vertical) {
385 m_verticalSmoothScroller->setPropertyName("scrollOffset");
386 m_horizontalSmoothScroller->setPropertyName("itemOffset");
387 } else {
388 m_horizontalSmoothScroller->setPropertyName("scrollOffset");
389 m_verticalSmoothScroller->setPropertyName("itemOffset");
390 }
391 }
392
updateScrollOffsetScrollBarPolicy()393 void KItemListContainer::updateScrollOffsetScrollBarPolicy()
394 {
395 const KItemListView* view = m_controller->view();
396 Q_ASSERT(view);
397 const bool vertical = (view->scrollOrientation() == Qt::Vertical);
398
399 QStyleOption option;
400 option.initFrom(this);
401 const int scrollBarInc = style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this);
402
403 QSizeF newViewSize = m_controller->view()->size();
404 if (vertical) {
405 newViewSize.rwidth() += scrollBarInc;
406 } else {
407 newViewSize.rheight() += scrollBarInc;
408 }
409
410 const Qt::ScrollBarPolicy policy = view->scrollBarRequired(newViewSize)
411 ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAsNeeded;
412 if (vertical) {
413 setVerticalScrollBarPolicy(policy);
414 } else {
415 setHorizontalScrollBarPolicy(policy);
416 }
417 }
418
419 #include "kitemlistcontainer.moc"
420