15.1.176. tablet_qt/layouts/widgetitemhfw.cpp

/*
    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_SET_GEOMETRY

// #define DISABLE_SET_GEOMETRY  // for debugging

#include "widgetitemhfw.h"
#include <QDebug>
#include <QStyle>
#include <QWidget>
#include "lib/sizehelpers.h"


// ============================================================================
// Constants
// ============================================================================

const int IGNORE_SIZEHINT = QSizePolicy::IgnoreFlag;
const int CAN_SHRINK = QSizePolicy::ShrinkFlag;
const int CAN_GROW =
        QSizePolicy::GrowFlag |
        QSizePolicy::ExpandFlag |
        QSizePolicy::IgnoreFlag;
const int WANTS_TO_GROW =
        QSizePolicy::ExpandFlag |
        QSizePolicy::IgnoreFlag;


// ============================================================================
// WidgetItemHfw
// ============================================================================

WidgetItemHfw::WidgetItemHfw(QWidget* widget) :
    QWidgetItemV2(widget)
{
}


QSize WidgetItemHfw::sizeHint() const
{
    // Simpler than QWidgetItem. It doesn't support any of the internal
    // margin nonsense (we leave that to the layout). That is, we ignore:
    // - Qt::WA_LayoutUsesWidgetRect
    // - toLayoutItemSize
    //   - toLayoutItemRect
    //     - QWidgetPrivate::leftLayoutItemMargin
    //     - QWidgetPrivate::topLayoutItemMargin
    //     - QWidgetPrivate::rightLayoutItemMargin
    //     - QWidgetPrivate::bottomLayoutItemMargin

    QSize& hint = m_cached_sizehint;  // shorthand
    if (!hint.isValid()) {
        if (isEmpty()) {
            hint = QSize(0, 0);
        } else {
            hint = wid->sizeHint()
                    .expandedTo(wid->minimumSizeHint())
                    .boundedTo(wid->maximumSize())
                    .expandedTo(wid->minimumSize());
            // But we continue to respect "ignore my size hint":
            const QSizePolicy sp = wid->sizePolicy();
            if (sp.horizontalPolicy() & IGNORE_SIZEHINT) {
                hint.setWidth(0);
            }
            if (sp.verticalPolicy() & IGNORE_SIZEHINT) {
                hint.setHeight(0);
            }
        }
#ifdef DEBUG_LAYOUT
        qDebug().nospace()
                << Q_FUNC_INFO
                << " [wid->metaObject()->className() == "
                << wid->metaObject()->className()
                << ", wid->testAttribute(Qt::WA_LayoutUsesWidgetRect) == "
                << wid->testAttribute(Qt::WA_LayoutUsesWidgetRect)
                << ", wid->minimumSize() == " << wid->minimumSize()
                << ", wid->minimumSizeHint() == " << wid->minimumSizeHint()
                << ", wid->sizeHint() == " << wid->sizeHint()
                << ", wid->sizePolicy() == " << wid->sizePolicy()
                << ", wid->sizePolicy().hasHeightForWidth() == "
                << wid->sizePolicy().hasHeightForWidth()
                // << ", wid->sizePolicy().horizontalPolicy() & QSizePolicy::ShrinkFlag == "
                // << (wid->sizePolicy().horizontalPolicy() & QSizePolicy::ShrinkFlag)
                << "]";
        qDebug() << Q_FUNC_INFO << "->" << hint;
#endif
    }
    return hint;
}


QSize WidgetItemHfw::minimumSize() const
{
    // Originals:
    //
    //    class QLayoutItem {
    //        // ...
    //        virtual QSize minimumSize() const = 0;
    //    };
    //
    //    class QWidgetItem : public QLayoutItem { ... };
    //
    //    QSize QWidgetItem::minimumSize() const
    //    {
    //        if (isEmpty())
    //            return QSize(0, 0);
    //        return !wid->testAttribute(Qt::WA_LayoutUsesWidgetRect)
    //               ? toLayoutItemSize(wid->d_func(), qSmartMinSize(this))
    //               : qSmartMinSize(this);
    //    }
    //
    // ... noting that d_func() returns QWidget's "QWidgetPrivate* d_ptr;"
    // ... see https://wiki.qt.io/D-Pointer
    //
    //    class QWidgetItemV2 : public QWidgetItem { ... }
    //
    //    QSize QWidgetItemV2::minimumSize() const
    //    {
    //        if (isEmpty())
    //            return QSize(0, 0);
    //
    //        if (useSizeCache()) {  // RNC: I think generally true
    //            updateCacheIfNecessary();
    //            return q_cachedMinimumSize;
    //        } else {
    //            return QWidgetItem::minimumSize();
    //        }
    //    }
    //
    //    void QWidgetItemV2::updateCacheIfNecessary() const  // RNC: NOT VIRTUAL
    //    {
    //        if (q_cachedMinimumSize.width() != Dirty)
    //            return;
    //
    //        const QSize sizeHint(wid->sizeHint());
    //        const QSize minimumSizeHint(wid->minimumSizeHint());
    //        const QSize minimumSize(wid->minimumSize());
    //        const QSize maximumSize(wid->maximumSize());
    //        const QSizePolicy sizePolicy(wid->sizePolicy());
    //        const QSize expandedSizeHint(sizeHint.expandedTo(minimumSizeHint));
    //
    //        const QSize smartMinSize(qSmartMinSize(sizeHint, minimumSizeHint, minimumSize, maximumSize, sizePolicy));
    //        const QSize smartMaxSize(qSmartMaxSize(expandedSizeHint, minimumSize, maximumSize, sizePolicy, align));
    //
    //        const bool useLayoutItemRect = !wid->testAttribute(Qt::WA_LayoutUsesWidgetRect);
    //
    //        q_cachedMinimumSize = useLayoutItemRect
    //               ? toLayoutItemSize(wid->d_func(), smartMinSize)
    //               : smartMinSize;
    //
    //        q_cachedSizeHint = expandedSizeHint;
    //        q_cachedSizeHint = q_cachedSizeHint.boundedTo(maximumSize)
    //                                           .expandedTo(minimumSize);
    //        q_cachedSizeHint = useLayoutItemRect
    //               ? toLayoutItemSize(wid->d_func(), q_cachedSizeHint)
    //               : q_cachedSizeHint;
    //
    //        if (wid->sizePolicy().horizontalPolicy() == QSizePolicy::Ignored)
    //            q_cachedSizeHint.setWidth(0);
    //        if (wid->sizePolicy().verticalPolicy() == QSizePolicy::Ignored)
    //            q_cachedSizeHint.setHeight(0);
    //
    //        q_cachedMaximumSize = useLayoutItemRect
    //                   ? toLayoutItemSize(wid->d_func(), smartMaxSize)
    //                   : smartMaxSize;
    //    }

    QSize& minsize = m_cached_minsize;  // shorthand
    if (!minsize.isValid()) {
        if (isEmpty()) {
            minsize = QSize(0, 0);
        } else {
            const QSizePolicy sp = wid->sizePolicy();
            if (sp.horizontalPolicy() & IGNORE_SIZEHINT) {
                minsize = QSize(0, 0);
            } else {
                minsize = sizeHint();
                if (sp.horizontalPolicy() & CAN_SHRINK) {
                    minsize.setWidth(0);
                }
                if (sp.verticalPolicy() & CAN_SHRINK) {
                    minsize.setHeight(0);
                }
                minsize = minsize.expandedTo(wid->minimumSize())
                        .expandedTo(wid->minimumSizeHint());
            }
        }
#ifdef DEBUG_LAYOUT
        qDebug() << Q_FUNC_INFO << "->" << minsize;
#endif
    }
    return minsize;
}


QSize WidgetItemHfw::maximumSize() const
{
    QSize& maxsize = m_cached_maxsize;  // shorthand
    if (!maxsize.isValid()) {
        if (isEmpty()) {
            maxsize = QSize(0, 0);
        } else {
            const QSizePolicy sp = wid->sizePolicy();
            if (sp.horizontalPolicy() & IGNORE_SIZEHINT) {
                maxsize = QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
            } else {
                maxsize = sizeHint();
                // Horizontal tweaks:
                if (sp.horizontalPolicy() & CAN_GROW) {
                    maxsize.setWidth(QWIDGETSIZE_MAX);
                }
                // Vertical tweaks:
                if (sp.verticalPolicy() & CAN_GROW) {
                    maxsize.setHeight(QWIDGETSIZE_MAX);
                } else if (hasHeightForWidth()) {
                    // A height-for-width widget that cannot expand vertically
                    // beyond its assigned height.
                    //
                    // For height-for-width widgets, the sizeHint() height
                    // isn't necessarily constraining -- it's the HFW
                    // transformation of the final width that is.
                    // We have two realistic choices:

                    // (a) We don't know, so we don't constrain.
                    maxsize.setHeight(QWIDGETSIZE_MAX);

                    // (b) HFW widgets tend to be "area conserving", in which
                    //     case their height is maximum when their width is
                    //     smallest, or "aspect ratio conserving", in which
                    //     case their height is maximum when their width is
                    //     largest. We could test accordingly:
                    //
                    //          const int h1 = heightForWidth(1);
                    //          const int h2 = heightForWidth(QWIDGETSIZE_MAX);
                    //          const int hmax = qMax(h1, h2);
                    //          maxsize.setHeight(hmax);
                    //
                    //     However, it's not impossible that a widget has
                    //     maximum height at some intermediate width -- there's
                    //     probably a composite widget (e.g. one with its own
                    //     layout) for which this is true. We could do:
                    //
                    // (c) Some sort of iteration through all possible widths,
                    //     or gradient descent, to find the maximum height.
                    //
                    // Let's try (a) for simplicity!
                }
                maxsize = maxsize.boundedTo(wid->maximumSize());
            }
        }
#ifdef DEBUG_LAYOUT
        qDebug() << Q_FUNC_INFO << "->" << maxsize;
#endif
    }
    return maxsize;
}


bool WidgetItemHfw::hasHeightForWidth() const
{
    if (isEmpty()) {
        return false;
    }
    return wid->hasHeightForWidth();
}


int WidgetItemHfw::heightForWidth(int w) const
{
    if (isEmpty()) {
        return -1;
    }
    if (!hasHeightForWidth()) {
        return -1;
    }
    if (!m_width_to_height.contains(w)) {
        const int h = wid->heightForWidth(w);
        m_width_to_height[w] = h;
    }
    return m_width_to_height[w];
}


void WidgetItemHfw::invalidate()
{
    m_cached_sizehint = QSize();
    m_cached_minsize = QSize();
    m_cached_maxsize = QSize();
    m_width_to_height.clear();
}


void WidgetItemHfw::setGeometry(const QRect& rect)
{
#ifdef DISABLE_SET_GEOMETRY
    QWidgetItemV2::setGeometry(rect);
#else
    // Note the problem that QWidgetItem::setGeometry() will mess up
    // height-for-width widgets.
    //
    // ... QLayoutItem::setGeometry()
    // ... overridden by QWidgetItem::setGeometry()
    // ... which does
    //     QSize s = r.size().boundedTo(maximumSize() + widgetRectSurplus);
    // ... which calls QWidgetItem::maximumSize()
    //     ... available for inspection
    // ... which calls qSmartMaxSize() in qlayoutengine.cpp
    //     ... which is probably the problem; under some (common?)
    //         circumstances, if the vertical policy doesn't have
    //         QSizePolicy::GrowFlag set, the maximum height is set to
    //         the sizeHint() height, without any regard to
    //         height-for-width.
    //
    // So we replace it here.
    //
    // WA_LayoutUsesWidgetRect is ignored; may be relevant under MacOS:
    // - https://stackoverflow.com/questions/3978889/why-is-qhboxlayout-causing-widgets-to-overlap
    // - https://stackoverflow.com/questions/41452512/how-to-remove-margins-of-qlayout-completely-mac-os-specific?noredirect=1&lq=1
    // - https://doc.qt.io/archives/qt-4.8/qmacnativewidget.html

#ifdef DEBUG_SET_GEOMETRY
    qDebug() << Q_FUNC_INFO << ": setting layout item geometry to" << rect;
#endif

    if (isEmpty()) {
        // No visible widget (and not an invisible widget retaining its size).
        return;
    }

    // ------------------------------------------------------------------------
    // Set the widget's target size.
    // ------------------------------------------------------------------------
    const QSize available = rect.size();
    QSize widget_size(sizeHint());  // layout item's preferred size
    // ... which in our simplified layout system is also the widget's
    //     preferred size;
    // ... except that this will be (0,0) if the widget's size policy is
    //     "Ignored".
    const QSizePolicy sp = wid->sizePolicy();  // widget's size policy

    // We are trying to get as close as possible to what we were told.
    const bool any_size_widget = !widget_size.isValid() ||
            widget_size == QSize(0, 0);
    // ... e.g. background stripe widgets made from a generic QWidget

    if (sp.horizontalPolicy() & WANTS_TO_GROW ||
            (hasHeightForWidth() && sp.horizontalPolicy() & CAN_GROW) ||
            any_size_widget) {  // e.g. background stripe widgets
        widget_size.setWidth(available.width());
    }
    if (sp.verticalPolicy() & WANTS_TO_GROW ||
            any_size_widget) {
        widget_size.setHeight(available.height());
    }

    // Apply constraints
    widget_size = widget_size
            .expandedTo(minimumSize())
            .boundedTo(maximumSize())
            .boundedTo(available);

#ifdef DEBUG_SET_GEOMETRY
    qDebug() << "... widget_size =" << widget_size;
#endif

    if (hasHeightForWidth()) {
        // Redo the height as necessary for a height-for-width widget.
        int h = heightForWidth(widget_size.width());
#ifdef DEBUG_SET_GEOMETRY
        qDebug() << "... HFW: width" << widget_size.width() << "-> height" << h;
#endif
        if (sp.verticalPolicy() & WANTS_TO_GROW) {
            h = available.height();
        }
        widget_size.setHeight(h);
        // Re-apply constraints
        widget_size = widget_size
                .expandedTo(minimumSize())
                .boundedTo(maximumSize())
                .boundedTo(available);
#ifdef DEBUG_SET_GEOMETRY
        qDebug().nospace()
                << "minimumSize() = " << minimumSize()
                << ", maximumSize() = " << maximumSize()
                << ", available = " << available;
        qDebug() << "... widget_size (after HFW) =" << widget_size;
#endif
    }

    // ------------------------------------------------------------------------
    // If the widget is smaller than the layout "box", it needs alignment.
    // ------------------------------------------------------------------------
    int x = rect.x();
    int y = rect.y();
    const Qt::Alignment align_horiz = QStyle::visualAlignment(
                wid->layoutDirection(), align);
    if (align_horiz & Qt::AlignRight) {
        // Right align
        x = x + (rect.width() - widget_size.width());
    } else if (!(align_horiz & Qt::AlignLeft)) {
        // Centre align
        x = x + (rect.width() - widget_size.width()) / 2;
    }

    if (align & Qt::AlignBottom) {
        // Bottom align
        y = y + (rect.height() - widget_size.height());
    } else if (!(align & Qt::AlignTop)) {
        // Vertical centre align
        y = y + (rect.height() - widget_size.height()) / 2;
    }

    // ------------------------------------------------------------------------
    // Tell the widget.
    // ------------------------------------------------------------------------
    const QRect widget_geom(x, y, widget_size.width(), widget_size.height());
#ifdef DEBUG_SET_GEOMETRY
    qDebug() << "... calling widget->setGeometry() with " << widget_geom;
#endif
    wid->setGeometry(widget_geom);
#endif
}