]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kitemlistcontainer.cpp
Overhaul main view accessibility
[dolphin.git] / src / kitemviews / kitemlistcontainer.cpp
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 #ifndef QT_NO_ACCESSIBILITY
16 #include <QAccessibleEvent>
17 #endif
18 #include <QApplication>
19 #include <QFontMetrics>
20 #include <QGraphicsScene>
21 #include <QGraphicsView>
22 #include <QScrollBar>
23 #include <QScroller>
24 #include <QStyleOption>
25
26 /**
27 * Replaces the default viewport of KItemListContainer by a
28 * non-scrollable viewport. The scrolling is done in an optimized
29 * way by KItemListView internally.
30 */
31 class KItemListContainerViewport : public QGraphicsView
32 {
33 Q_OBJECT
34
35 public:
36 KItemListContainerViewport(QGraphicsScene *scene, QWidget *parent);
37
38 protected:
39 void wheelEvent(QWheelEvent *event) override;
40 };
41
42 KItemListContainerViewport::KItemListContainerViewport(QGraphicsScene *scene, QWidget *parent)
43 : QGraphicsView(scene, parent)
44 {
45 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
46 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
47 setViewportMargins(0, 0, 0, 0);
48 setFrameShape(QFrame::NoFrame);
49 }
50
51 void KItemListContainerViewport::wheelEvent(QWheelEvent *event)
52 {
53 // Assure that the wheel-event gets forwarded to the parent
54 // and not handled at all by QGraphicsView.
55 event->ignore();
56 }
57
58 KItemListContainer::KItemListContainer(KItemListController *controller, QWidget *parent)
59 : QAbstractScrollArea(parent)
60 , m_controller(controller)
61 , m_horizontalSmoothScroller(nullptr)
62 , m_verticalSmoothScroller(nullptr)
63 , m_scroller(nullptr)
64 {
65 Q_ASSERT(controller);
66 controller->setParent(this);
67
68 QGraphicsView *graphicsView = new KItemListContainerViewport(new QGraphicsScene(this), this);
69 setViewport(graphicsView);
70
71 m_horizontalSmoothScroller = new KItemListSmoothScroller(horizontalScrollBar(), this);
72 m_verticalSmoothScroller = new KItemListSmoothScroller(verticalScrollBar(), this);
73
74 if (controller->model()) {
75 slotModelChanged(controller->model(), nullptr);
76 }
77 if (controller->view()) {
78 slotViewChanged(controller->view(), nullptr);
79 }
80
81 connect(controller, &KItemListController::modelChanged, this, &KItemListContainer::slotModelChanged);
82 connect(controller, &KItemListController::viewChanged, this, &KItemListContainer::slotViewChanged);
83
84 m_scroller = QScroller::scroller(viewport());
85 m_scroller->grabGesture(viewport());
86 connect(controller, &KItemListController::scrollerStop, this, &KItemListContainer::stopScroller);
87 connect(m_scroller, &QScroller::stateChanged, controller, &KItemListController::slotStateChanged);
88 }
89
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
98 KItemListController *KItemListContainer::controller() const
99 {
100 return m_controller;
101 }
102
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
124 bool KItemListContainer::enabledFrame() const
125 {
126 const QGraphicsView *graphicsView = qobject_cast<QGraphicsView *>(viewport());
127 return graphicsView->autoFillBackground();
128 }
129
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
144 void KItemListContainer::contextMenuEvent(QContextMenuEvent *event)
145 {
146 // Note copied from the keyPressEvent() method above because the same reasons probably also apply here.
147 // TODO: We should find a better way to handle the context menu events in the view.
148 // The reasons why we need this hack are:
149 // 1. Without reimplementing contextMenuEvent() here, the event would not reach the QGraphicsView.
150 // 2. By default, the KItemListView does not have the keyboard focus in the QGraphicsScene, so
151 // simply sending the event to the QGraphicsView which is the KItemListContainer's viewport
152 // does not work.
153 KItemListView *view = m_controller->view();
154 if (view) {
155 QApplication::sendEvent(view, event);
156 }
157 }
158
159 void KItemListContainer::showEvent(QShowEvent *event)
160 {
161 QAbstractScrollArea::showEvent(event);
162 updateGeometries();
163 }
164
165 void KItemListContainer::resizeEvent(QResizeEvent *event)
166 {
167 QAbstractScrollArea::resizeEvent(event);
168 updateGeometries();
169 }
170
171 void KItemListContainer::scrollContentsBy(int dx, int dy)
172 {
173 m_horizontalSmoothScroller->scrollContentsBy(dx);
174 m_verticalSmoothScroller->scrollContentsBy(dy);
175 }
176
177 void KItemListContainer::wheelEvent(QWheelEvent *event)
178 {
179 if (event->modifiers().testFlag(Qt::ControlModifier)) {
180 event->ignore();
181 return;
182 }
183
184 KItemListView *view = m_controller->view();
185 if (!view) {
186 event->ignore();
187 return;
188 }
189
190 const bool scrollHorizontally = (qAbs(event->angleDelta().y()) < qAbs(event->angleDelta().x())) || (!verticalScrollBar()->isVisible());
191 KItemListSmoothScroller *smoothScroller = scrollHorizontally ? m_horizontalSmoothScroller : m_verticalSmoothScroller;
192
193 smoothScroller->handleWheelEvent(event);
194 }
195
196 void KItemListContainer::focusInEvent(QFocusEvent *event)
197 {
198 KItemListView *view = m_controller->view();
199 if (view) {
200 QApplication::sendEvent(view, event);
201
202 // We need to set the focus to the view or accessibility software will only announce the container (which has no information available itself).
203 // For some reason actively setting the focus to the view needs to be delayed or the focus will immediately go back to this container.
204 QTimer::singleShot(0, this, [this, view]() {
205 view->setFocus();
206 #ifndef QT_NO_ACCESSIBILITY
207 QAccessibleEvent accessibleFocusInEvent(this, QAccessible::Focus);
208 accessibleFocusInEvent.setChild(0);
209 QAccessible::updateAccessibility(&accessibleFocusInEvent);
210 #endif
211 });
212 }
213 }
214
215 void KItemListContainer::focusOutEvent(QFocusEvent *event)
216 {
217 KItemListView *view = m_controller->view();
218 if (view) {
219 QApplication::sendEvent(view, event);
220 }
221 }
222
223 void KItemListContainer::slotScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous)
224 {
225 Q_UNUSED(previous)
226 updateSmoothScrollers(current);
227 }
228
229 void KItemListContainer::slotModelChanged(KItemModelBase *current, KItemModelBase *previous)
230 {
231 Q_UNUSED(current)
232 Q_UNUSED(previous)
233 }
234
235 void KItemListContainer::slotViewChanged(KItemListView *current, KItemListView *previous)
236 {
237 QGraphicsScene *scene = static_cast<QGraphicsView *>(viewport())->scene();
238 if (previous) {
239 scene->removeItem(previous);
240 disconnect(previous, &KItemListView::scrollOrientationChanged, this, &KItemListContainer::slotScrollOrientationChanged);
241 disconnect(previous, &KItemListView::scrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar);
242 disconnect(previous, &KItemListView::maximumScrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar);
243 disconnect(previous, &KItemListView::itemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar);
244 disconnect(previous, &KItemListView::maximumItemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar);
245 disconnect(previous, &KItemListView::scrollTo, this, &KItemListContainer::scrollTo);
246 disconnect(m_horizontalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, previous, &KItemListView::scrollingStopped);
247 disconnect(m_verticalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, previous, &KItemListView::scrollingStopped);
248 m_horizontalSmoothScroller->setTargetObject(nullptr);
249 m_verticalSmoothScroller->setTargetObject(nullptr);
250 }
251 if (current) {
252 scene->addItem(current);
253 connect(current, &KItemListView::scrollOrientationChanged, this, &KItemListContainer::slotScrollOrientationChanged);
254 connect(current, &KItemListView::scrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar);
255 connect(current, &KItemListView::maximumScrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar);
256 connect(current, &KItemListView::itemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar);
257 connect(current, &KItemListView::maximumItemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar);
258 connect(current, &KItemListView::scrollTo, this, &KItemListContainer::scrollTo);
259 connect(m_horizontalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, current, &KItemListView::scrollingStopped);
260 connect(m_verticalSmoothScroller, &KItemListSmoothScroller::scrollingStopped, current, &KItemListView::scrollingStopped);
261
262 m_horizontalSmoothScroller->setTargetObject(current);
263 m_verticalSmoothScroller->setTargetObject(current);
264 updateSmoothScrollers(current->scrollOrientation());
265 }
266 }
267
268 void KItemListContainer::scrollTo(qreal offset)
269 {
270 const KItemListView *view = m_controller->view();
271 if (view) {
272 if (view->scrollOrientation() == Qt::Vertical) {
273 m_verticalSmoothScroller->scrollTo(offset);
274 } else {
275 m_horizontalSmoothScroller->scrollTo(offset);
276 }
277 }
278 }
279
280 void KItemListContainer::updateScrollOffsetScrollBar()
281 {
282 const KItemListView *view = m_controller->view();
283 if (!view) {
284 return;
285 }
286
287 KItemListSmoothScroller *smoothScroller = nullptr;
288 QScrollBar *scrollOffsetScrollBar = nullptr;
289 int singleStep = 0;
290 int pageStep = 0;
291 int maximum = 0;
292 if (view->scrollOrientation() == Qt::Vertical) {
293 smoothScroller = m_verticalSmoothScroller;
294 scrollOffsetScrollBar = verticalScrollBar();
295
296 // Don't scroll super fast when using a wheel mouse:
297 // We want to consider one "line" to be the text label which has a
298 // roughly fixed height rather than using the height of the icon which
299 // may be very tall
300 const QFontMetrics metrics(font());
301 singleStep = metrics.height() * QApplication::wheelScrollLines();
302
303 // We cannot use view->size().height() because this height might
304 // include the header widget, which is not part of the scrolled area.
305 pageStep = view->verticalPageStep();
306
307 // However, the total height of the view must be considered for the
308 // maximum value of the scroll bar. Note that the view's scrollOffset()
309 // refers to the offset of the top part of the view, which might be
310 // hidden behind the header.
311 maximum = qMax(0, int(view->maximumScrollOffset() - view->size().height()));
312 } else {
313 smoothScroller = m_horizontalSmoothScroller;
314 scrollOffsetScrollBar = horizontalScrollBar();
315 singleStep = view->itemSize().width();
316 pageStep = view->size().width();
317 maximum = qMax(0, int(view->maximumScrollOffset() - view->size().width()));
318 }
319
320 const int value = view->scrollOffset();
321 if (smoothScroller->requestScrollBarUpdate(maximum)) {
322 const bool updatePolicy = (scrollOffsetScrollBar->maximum() > 0 && maximum == 0) || horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOn;
323
324 scrollOffsetScrollBar->setSingleStep(singleStep);
325 scrollOffsetScrollBar->setPageStep(pageStep);
326 scrollOffsetScrollBar->setMinimum(0);
327 scrollOffsetScrollBar->setMaximum(maximum);
328 scrollOffsetScrollBar->setValue(value);
329
330 if (updatePolicy) {
331 // Prevent a potential endless layout loop (see bug #293318).
332 updateScrollOffsetScrollBarPolicy();
333 }
334 }
335 }
336
337 void KItemListContainer::updateItemOffsetScrollBar()
338 {
339 const KItemListView *view = m_controller->view();
340 if (!view) {
341 return;
342 }
343
344 KItemListSmoothScroller *smoothScroller = nullptr;
345 QScrollBar *itemOffsetScrollBar = nullptr;
346 int singleStep = 0;
347 int pageStep = 0;
348 if (view->scrollOrientation() == Qt::Vertical) {
349 smoothScroller = m_horizontalSmoothScroller;
350 itemOffsetScrollBar = horizontalScrollBar();
351 singleStep = view->size().width() / 10;
352 pageStep = view->size().width();
353 } else {
354 smoothScroller = m_verticalSmoothScroller;
355 itemOffsetScrollBar = verticalScrollBar();
356 singleStep = view->size().height() / 10;
357 pageStep = view->size().height();
358 }
359
360 const int value = view->itemOffset();
361 const int maximum = qMax(0, int(view->maximumItemOffset()) - pageStep);
362 if (smoothScroller->requestScrollBarUpdate(maximum)) {
363 itemOffsetScrollBar->setSingleStep(singleStep);
364 itemOffsetScrollBar->setPageStep(pageStep);
365 itemOffsetScrollBar->setMinimum(0);
366 itemOffsetScrollBar->setMaximum(maximum);
367 itemOffsetScrollBar->setValue(value);
368 }
369 }
370
371 void KItemListContainer::stopScroller()
372 {
373 m_scroller->stop();
374 }
375
376 void KItemListContainer::updateGeometries()
377 {
378 QRect rect = geometry();
379
380 int extra = frameWidth() * 2;
381 QStyleOption option;
382 option.initFrom(this);
383 int scrollbarSpacing = 0;
384 if (style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &option, this)) {
385 scrollbarSpacing = style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing, &option, this);
386 }
387
388 const int widthDec = verticalScrollBar()->isVisible() ? extra + scrollbarSpacing + style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this) : extra;
389
390 const int heightDec =
391 horizontalScrollBar()->isVisible() ? extra + scrollbarSpacing + style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this) : extra;
392
393 const QRectF newGeometry(0, 0, rect.width() - widthDec, rect.height() - heightDec);
394 if (m_controller->view()->geometry() != newGeometry) {
395 m_controller->view()->setGeometry(newGeometry);
396
397 // Get the real geometry of the view again since the scrollbars
398 // visibilities and the view geometry may have changed in re-layout.
399 static_cast<KItemListContainerViewport *>(viewport())->scene()->setSceneRect(m_controller->view()->geometry());
400 static_cast<KItemListContainerViewport *>(viewport())->viewport()->setGeometry(m_controller->view()->geometry().toRect());
401
402 updateScrollOffsetScrollBar();
403 updateItemOffsetScrollBar();
404 }
405 }
406
407 void KItemListContainer::updateSmoothScrollers(Qt::Orientation orientation)
408 {
409 if (orientation == Qt::Vertical) {
410 m_verticalSmoothScroller->setPropertyName("scrollOffset");
411 m_horizontalSmoothScroller->setPropertyName("itemOffset");
412 } else {
413 m_horizontalSmoothScroller->setPropertyName("scrollOffset");
414 m_verticalSmoothScroller->setPropertyName("itemOffset");
415 }
416
417 const bool isRightToLeft = m_controller->view()->layoutDirection() == Qt::RightToLeft;
418 QScrollBar *hScrollBar = horizontalScrollBar();
419 hScrollBar->setInvertedAppearance(isRightToLeft && orientation == Qt::Vertical);
420 hScrollBar->setInvertedControls(!isRightToLeft || orientation == Qt::Vertical);
421 }
422
423 void KItemListContainer::updateScrollOffsetScrollBarPolicy()
424 {
425 const KItemListView *view = m_controller->view();
426 Q_ASSERT(view);
427 const bool vertical = (view->scrollOrientation() == Qt::Vertical);
428
429 QStyleOption option;
430 option.initFrom(this);
431 const int scrollBarInc = style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this);
432
433 QSizeF newViewSize = m_controller->view()->size();
434 if (vertical) {
435 newViewSize.rwidth() += scrollBarInc;
436 } else {
437 newViewSize.rheight() += scrollBarInc;
438 }
439
440 const Qt::ScrollBarPolicy policy = view->scrollBarRequired(newViewSize) ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAsNeeded;
441 if (vertical) {
442 setVerticalScrollBarPolicy(policy);
443 } else {
444 setHorizontalScrollBarPolicy(policy);
445 }
446 }
447
448 #include "kitemlistcontainer.moc"
449 #include "moc_kitemlistcontainer.cpp"