File indexing completed on 2024-04-28 05:45:06

0001 /*
0002  * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
0003  *
0004  * Based on the Itemviews NG project from Trolltech Labs
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "kitemlistcontainer.h"
0010 
0011 #include "kitemlistcontroller.h"
0012 #include "kitemlistview.h"
0013 #include "private/kitemlistsmoothscroller.h"
0014 
0015 #include <QApplication>
0016 #include <QFontMetrics>
0017 #include <QGraphicsScene>
0018 #include <QGraphicsView>
0019 #include <QScrollBar>
0020 #include <QScroller>
0021 #include <QStyleOption>
0022 
0023 /**
0024  * Replaces the default viewport of KItemListContainer by a
0025  * non-scrollable viewport. The scrolling is done in an optimized
0026  * way by KItemListView internally.
0027  */
0028 class KItemListContainerViewport : public QGraphicsView
0029 {
0030     Q_OBJECT
0031 
0032 public:
0033     KItemListContainerViewport(QGraphicsScene *scene, QWidget *parent);
0034 
0035 protected:
0036     void wheelEvent(QWheelEvent *event) override;
0037 };
0038 
0039 KItemListContainerViewport::KItemListContainerViewport(QGraphicsScene *scene, QWidget *parent)
0040     : QGraphicsView(scene, parent)
0041 {
0042     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0043     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0044     setViewportMargins(0, 0, 0, 0);
0045     setFrameShape(QFrame::NoFrame);
0046 }
0047 
0048 void KItemListContainerViewport::wheelEvent(QWheelEvent *event)
0049 {
0050     // Assure that the wheel-event gets forwarded to the parent
0051     // and not handled at all by QGraphicsView.
0052     event->ignore();
0053 }
0054 
0055 KItemListContainer::KItemListContainer(KItemListController *controller, QWidget *parent)
0056     : QAbstractScrollArea(parent)
0057     , m_controller(controller)
0058     , m_horizontalSmoothScroller(nullptr)
0059     , m_verticalSmoothScroller(nullptr)
0060     , m_scroller(nullptr)
0061 {
0062     Q_ASSERT(controller);
0063     controller->setParent(this);
0064 
0065     QGraphicsView *graphicsView = new KItemListContainerViewport(new QGraphicsScene(this), this);
0066     setViewport(graphicsView);
0067 
0068     m_horizontalSmoothScroller = new KItemListSmoothScroller(horizontalScrollBar(), this);
0069     m_verticalSmoothScroller = new KItemListSmoothScroller(verticalScrollBar(), this);
0070 
0071     if (controller->model()) {
0072         slotModelChanged(controller->model(), nullptr);
0073     }
0074     if (controller->view()) {
0075         slotViewChanged(controller->view(), nullptr);
0076     }
0077 
0078     connect(controller, &KItemListController::modelChanged, this, &KItemListContainer::slotModelChanged);
0079     connect(controller, &KItemListController::viewChanged, this, &KItemListContainer::slotViewChanged);
0080 
0081     m_scroller = QScroller::scroller(viewport());
0082     m_scroller->grabGesture(viewport());
0083     connect(controller, &KItemListController::scrollerStop, this, &KItemListContainer::stopScroller);
0084     connect(m_scroller, &QScroller::stateChanged, controller, &KItemListController::slotStateChanged);
0085 }
0086 
0087 KItemListContainer::~KItemListContainer()
0088 {
0089     // Don't rely on the QObject-order to delete the controller, otherwise
0090     // the QGraphicsScene might get deleted before the view.
0091     delete m_controller;
0092     m_controller = nullptr;
0093 }
0094 
0095 KItemListController *KItemListContainer::controller() const
0096 {
0097     return m_controller;
0098 }
0099 
0100 void KItemListContainer::setEnabledFrame(bool enable)
0101 {
0102     QGraphicsView *graphicsView = qobject_cast<QGraphicsView *>(viewport());
0103     if (enable) {
0104         setFrameShape(QFrame::StyledPanel);
0105         graphicsView->setPalette(palette());
0106         graphicsView->viewport()->setAutoFillBackground(true);
0107     } else {
0108         setFrameShape(QFrame::NoFrame);
0109         // Make the background of the container transparent and apply the window-text color
0110         // to the text color, so that enough contrast is given for all color
0111         // schemes
0112         QPalette p = graphicsView->palette();
0113         p.setColor(QPalette::Active, QPalette::Text, p.color(QPalette::Active, QPalette::WindowText));
0114         p.setColor(QPalette::Inactive, QPalette::Text, p.color(QPalette::Inactive, QPalette::WindowText));
0115         p.setColor(QPalette::Disabled, QPalette::Text, p.color(QPalette::Disabled, QPalette::WindowText));
0116         graphicsView->setPalette(p);
0117         graphicsView->viewport()->setAutoFillBackground(false);
0118     }
0119 }
0120 
0121 bool KItemListContainer::enabledFrame() const
0122 {
0123     const QGraphicsView *graphicsView = qobject_cast<QGraphicsView *>(viewport());
0124     return graphicsView->autoFillBackground();
0125 }
0126 
0127 void KItemListContainer::keyPressEvent(QKeyEvent *event)
0128 {
0129     // TODO: We should find a better way to handle the key press events in the view.
0130     // The reasons why we need this hack are:
0131     // 1. Without reimplementing keyPressEvent() here, the event would not reach the QGraphicsView.
0132     // 2. By default, the KItemListView does not have the keyboard focus in the QGraphicsScene, so
0133     //    simply sending the event to the QGraphicsView which is the KItemListContainer's viewport
0134     //    does not work.
0135     KItemListView *view = m_controller->view();
0136     if (view) {
0137         QApplication::sendEvent(view, event);
0138     }
0139 }
0140 
0141 void KItemListContainer::contextMenuEvent(QContextMenuEvent *event)
0142 {
0143     // Note copied from the keyPressEvent() method above because the same reasons probably also apply here.
0144     // TODO: We should find a better way to handle the context menu events in the view.
0145     // The reasons why we need this hack are:
0146     // 1. Without reimplementing contextMenuEvent() here, the event would not reach the QGraphicsView.
0147     // 2. By default, the KItemListView does not have the keyboard focus in the QGraphicsScene, so
0148     //    simply sending the event to the QGraphicsView which is the KItemListContainer's viewport
0149     //    does not work.
0150     KItemListView *view = m_controller->view();
0151     if (view) {
0152         QApplication::sendEvent(view, event);
0153     }
0154 }
0155 
0156 void KItemListContainer::showEvent(QShowEvent *event)
0157 {
0158     QAbstractScrollArea::showEvent(event);
0159     updateGeometries();
0160 }
0161 
0162 void KItemListContainer::resizeEvent(QResizeEvent *event)
0163 {
0164     QAbstractScrollArea::resizeEvent(event);
0165     updateGeometries();
0166 }
0167 
0168 void KItemListContainer::scrollContentsBy(int dx, int dy)
0169 {
0170     m_horizontalSmoothScroller->scrollContentsBy(dx);
0171     m_verticalSmoothScroller->scrollContentsBy(dy);
0172 }
0173 
0174 void KItemListContainer::wheelEvent(QWheelEvent *event)
0175 {
0176     if (event->modifiers().testFlag(Qt::ControlModifier)) {
0177         event->ignore();
0178         return;
0179     }
0180 
0181     KItemListView *view = m_controller->view();
0182     if (!view) {
0183         event->ignore();
0184         return;
0185     }
0186 
0187     const bool scrollHorizontally = (qAbs(event->angleDelta().y()) < qAbs(event->angleDelta().x())) || (!verticalScrollBar()->isVisible());
0188     KItemListSmoothScroller *smoothScroller = scrollHorizontally ? m_horizontalSmoothScroller : m_verticalSmoothScroller;
0189 
0190     smoothScroller->handleWheelEvent(event);
0191 }
0192 
0193 void KItemListContainer::focusInEvent(QFocusEvent *event)
0194 {
0195     KItemListView *view = m_controller->view();
0196     if (view) {
0197         QApplication::sendEvent(view, event);
0198     }
0199 }
0200 
0201 void KItemListContainer::focusOutEvent(QFocusEvent *event)
0202 {
0203     KItemListView *view = m_controller->view();
0204     if (view) {
0205         QApplication::sendEvent(view, event);
0206     }
0207 }
0208 
0209 void KItemListContainer::slotScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous)
0210 {
0211     Q_UNUSED(previous)
0212     updateSmoothScrollers(current);
0213 }
0214 
0215 void KItemListContainer::slotModelChanged(KItemModelBase *current, KItemModelBase *previous)
0216 {
0217     Q_UNUSED(current)
0218     Q_UNUSED(previous)
0219 }
0220 
0221 void KItemListContainer::slotViewChanged(KItemListView *current, KItemListView *previous)
0222 {
0223     QGraphicsScene *scene = static_cast<QGraphicsView *>(viewport())->scene();
0224     if (previous) {
0225         scene->removeItem(previous);
0226         disconnect(previous, &KItemListView::scrollOrientationChanged, this, &KItemListContainer::slotScrollOrientationChanged);
0227         disconnect(previous, &KItemListView::scrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar);
0228         disconnect(previous, &KItemListView::maximumScrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar);
0229         disconnect(previous, &KItemListView::itemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar);
0230         disconnect(previous, &KItemListView::maximumItemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar);
0231         disconnect(previous, &KItemListView::scrollTo, this, &KItemListContainer::scrollTo);
0232         disconnect(m_horizontalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, previous, &KItemListView::scrollingStopped);
0233         disconnect(m_verticalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, previous, &KItemListView::scrollingStopped);
0234         m_horizontalSmoothScroller->setTargetObject(nullptr);
0235         m_verticalSmoothScroller->setTargetObject(nullptr);
0236     }
0237     if (current) {
0238         scene->addItem(current);
0239         connect(current, &KItemListView::scrollOrientationChanged, this, &KItemListContainer::slotScrollOrientationChanged);
0240         connect(current, &KItemListView::scrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar);
0241         connect(current, &KItemListView::maximumScrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar);
0242         connect(current, &KItemListView::itemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar);
0243         connect(current, &KItemListView::maximumItemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar);
0244         connect(current, &KItemListView::scrollTo, this, &KItemListContainer::scrollTo);
0245         connect(m_horizontalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, current, &KItemListView::scrollingStopped);
0246         connect(m_verticalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, current, &KItemListView::scrollingStopped);
0247 
0248         m_horizontalSmoothScroller->setTargetObject(current);
0249         m_verticalSmoothScroller->setTargetObject(current);
0250         updateSmoothScrollers(current->scrollOrientation());
0251     }
0252 }
0253 
0254 void KItemListContainer::scrollTo(qreal offset)
0255 {
0256     const KItemListView *view = m_controller->view();
0257     if (view) {
0258         if (view->scrollOrientation() == Qt::Vertical) {
0259             m_verticalSmoothScroller->scrollTo(offset);
0260         } else {
0261             m_horizontalSmoothScroller->scrollTo(offset);
0262         }
0263     }
0264 }
0265 
0266 void KItemListContainer::updateScrollOffsetScrollBar()
0267 {
0268     const KItemListView *view = m_controller->view();
0269     if (!view) {
0270         return;
0271     }
0272 
0273     KItemListSmoothScroller *smoothScroller = nullptr;
0274     QScrollBar *scrollOffsetScrollBar = nullptr;
0275     int singleStep = 0;
0276     int pageStep = 0;
0277     int maximum = 0;
0278     if (view->scrollOrientation() == Qt::Vertical) {
0279         smoothScroller = m_verticalSmoothScroller;
0280         scrollOffsetScrollBar = verticalScrollBar();
0281 
0282         // Don't scroll super fast when using a wheel mouse:
0283         // We want to consider one "line" to be the text label which has a
0284         // roughly fixed height rather than using the height of the icon which
0285         // may be very tall
0286         const QFontMetrics metrics(font());
0287         singleStep = metrics.height() * QApplication::wheelScrollLines();
0288 
0289         // We cannot use view->size().height() because this height might
0290         // include the header widget, which is not part of the scrolled area.
0291         pageStep = view->verticalPageStep();
0292 
0293         // However, the total height of the view must be considered for the
0294         // maximum value of the scroll bar. Note that the view's scrollOffset()
0295         // refers to the offset of the top part of the view, which might be
0296         // hidden behind the header.
0297         maximum = qMax(0, int(view->maximumScrollOffset() - view->size().height()));
0298     } else {
0299         smoothScroller = m_horizontalSmoothScroller;
0300         scrollOffsetScrollBar = horizontalScrollBar();
0301         singleStep = view->itemSize().width();
0302         pageStep = view->size().width();
0303         maximum = qMax(0, int(view->maximumScrollOffset() - view->size().width()));
0304     }
0305 
0306     const int value = view->scrollOffset();
0307     if (smoothScroller->requestScrollBarUpdate(maximum)) {
0308         const bool updatePolicy = (scrollOffsetScrollBar->maximum() > 0 && maximum == 0) || horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOn;
0309 
0310         scrollOffsetScrollBar->setSingleStep(singleStep);
0311         scrollOffsetScrollBar->setPageStep(pageStep);
0312         scrollOffsetScrollBar->setMinimum(0);
0313         scrollOffsetScrollBar->setMaximum(maximum);
0314         scrollOffsetScrollBar->setValue(value);
0315 
0316         if (updatePolicy) {
0317             // Prevent a potential endless layout loop (see bug #293318).
0318             updateScrollOffsetScrollBarPolicy();
0319         }
0320     }
0321 }
0322 
0323 void KItemListContainer::updateItemOffsetScrollBar()
0324 {
0325     const KItemListView *view = m_controller->view();
0326     if (!view) {
0327         return;
0328     }
0329 
0330     KItemListSmoothScroller *smoothScroller = nullptr;
0331     QScrollBar *itemOffsetScrollBar = nullptr;
0332     int singleStep = 0;
0333     int pageStep = 0;
0334     if (view->scrollOrientation() == Qt::Vertical) {
0335         smoothScroller = m_horizontalSmoothScroller;
0336         itemOffsetScrollBar = horizontalScrollBar();
0337         singleStep = view->size().width() / 10;
0338         pageStep = view->size().width();
0339     } else {
0340         smoothScroller = m_verticalSmoothScroller;
0341         itemOffsetScrollBar = verticalScrollBar();
0342         singleStep = view->size().height() / 10;
0343         pageStep = view->size().height();
0344     }
0345 
0346     const int value = view->itemOffset();
0347     const int maximum = qMax(0, int(view->maximumItemOffset()) - pageStep);
0348     if (smoothScroller->requestScrollBarUpdate(maximum)) {
0349         itemOffsetScrollBar->setSingleStep(singleStep);
0350         itemOffsetScrollBar->setPageStep(pageStep);
0351         itemOffsetScrollBar->setMinimum(0);
0352         itemOffsetScrollBar->setMaximum(maximum);
0353         itemOffsetScrollBar->setValue(value);
0354     }
0355 }
0356 
0357 void KItemListContainer::stopScroller()
0358 {
0359     m_scroller->stop();
0360 }
0361 
0362 void KItemListContainer::updateGeometries()
0363 {
0364     QRect rect = geometry();
0365 
0366     int extra = frameWidth() * 2;
0367     QStyleOption option;
0368     option.initFrom(this);
0369     int scrollbarSpacing = 0;
0370     if (style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &option, this)) {
0371         scrollbarSpacing = style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing, &option, this);
0372     }
0373 
0374     const int widthDec = verticalScrollBar()->isVisible() ? extra + scrollbarSpacing + style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this) : extra;
0375 
0376     const int heightDec =
0377         horizontalScrollBar()->isVisible() ? extra + scrollbarSpacing + style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this) : extra;
0378 
0379     const QRectF newGeometry(0, 0, rect.width() - widthDec, rect.height() - heightDec);
0380     if (m_controller->view()->geometry() != newGeometry) {
0381         m_controller->view()->setGeometry(newGeometry);
0382 
0383         // Get the real geometry of the view again since the scrollbars
0384         // visibilities and the view geometry may have changed in re-layout.
0385         static_cast<KItemListContainerViewport *>(viewport())->scene()->setSceneRect(m_controller->view()->geometry());
0386         static_cast<KItemListContainerViewport *>(viewport())->viewport()->setGeometry(m_controller->view()->geometry().toRect());
0387 
0388         updateScrollOffsetScrollBar();
0389         updateItemOffsetScrollBar();
0390     }
0391 }
0392 
0393 void KItemListContainer::updateSmoothScrollers(Qt::Orientation orientation)
0394 {
0395     if (orientation == Qt::Vertical) {
0396         m_verticalSmoothScroller->setPropertyName("scrollOffset");
0397         m_horizontalSmoothScroller->setPropertyName("itemOffset");
0398     } else {
0399         m_horizontalSmoothScroller->setPropertyName("scrollOffset");
0400         m_verticalSmoothScroller->setPropertyName("itemOffset");
0401     }
0402 }
0403 
0404 void KItemListContainer::updateScrollOffsetScrollBarPolicy()
0405 {
0406     const KItemListView *view = m_controller->view();
0407     Q_ASSERT(view);
0408     const bool vertical = (view->scrollOrientation() == Qt::Vertical);
0409 
0410     QStyleOption option;
0411     option.initFrom(this);
0412     const int scrollBarInc = style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this);
0413 
0414     QSizeF newViewSize = m_controller->view()->size();
0415     if (vertical) {
0416         newViewSize.rwidth() += scrollBarInc;
0417     } else {
0418         newViewSize.rheight() += scrollBarInc;
0419     }
0420 
0421     const Qt::ScrollBarPolicy policy = view->scrollBarRequired(newViewSize) ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAsNeeded;
0422     if (vertical) {
0423         setVerticalScrollBarPolicy(policy);
0424     } else {
0425         setHorizontalScrollBarPolicy(policy);
0426     }
0427 }
0428 
0429 #include "kitemlistcontainer.moc"
0430 #include "moc_kitemlistcontainer.cpp"