/*
Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CamCOPS.
CamCOPS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CamCOPS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/
/*
OPTIONAL LGPL: Alternatively, this file may be used under the terms of the
GNU Lesser General Public License version 3 as published by the Free
Software Foundation. You should have received a copy of the GNU Lesser
General Public License along with CamCOPS. If not, see
<https://www.gnu.org/licenses/>.
*/
// #define DEBUG_LAYOUT
// #define DEBUG_IRRELEVANT_EVENTS
// #define DEBUG_VIEWPORT_CHILD_SIZE
// Maximum of one of these:
// #define USE_STRETCH
#define RESIZE_FOR_HFW // define this for proper performance!
// #define HFW_METHOD_1
#define VANISHING_SCROLLBAR // define this to look better
#define USE_CUSTOM_VIEWPORT
// One of these only:
// #define TOUCHSCROLL_DIRECT // DOES NOT WORK
#define TOUCHSCROLL_SCROLLER // Works
// #define TOUCHSCROLL_FLICKCHARM // DOESN'T WORK (well? at all?)
#include "verticalscrollarea.h"
#include <QDebug>
#include <QEvent>
#include <QGestureEvent>
#include <QLayout>
#include <QScrollBar>
#include <QScroller>
#include "common/widgetconst.h"
#include "lib/margins.h"
#include "lib/reentrydepthguard.h"
#include "lib/sizehelpers.h"
#include "lib/uifunc.h"
#include "qobjects/flickcharm.h"
#include "widgets/verticalscrollareaviewport.h"
#ifdef DEBUG_LAYOUT
#include "lib/layoutdumper.h"
#endif
#ifdef USE_STRETCH
#include "layouts/layouts.h" // for VBoxLayout
#include "widgets/basewidget.h"
#endif
#if defined USE_STRETCH && defined RESIZE_FOR_HFW
#error Cannot #define both USE_SPACER and RESIZE_FOR_HFW
#endif
#if defined TOUCHSCROLL_DIRECT \
&& (defined TOUCHSCROLL_SCROLLER || defined TOUCHSCROLL_FLICKCHARM)
#error #define only one touch scrolling method
#endif
#if defined TOUCHSCROLL_SCROLLER \
&& (defined TOUCHSCROLL_DIRECT || defined TOUCHSCROLL_FLICKCHARM)
#error #define only one touch scrolling method
#endif
#if defined TOUCHSCROLL_FLICKCHARM \
&& (defined TOUCHSCROLL_DIRECT || defined TOUCHSCROLL_SCROLLER)
#error #define only one touch scrolling method
#endif
const int SQUASH_DOWN_TO_HEIGHT = 100;
/*
Widget layout looks like this:
. VerticalScrollArea [widget] // this
v QWidget 'qt_scrollarea_viewport' [widget] // viewport()
... Non-layout children:
SomeWidget [widget] // widget()
... Non-layout children:
QWidget 'qt_scrollarea_hcontainer' [widget] [HIDDEN]
QBoxLayout [layout]
QScrollBar [widget]
S QWidget 'qt_scrollarea_vcontainer' [widget]
// ... QAbstractScrollAreaScrollBarContainer
QBoxLayout [layout]
QScrollBar [widget]
// ... verticalScrollBar() ->
// QScrollBar* QAbstractScrollAreaPrivate::vbar
.............................................................
. .
. vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv SSS .
. vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv SSS .
. vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv SSS .
. vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv SSS .
. vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv SSS .
. vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv SSS .
. .
.............................................................
Typical values:
. 810 x 118 @ 0, 0 X extent 0-809 Y extent 0-117
v 798 x 116 @ 1, 1 X extent 1-798 Y extent 1-116
S 10 x 116 @ 799, 1 X extent 799-808 Y extent 1-116
I was doing this:
QRect viewport_rect = viewport()->geometry();
QRect scrollarea_rect = geometry();
Margins diffmargins = Margins::rectDiff(scrollarea_rect, viewport_rect);
new_min_width += diffmargins.totalWidth();
new_max_height += diffmargins.totalHeight();
and similar; but sometimes, e.g. in sizeHint(), scrollarea_rect doesn't
contain viewport_rect; e.g.
scrollarea_rect -- outer QRect(0,2 802x119)
viewport_rect -- inner QRect(1,1 790x117)
Aha! The second isn't in the same coordinates; it's relative to the top.
So we want to use this instead:
Margins::subRectMargins(scrollarea_rect, viewport_rect);
Sometimes the viewport is at (0,0) and is the same size as the scroll area,
so you have to check.
*/
/*
Leftover problem: you can get this situation:
VerticalScrollArea<0x0000000004de0050 'questionnaire_background_clinician'>,
visible, pos[DOWN] (0, 79), size[DOWN] (1920 x 649),
hasHeightForWidth()[UP] false, heightForWidth(1920)[UP] -1,
minimumSize (369 x 100), maximumSize (16777215 x 679),
sizeHint[UP] (551 x 649), minimumSizeHint[UP] (57 x 57),
sizePolicy[UP] (Expanding, Expanding) [hasHeightForWidth=false],
stylesheet: false,
properties: [_q_styleSheetWidgetFont="Sans Serif,9,-1,5,50,0,0,0,0,0"]
[alignment from layout: <horizontal_none> | <vertical_none>]
QWidget<0x00000000045e0750 'qt_scrollarea_viewport'>, visible, pos[DOWN]
(0, 0), size[DOWN] (1904 x 649), hasHeightForWidth()[UP] false,
heightForWidth(1904)[UP] -1, minimumSize (0 x 0),
maximumSize (16777215 x 16777215), sizeHint[UP] (-1 x -1),
minimumSizeHint[UP] (-1 x -1),
sizePolicy[UP] (Preferred, Preferred) [hasHeightForWidth=false],
stylesheet: false
... Non-layout children of QWidget<0x00000000045e0750
'qt_scrollarea_viewport'>:
BaseWidget<0x0000000003e87cb0 ''>, visible, pos[DOWN] (0, 0),
size[DOWN] (1904 x 679), hasHeightForWidth()[UP] true,
heightForWidth(1904)[UP] 649, minimumSize (0 x 0),
maximumSize (16777215 x 16777215), sizeHint[UP] (535 x 679),
minimumSizeHint[UP] (353 x 679),
sizePolicy[UP] (Preferred, Preferred)
[hasHeightForWidth=false], stylesheet: false,
properties:
[_q_styleSheetWidgetFont="Sans Serif,9,-1,5,50,0,0,0,0,0"]
i.e.
- the BaseWidget has HFW 1904 -> 649, but is given height 679 instead
by the QScrollArea code, because that's its sizeHint().
- QScrollArea::setWidget() does this:
if (!widget->testAttribute(Qt::WA_Resized))
widget->resize(widget->sizeHint());
- ... anywhere else?
Could we cope with that using setViewport(), using an HFW
widget rather than a plain widget?
Alternative would be to rewrite QScrollArea (and several parent classes)...
Specifically:
- QScrollArea / QScrollAreaPrivate
- QAbstractScrollArea / QAbstractScrollAreaPrivate
- ... and then, to make matters harder, QAbstractScrollArea has in its
header "friend class QWidgetPrivate;", and in qwidget.cpp we see that
QWidgetPrivate has special handling for QAbstractScrollArea.
Argh.
Alternatively: why is an HFW widget giving a sizeHint() where the height isn't
the HFW for its width?
... well, the prototypical example is:
BaseWidget<0x0000000004186f40 ''>, visible, pos[DOWN] (0, 0),
size[DOWN] (1904 x 679), hasHeightForWidth()[UP] true,
heightForWidth(1904)[UP] 649, minimumSize (0 x 0),
maximumSize (16777215 x 16777215), sizeHint[UP] (535 x 679),
minimumSizeHint[UP] (353 x 679),
sizePolicy[UP] (Preferred, Preferred) [hasHeightForWidth=false],
stylesheet: false,
properties:
[_q_styleSheetWidgetFont="Sans Serif,9,-1,5,50,0,0,0,0,0"]
Layout: VBoxLayoutHfw, constraint SetDefaultConstraint,
minimumSize[UP] (353 x 679), sizeHint[UP] (535 x 679),
maximumSize[UP] (524287 x 679), hasHeightForWidth[UP] true,
margin (l=9,t=9,r=9,b=9), spacing[UP] 6,
heightForWidth(1904)[UP] 649, minimumHeightForWidth(1904)[UP] 649
... where the VBoxLayoutHfw has
sizeHint[UP] (535 x 679)
heightForWidth(1904)[UP] 649
... so that's sensible (and the sizeHint is true as "how big it'd like to be").
---
Further leftover problem: an infinite bistable state.
For example:
... resetSizeLimits() - Child widget resized to QRect(0,0 365x377); setting
VerticalScrollArea minimum width to 140 (124 for widget, 16 for scrollbar);
setting minimum height to 100; setting maximum height to 377 ([scrollbar
inactive] widget's width 365 -> not narrowed -> max height remains 377)
[viewport margins: QMargins(0, 0, 0, 0), viewport_geometry: QRect(0,0
365x377), scrollarea_geometry: QRect(0,80 381x377)]
... VerticalScrollArea::eventFilter(QObject*, QEvent*) - Child is resizing to
QRect(0,0 365x377)
... VerticalScrollArea::eventFilter(QObject*, QEvent*) - Child is resizing to
QRect(0,0 381x389)
... VerticalScrollArea::resetSizeLimits() - Child widget resized to QRect(0,0
381x389); setting VerticalScrollArea minimum width to 140 (124 for widget,
16 for scrollbar); setting minimum height to 100; setting maximum height to
389 ([scrollbar active] widget's width 381 -> not narrowed -> max height
remains 389) [viewport margins: QMargins(0, 0, 0, 0), viewport_geometry:
QRect(0,0 365x377), scrollarea_geometry: QRect(0,80 381x377)]
... VerticalScrollArea::eventFilter(QObject*, QEvent*) - Child is resizing to
QRect(0,0 381x389)
... VerticalScrollArea::eventFilter(QObject*, QEvent*) - Child is resizing to
QRect(0,0 365x377)
... VerticalScrollArea::eventFilter(QObject*, QEvent*) - Child is resizing to
QRect(0,0 381x389)
... VerticalScrollArea::resetSizeLimits() - Child widget resized to QRect(0,0
381x389); setting VerticalScrollArea minimum width to 140 (124 for widget,
16 for scrollbar); setting minimum height to 100; setting maximum height to
389 ([scrollbar active] widget's width 381 -> not narrowed -> max height
remains 389) [viewport margins: QMargins(0, 0, 0, 0), viewport_geometry:
QRect(0,0 365x377), scrollarea_geometry: QRect(0,80 381x377)]
... VerticalScrollArea::eventFilter(QObject*, QEvent*) - Child is resizing to
QRect(0,0 381x389)
... VerticalScrollArea::eventFilter(QObject*, QEvent*) - Child is resizing to
QRect(0,0 365x377)
... VerticalScrollArea::eventFilter(QObject*, QEvent*) - Child is resizing to
QRect(0,0 381x389)
... VerticalScrollArea::resetSizeLimits() - Child widget resized to QRect(0,0
381x389); setting VerticalScrollArea minimum width to 140 (124 for widget,
16 for scrollbar); setting minimum height to 100; setting maximum height to
389 ([scrollbar active] widget's width 381 -> not narrowed -> max height
remains 389) [viewport margins: QMargins(0, 0, 0, 0), viewport_geometry:
QRect(0,0 365x377), scrollarea_geometry: QRect(0,80 381x377)]
...
... VerticalScrollArea::resetSizeLimits() - Child widget resized to QRect(0,0
365x377); setting VerticalScrollArea minimum width to 140 (124 for widget,
16 for scrollbar); setting minimum height to 100; setting maximum height to
377 ([scrollbar inactive] widget's width 365 -> not narrowed -> max height
remains 377) [viewport margins: QMargins(0, 0, 0, 0), viewport_geometry:
QRect(0,0 365x377), scrollarea_geometry: QRect(0,80 381x377)]
... VerticalScrollArea::resetSizeLimits() - Child widget resized to
QRect(0,0 365x377); setting VerticalScrollArea minimum width to 140 (124
for widget, 16 for scrollbar); setting minimum height to 100; setting
maximum height to 377 ([scrollbar inactive] widget's width 365 -> not
narrowed -> max height remains 377) [viewport margins:
QMargins(0, 0, 0, 0), viewport_geometry: QRect(0,0 365x377),
scrollarea_geometry: QRect(0,80 381x377)]
...
... VerticalScrollArea::resetSizeLimits() - Child widget resized to
QRect(0,0 381x389); setting VerticalScrollArea minimum width to 140 (124
for widget, 16 for scrollbar); setting minimum height to 100; setting
maximum height to 389 ([scrollbar active] widget's width 381 -> not
narrowed -> max height remains 389) [viewport margins:
QMargins(0, 0, 0, 0), viewport_geometry: QRect(0,0 365x377),
scrollarea_geometry: QRect(0,80 381x377)]
...
That is, we're flitting between 365x377 and 381x389.
This is with a photo trying to maintain its aspect ratio.
So, presumably, we have a state where it's equally happy (or unhappy) with
wwwwwwwwwwww wwwwwwww ss
wwwwwwwwwwww wwwwwwww ss
wwwwwwwwwwww wwwwwwww ss
wwwwwwwwwwww wwwwwwww ss
wwwwwwwwwwww
wwwwwwwwwwww
or something like that.
---
2017-05-08: still getting situations where there's enough space but the
contained widget is being scrolled. Here's an example, from the RAND-36, p3:
VerticalScrollArea<0x0000000004dc9a40 'questionnaire_background_clinician'>,
visible, pos[DOWN] (0, 79), size[DOWN] (1920 x 640),
hasHeightForWidth()[UP] false, heightForWidth(1920)[UP] -1,
minimumSize (901 x 100), maximumSize (16777215 x 740),
sizeHint[UP] (1843 x 640), minimumSizeHint[UP] (57 x 57),
sizePolicy[UP] (Expanding, Expanding) [hasHeightForWidth=false]...
VerticalScrollAreaViewport<0x0000000004d81b80 ''>, visible,
pos[DOWN] (0, 0), size[DOWN] (1904 x 640),
hasHeightForWidth()[UP] false, heightForWidth(1904)[UP] -1,
minimumSize (0 x 0), maximumSize (16777215 x 16777215),
sizeHint[UP] (-1 x -1), minimumSizeHint[UP] (-1 x -1),
sizePolicy[UP] (Preferred, Preferred) [hasHeightForWidth=false]...
... Non-layout children of
VerticalScrollAreaViewport<0x0000000004d81b80 ''>:
BaseWidget<0x0000000004cf6680 ''>, visible, pos[DOWN] (0, 0),
size[DOWN] (1904 x 740), hasHeightForWidth()[UP] true,
heightForWidth(1904)[UP] 640, minimumSize (0 x 0),
maximumSize (16777215 x 16777215), sizeHint[UP] (1827 x 740),
minimumSizeHint[UP] (885 x 740), sizePolicy[UP] (Preferred,
Preferred) [hasHeightForWidth=false]...
Note particularly:
VerticalScrollAreaViewport size[DOWN] (1904 x 640),
hasHeightForWidth()[UP] false
BaseWidget size[DOWN] (1904 x 740), hasHeightForWidth()[UP] true,
heightForWidth(1904)[UP] 640, sizeHint[UP] (1827 x 740)
So the BaseWidget is being made TOO BIG vertically (for width 1904 it wants
height 640 and is being given 740, even though the viewport is 640).
Unfortunately, our VerticalScrollAreaViewport::resizeEvent() is not being
called. The problem may lie in QAbstractScrollAreaPrivate::layoutChildren(),
which, of course, is not something that's virtual and amenable to overriding.
As above.
Can we catch VerticalScrollArea::resizeEvent() and manually call
VerticalScrollAreaViewport::resizeSingleChild()?
*/
// ============================================================================
// Constructor
// ============================================================================
VerticalScrollArea::VerticalScrollArea(QWidget* parent) :
QScrollArea(parent),
m_last_widget_width(-1),
m_reentry_depth(0)
{
// ------------------------------------------------------------------------
// Viewport: change from the default
// ------------------------------------------------------------------------
#ifdef USE_CUSTOM_VIEWPORT
auto vp = new VerticalScrollAreaViewport();
#ifdef USE_STRETCH
vp->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
#endif
setViewport(vp);
#endif
// ------------------------------------------------------------------------
// Sizing
// ------------------------------------------------------------------------
setWidgetResizable(true);
// ... definitely true! If false, you get a narrow strip of widgets
// instead of them expanding to the full width.
// Vertical scroll bar if required:
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
#ifdef VANISHING_SCROLLBAR
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
#else
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
#endif
#ifdef RESIZE_FOR_HFW
// RNC addition:
// setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
// ... see notes at end
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// ... even better, when also we set our maximum height upon widget resize?
// NOT THIS: enlarges the scroll area rather than scrolling...
// setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
// Do NOT make VerticalScrollArea height-for-width itself.
// setSizePolicy(sizehelpers::expandingExpandingHFWPolicy());
// NOT THIS: doesn't work
// setSizePolicy(UiFunc::expandingMaximumHFWPolicy());
setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
// https://doc.qt.io/qt-6.5/qabstractscrollarea.html#SizeAdjustPolicy-enum
#else
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setSizeAdjustPolicy(QAbstractScrollArea::AdjustIgnored);
#endif
// ------------------------------------------------------------------------
// For scroll-by-swipe:
// ------------------------------------------------------------------------
#ifdef TOUCHSCROLL_DIRECT
// DOES NOT WORK:
setAttribute(Qt::WA_AcceptTouchEvents);
grabGesture(Qt::SwipeGesture); // will arrive via event()
// Note that mouse "gestures" are not supported. They can be manually
// calculated/simulated; see
// https://doc.qt.io/archives/qq/qq18-mousegestures.html
// https://forum.qt.io/topic/27422/solved-qswipegesture-implementation-on-desktop/4
#endif
#ifdef TOUCHSCROLL_SCROLLER
uifunc::applyScrollGestures(viewport());
#endif
#ifdef TOUCHSCROLL_FLICKCHARM
// DOESN'T WORK (well? at all?)
FlickCharm charm;
charm.activateOn(this);
#endif
}
// ============================================================================
// Resizing
// ============================================================================
void VerticalScrollArea::setWidget(QWidget* widget) // hides parent version
{
#ifdef USE_STRETCH
auto bw = new BaseWidget();
auto layout = new VBoxLayout();
bw->setLayout(layout);
layout->addWidget(widget);
layout->addStretch();
QScrollArea::setWidget(bw);
#else
QScrollArea::setWidget(widget);
#endif
}
void VerticalScrollArea::resizeEvent(QResizeEvent* event)
{
#ifdef DEBUG_LAYOUT
qDebug() << Q_FUNC_INFO << event->size();
#endif
QScrollArea::resizeEvent(event); // doesn't actually do anything?
#ifdef RESIZE_FOR_HFW
// sizehelpers::resizeEventForHFWParentWidget(this);
#endif
#ifdef USE_CUSTOM_VIEWPORT
#ifdef DEBUG_VIEWPORT_CHILD_SIZE
QWidget* w = viewport();
VerticalScrollAreaViewport* vp
= dynamic_cast<VerticalScrollAreaViewport*>(w);
if (vp) {
vp->checkChildSize();
}
#endif
#endif
}
bool VerticalScrollArea::eventFilter(QObject* o, QEvent* e)
{
// This deals with the "owned" widget changing size.
// Return true for "I've dealt with it; nobody else should".
// https://doc.qt.io/qt-6.5/eventsandfilters.html
// We use eventFilter(), not event(), because we are looking for events on
// the widget that we are scrolling, not our own widget.
// This works because QScrollArea::setWidget installs an eventFilter on the
// widget.
if (o && o == widget() && e && e->type() == QEvent::Resize) {
auto w = dynamic_cast<QWidget*>(o);
#ifdef DEBUG_LAYOUT
qDebug() << Q_FUNC_INFO << "- Child is resizing to" << w->geometry();
#endif
const bool skip = w->size() == m_widget_size_back_1
|| w->size() == m_widget_size_back_2;
m_widget_size_back_2 = m_widget_size_back_1;
m_widget_size_back_1 = w->size();
if (skip) {
#ifdef DEBUG_LAYOUT
qDebug() << "Size matches 1-back or 2-back; stopping";
#endif
return false;
}
#ifdef RESIZE_FOR_HFW
// --------------------------------------------------------------------
// Prevent infinite recursion
// --------------------------------------------------------------------
if (m_reentry_depth >= widgetconst::SET_GEOMETRY_MAX_REENTRY_DEPTH) {
#ifdef DEBUG_LAYOUT
qDebug() << Q_FUNC_INFO
<< "- ... recursion depth exceeded; ignoring";
#endif
return false;
}
ReentryDepthGuard guard(m_reentry_depth);
Q_UNUSED(guard)
const bool parent_result = QScrollArea::eventFilter(o, e);
// ... will call d->updateScrollBars();
resetSizeLimits();
return parent_result;
#else
return QScrollArea::eventFilter(o, e);
#endif
}
#ifdef DEBUG_IRRELEVANT_EVENTS
/*
Beware this almost-infinite loop (read from bottom to top):
#29 0x00000000005b6b6c in VerticalScrollArea::eventFilter (this=0x33e54d0,
o=0x3345140, e=0x7fffffffaac0)
at ../tablet_qt/widgets/verticalscrollarea.cpp:321
#30 0x000000000140f7d2 in QCoreApplicationPrivate::
sendThroughObjectEventFilters(QObject*, QEvent*) ()
#31 0x0000000000837e35 in QApplicationPrivate::notify_helper(QObject*,
QEvent*) ()
#32 0x000000000083f45c in QApplication::notify(QObject*, QEvent*) ()
#33 0x000000000140faf8 in QCoreApplication::notifyInternal2(QObject*,
QEvent*) ()
#34 0x0000000000867a47 in QWidgetPrivate::setGeometry_sys(int, int, int,
int, bool) ()
#35 0x0000000000867f34 in QWidget::resize(QSize const&) ()
#36 0x0000000000979244 in QScrollAreaPrivate::updateScrollBars() ()
#37 0x0000000000979e19 in QScrollArea::eventFilter(QObject*, QEvent*) ()
#38 0x00000000005b6b6c in VerticalScrollArea::eventFilter (this=0x33e54d0,
o=0x3345140, e=0x7fffffffaf10)
at ../tablet_qt/widgets/verticalscrollarea.cpp:321
*/
qDebug() << Q_FUNC_INFO << "- event type:" << e->type();
#endif
return QScrollArea::eventFilter(o, e);
}
// RNC addition:
// Without this (and a vertical size policy of Maximum), it's very hard to
// get the scroll area to avoid one of the following:
// - expand too large vertically; distribute its contents vertically; thus
// need an internal spacer at the end of its contents; thus have a duff
// endpoint;
// - be too small vertically (e.g. if a spacer is put below it to prevent it
// expanding too much) when there is vertical space available to use.
// So the answer is a Maximum(*) vertical size policy, and a size hint that is
// exactly that of its contents.
// (*) Or Expanding with an explicit maximum set.
QSize VerticalScrollArea::sizeHint() const
{
// "Q. How big would you *like* to be?"
// "A. The size my widget wants to be (or is), so my scroll bars can
// disappear."
// ... although we also have a small margin to deal with, even when
// scrollbars have gone.
QWidget* w = widget();
if (!w) {
return QSize();
}
// Work out best size for widget
QSize sh = w->sizeHint();
if (w->hasHeightForWidth()) {
int widget_working_width;
if (m_last_widget_width == -1) {
// First time through
const int widget_preferred_width = sh.width();
widget_working_width = widget_preferred_width;
} else {
// The widget's not (necessarily) getting its preferred width, but
// we have an idea of the width that it *is* getting.
widget_working_width = m_last_widget_width;
}
const int likely_best_height = w->heightForWidth(widget_working_width);
sh.rheight() = likely_best_height;
}
#ifndef VANISHING_SCROLLBAR
int scrollbar_width = verticalScrollBar()->width();
sh.rwidth() += scrollbar_width;
#endif
// Correct for our margins
const QRect viewport_rect = viewport()->geometry();
const QRect scrollarea_rect = geometry();
const Margins marg
= Margins::subRectMargins(scrollarea_rect, viewport_rect);
sh.rheight() += marg.totalHeight();
#ifdef DEBUG_LAYOUT
qDebug().nospace() << Q_FUNC_INFO << " - [widget sizeHint() "
<< w->sizeHint() << "] -> final sizeHint() " << sh;
#endif
return sh;
}
void VerticalScrollArea::resetSizeLimits()
{
// We get here when our child widget resizes.
// We use this code plus the Expanding policy.
QWidget* w = widget(); // The contained widget being scrolled.
if (!w) {
return;
}
const bool widget_has_hfw = w->hasHeightForWidth();
QScrollBar* vsb = verticalScrollBar();
if (!vsb) {
qWarning() << "No vertical scrollbar!";
return;
}
// The widget size coming here might be this (w widget, s scrollbar):
//
// www ss
// www ss
// www ss
// www ss
//
// or this:
//
// wwww
// wwww
// wwww
//
// Can we distinguish these?
#ifdef HFW_METHOD_1
const int scrollbar_width = vsb->width();
#endif
#ifdef DEBUG_LAYOUT
const int vsb_value = vsb->value();
const bool scrollbar_active
= vsb_value != vsb->minimum() || vsb_value != vsb->maximum();
const QString hfw_explanation
= scrollbar_active ? "[scrollbar active] " : "[scrollbar inactive] ";
#endif
// And: in this example, we want (I think) our minimum width to be 4,
// which is either width + scrollbar (if present), or width (if absent),
// ... for HFW widgets.
const int widget_width = w->geometry().width();
const int widget_min_width = qMax(0, w->minimumSizeHint().width());
int widget_min_height;
int widget_max_height;
if (widget_has_hfw) {
// ====================================================================
// HFW
// ====================================================================
#ifdef DEBUG_LAYOUT
hfw_explanation += QString("[widget has HFW] ");
#endif
m_last_widget_width = widget_width;
#ifdef HFW_METHOD_1
// Calculate widget_max_height
if (false) {
// It is quite likely that the widget is now a sensible width for
// us without scroll bars - so when we add scroll bars, it'll get
// narrower and thus taller. If we don't account for these, our
// scroll area will often be a fraction too short vertically.
int narrower_widget_width
= qMax(1, widget_width - scrollbar_width);
widget_max_height = w->heightForWidth(narrower_widget_width);
#ifdef DEBUG_LAYOUT
hfw_explanation += QString(
"widget's width %1 -> narrowed %2 in "
"case scrollbars added -> HFW %3"
)
.arg(widget_width)
.arg(narrower_widget_width)
.arg(widget_max_height);
#endif
} else {
widget_max_height = w->heightForWidth(widget_width);
#ifdef DEBUG_LAYOUT
hfw_explanation += QString(
"widget's width %1 -> not narrowed -> "
"max height remains %2"
)
.arg(widget_width)
.arg(widget_max_height);
#endif
}
// Calculate widget_min_height
// For height-for-width widgets, minimumSizeHint().height() may be
// misleading.
int widget_max_width = qMin(w->maximumWidth(), QWIDGETSIZE_MAX);
widget_min_height = qMin(
w->heightForWidth(widget_min_width),
qMin(
w->heightForWidth(widget_width),
w->heightForWidth(widget_max_width)
)
);
#ifdef DEBUG_LAYOUT
hfw_explanation
+= QString(
"; widget HFWs: min_width %1 -> %2; width %3 -> %4; "
"max_width %5 -> %6; overall widget_min_height %7"
)
.arg(widget_min_width)
.arg(w->heightForWidth(widget_min_width))
.arg(widget_width)
.arg(w->heightForWidth(widget_width))
.arg(widget_max_width)
.arg(w->heightForWidth(widget_max_width))
.arg(widget_min_height);
#endif
#else
const int widget_hfw_height = w->heightForWidth(widget_width);
widget_min_height = widget_hfw_height;
widget_max_height = widget_hfw_height;
#endif
} else {
// ====================================================================
// Not HFW
// ====================================================================
// Minimum height: if the widget is small, then the widget height (3 in
// this example, i.e. without scrollbars), but if it's large, then
// SQUASH_DOWN_TO_HEIGHT.
//
// Rephrased:
// Vertically, the scroller can get as SMALL as the widget if that's
// less than SQUASH_DOWN_TO_HEIGHT, but if the widget is bigger, the
// MINIMUM size of the scroller can be something small, if the window
// is small, i.e. SQUASH_DOWN_TO_HEIGHT.
// But it's also possible that the widget's minimum height is -1, which
// we'll translate to 0.
widget_min_height = w->minimumSizeHint().height();
// Maximum height: that with scrollbars (4 in this example), for HFW
// widgets.
widget_max_height = w->maximumHeight();
#ifdef DEBUG_LAYOUT
hfw_explanation += QString("[widget does not have HFW] ");
hfw_explanation
+= QString("widget's maximum height is %1").arg(widget_max_height);
#endif
}
int new_min_width = widget_min_width;
const int new_min_height
= qBound(0, widget_min_height, SQUASH_DOWN_TO_HEIGHT);
int new_max_height = widget_max_height;
// The only other odd bit is that VerticalScrollArea can position its
// qt_scrollarea_viewport widget at e.g. pos (1, 1), not (0, 0), so our
// maximum height is a little too small.
// Basically, there is small boundary, as above.
const QRect viewport_rect = viewport()->geometry();
const QRect scrollarea_rect = geometry();
const Margins marg
= Margins::subRectMargins(scrollarea_rect, viewport_rect);
new_min_width += marg.totalWidth();
new_max_height += marg.totalHeight();
#ifdef DEBUG_LAYOUT
QMargins viewport_margins = viewportMargins(); // zero!
qDebug().nospace().noquote()
<< Q_FUNC_INFO << " - Child widget resized to " << w->geometry()
<< "; setting VerticalScrollArea minimum width to " << new_min_width
<< " (" << widget_min_width << " for widget, " << marg.totalWidth()
<< " for margins)"
<< "; setting minimum height to " << new_min_height
<< "; setting maximum height to " << new_max_height << " ("
<< hfw_explanation << ") "
<< "[viewport margins: " << viewport_margins
<< ", viewport_geometry: " << viewport_rect
<< ", scrollarea_geometry: " << scrollarea_rect << "]";
qDebug() << Q_FUNC_INFO
<< "Child widget:" << layoutdumper::getWidgetInfo(w);
#endif
const bool change = new_min_width != minimumWidth()
|| new_min_height != minimumHeight()
|| new_max_height != maximumHeight();
if (!change) {
return;
}
// --------------------------------------------------------------------
// Prevent infinite recursion
// --------------------------------------------------------------------
if (m_reentry_depth >= widgetconst::SET_GEOMETRY_MAX_REENTRY_DEPTH) {
return;
}
ReentryDepthGuard guard(m_reentry_depth);
Q_UNUSED(guard)
// We're not doing horizontal scrolling, so we must be at least as wide
// as our widget's minimum:
setMinimumWidth(new_min_width);
// We don't have a maximum width; we'll expand as required.
// We do NOT allow our *minimum* height to be determined by the widget.
// If the widget's minimum height is very big, well, we'll scroll.
// If it's tiny, though, we'll respect it and not go bigger.
setMinimumHeight(new_min_height);
// We don't want to be any taller than the maximum space our widget wants
// (plus our margins).
setMaximumHeight(new_max_height);
// If the scrollbox starts out small (because its contents are small),
// and the contents grow, we will learn about it here -- and we need
// to grow ourselves. When your sizeHint() changes, you should call
// updateGeometry().
// Except...
// https://doc.qt.io/qt-6.5/qwidget.html
// Warning: Calling setGeometry() inside resizeEvent() or moveEvent()
// can lead to infinite recursion.
// ... and we certainly had infinite recursion.
// One way in which this can happen:
// http://stackoverflow.com/questions/9503231
updateGeometry();
// Do NOT attempt to invalidate the parent widget's layout here.
// PREVIOUS RESIDUAL PROBLEM:
// - On some machines (e.g. wombat, Linux), when a multiline text box
// within a smaller-than-full-screen VerticalScroll area grows, the
// VerticalScrollBox stays the same size but its scroll bar adapts
// to the contents. Not ideal.
// - On other machines (e.g. shrike, Linux), the VerticalScrollArea
// also grows, until it needs to scroll. This is optimal.
// - Adding an updateGeometry() call fixed the problem on wombat.
// - However, it caused a crash via infinite recursion on shrike,
// because (I think) the VerticalScrollBar's updateGeometry() call
// triggered similar geometry updating in the contained widgets (esp.
// LabelWordWrapWide), which triggered an update for the
// VerticalScrollBar, which...
// - So, better to be cosmetically imperfect than to crash.
// - Not sure if this can be solved consistently and perfectly.
// - Try a guard (m_updating_geometry) so it can only do this once.
// Works well on Wombat!
}
// ============================================================================
// Swipe to scroll
// ============================================================================
bool VerticalScrollArea::event(QEvent* event)
{
#ifdef TOUCHSCROLL_DIRECT
// https://doc.qt.io/qt-6.5/gestures-overview.html
if (event->type() == QEvent::Gesture) {
return gestureEvent(static_cast<QGestureEvent*>(event));
}
#endif
return QScrollArea::event(event);
}
bool VerticalScrollArea::gestureEvent(QGestureEvent* event)
{
#ifdef TOUCHSCROLL_DIRECT
qDebug() << Q_FUNC_INFO;
// https://doc.qt.io/qt-6.5/gestures-overview.html
if (QGesture* swipe = event->gesture(Qt::SwipeGesture)) {
swipeTriggered(static_cast<QSwipeGesture*>(swipe));
}
#else
Q_UNUSED(event)
#endif
return true;
}
void VerticalScrollArea::swipeTriggered(QSwipeGesture* gesture)
{
#ifdef TOUCHSCROLL_DIRECT
qDebug() << Q_FUNC_INFO;
if (gesture->state() == Qt::GestureUpdated) {
const bool up = gesture->verticalDirection() == QSwipeGesture::Up;
const bool down = gesture->verticalDirection() == QSwipeGesture::Down;
if (up || down) {
int dy = 50;
if (down) {
dy = -dy;
}
scroll(0, dy); // dx, dy
}
}
#else
Q_UNUSED(gesture)
#endif
}