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