15.1.205. tablet_qt/lib/layoutdumper.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/>.
*/

#include "layoutdumper.h"

#include <QDebug>
#include <QScrollArea>
#include <QString>
#include <QStringBuilder>
#include <QtWidgets/QLayout>
#include <QtWidgets/QWidget>

#include "lib/convert.h"
#include "lib/sizehelpers.h"
#include "lib/stringfunc.h"

namespace layoutdumper {

const QString NULL_WIDGET_STRING("<null_widget>");

QString toString(const QSizePolicy::Policy& policy)
{
    switch (policy) {
        case QSizePolicy::Fixed:
            return "Fixed";
        case QSizePolicy::Minimum:
            return "Minimum";
        case QSizePolicy::Maximum:
            return "Maximum";
        case QSizePolicy::Preferred:
            return "Preferred";
        case QSizePolicy::MinimumExpanding:
            return "MinimumExpanding";
        case QSizePolicy::Expanding:
            return "Expanding";
        case QSizePolicy::Ignored:
            return "Ignored";
    }
    return "unknown_QSizePolicy";
}

QString toString(const QSizePolicy& policy)
{
    QString result = QString("(%1, %2) [hasHeightForWidth=%3]")
                         .arg(
                             toString(policy.horizontalPolicy()),
                             toString(policy.verticalPolicy()),
                             toString(policy.hasHeightForWidth())
                         );
    return result;
}

QString toString(const QLayout::SizeConstraint constraint)
{
    switch (constraint) {
        case QLayout::SetDefaultConstraint:
            return "SetDefaultConstraint";
        case QLayout::SetNoConstraint:
            return "SetNoConstraint";
        case QLayout::SetMinimumSize:
            return "SetMinimumSize";
        case QLayout::SetFixedSize:
            return "SetFixedSize";
        case QLayout::SetMaximumSize:
            return "SetMaximumSize";
        case QLayout::SetMinAndMaxSize:
            return "SetMinAndMaxSize";
    }
    return "unknown_SizeConstraint";
}

QString toString(const Qt::Alignment& alignment)
{
    QStringList elements;

    if (alignment & Qt::AlignLeft) {
        elements.append("AlignLeft");
    }
    if (alignment & Qt::AlignRight) {
        elements.append("AlignRight");
    }
    if (alignment & Qt::AlignHCenter) {
        elements.append("AlignHCenter");
    }
    if (alignment & Qt::AlignJustify) {
        elements.append("AlignJustify");
    }
    if (alignment & Qt::AlignAbsolute) {
        elements.append("AlignAbsolute");
    }
    if ((alignment & Qt::AlignHorizontal_Mask) == 0) {
        elements.append("<horizontal_none>");
    }

    if (alignment & Qt::AlignTop) {
        elements.append("AlignTop");
    }
    if (alignment & Qt::AlignBottom) {
        elements.append("AlignBottom");
    }
    if (alignment & Qt::AlignVCenter) {
        elements.append("AlignVCenter");
    }
    if (alignment & Qt::AlignBaseline) {
        elements.append("AlignBaseline");
    }
    if ((alignment & Qt::AlignVertical_Mask) == 0) {
        elements.append("<vertical_none>");
    }

    return elements.join(" | ");
}

QString toString(const void* pointer)
{
    return convert::prettyPointer(pointer);
}

QString toString(const bool boolean)
{
    return boolean ? "true" : "false";
}

QString getWidgetDescriptor(const QWidget* w)
{
    if (!w) {
        return NULL_WIDGET_STRING;
    }
    return QString("%1<%2 '%3'>")
        .arg(
            w->metaObject()->className(),
            toString(reinterpret_cast<const void*>(w)),
            w->objectName()
        );
}

QString getWidgetInfo(const QWidget* w, const DumperConfig& config)
{
    if (!w) {
        return NULL_WIDGET_STRING;
    }

    const QRect& geom = w->geometry();

    // Can't have >9 arguments to QString arg() system.
    // Using QStringBuilder with % leads to more type faff.
    QStringList elements;
    elements.append(getWidgetDescriptor(w));
    elements.append(w->isVisible() ? "visible" : "HIDDEN");
    elements.append(QString("pos[DOWN] (%1, %2)").arg(geom.x()).arg(geom.y()));
    elements.append(
        QString("size[DOWN] (%1 x %2)").arg(geom.width()).arg(geom.height())
    );
    elements.append(QString("hasHeightForWidth()[UP] %1")
                        .arg(w->hasHeightForWidth() ? "true" : "false"));
    elements.append(QString("heightForWidth(%1[DOWN])[UP] %2")
                        .arg(geom.width())
                        .arg(w->heightForWidth(geom.width())));
    elements.append(QString("minimumSize (%1 x %2)")
                        .arg(w->minimumSize().width())
                        .arg(w->minimumSize().height()));
    elements.append(QString("maximumSize (%1 x %2)")
                        .arg(w->maximumSize().width())
                        .arg(w->maximumSize().height()));
    elements.append(QString("sizeHint[UP] (%1 x %2)")
                        .arg(w->sizeHint().width())
                        .arg(w->sizeHint().height()));
    elements.append(QString("minimumSizeHint[UP] (%1 x %2)")
                        .arg(w->minimumSizeHint().width())
                        .arg(w->minimumSizeHint().height()));
    elements.append(QString("sizePolicy[UP] %1").arg(toString(w->sizePolicy()))
    );
    elements.append(QString("stylesheet: %1")
                        .arg(w->styleSheet().isEmpty() ? "false" : "true"));

    if (config.show_all_widget_attributes
        || config.show_set_widget_attributes) {
        elements.append(QString("attributes: [%1]")
                            .arg(getWidgetAttributeInfo(
                                w, config.show_all_widget_attributes
                            )));
    }

    if (config.show_widget_properties) {
        const QString properties = getDynamicProperties(w);
        if (!properties.isEmpty()) {
            elements.append(QString("properties: [%1]").arg(properties));
        }
    }

    if (config.show_widget_stylesheets) {
        elements.append(QString("stylesheet contents: %1")
                            .arg(convert::stringToCppLiteral(w->styleSheet()))
        );
    }

    // Geometry within bounds?
    if (geom.width() < w->minimumSize().width()) {
        elements.append("[BUG? geometry().width() < minimumSize().width()]");
    }
    if (geom.height() < w->minimumSize().height()) {
        elements.append("[BUG? geometry().height() < minimumSize().height()]");
    }
    if (geom.width() < w->minimumSizeHint().width()) {
        elements.append(
            "[WARNING: geometry().width() < "
            "minimumSizeHint().width()]"
        );
    }
    if (!w->hasHeightForWidth()
        && geom.height() < w->minimumSizeHint().height()) {
        elements.append(
            "[WARNING: geometry().height() < "
            "minimumSizeHint().height()]"
        );
    }
    if (geom.width() > w->maximumSize().width()) {
        elements.append("[BUG? geometry().width() > maximumSize().width()]");
    }
    if (geom.height() > w->maximumSize().height()) {
        elements.append("[BUG? geometry().height() > maximumSize().height()]");
    }
    if (w->hasHeightForWidth()
        && geom.height() < w->heightForWidth(geom.width())) {
        const bool can_shrink_vertically
            = sizehelpers::canHFWPolicyShrinkVertically(w->sizePolicy());
        if (!can_shrink_vertically) {
            elements.append(
                "[WARNING: geometry().height() < "
                "heightForWidth(geometry().width()) and policy doesn't allow "
                "vertical shrinkage]"
            );
        }
    }

    // Bounds themselves consistent?
    if (w->sizeHint().width() != -1 && w->sizeHint().height() != -1) {
        if (w->sizeHint().width() < w->minimumSizeHint().width()) {
            elements.append(
                "[WIDGET BUG? sizeHint().width() < "
                "minimumSizeHint().width()]"
            );
        }
        /*
        // Not clear that these are wrong; the layout may have other reasons
        // for setting our minimumSize() bigger than our sizeHint().
        if (w->sizeHint().width() < w->minimumSize().width()) {
            elements.append("[BUG? sizeHint().width() < "
                            "minimumSize().width()]");
        }
        if (w->sizeHint().width() > w->maximumSize().width()) {
            elements.append("[BUG? sizeHint().width() > "
                            "maximumSize().width()]");
        }
        */
        if (w->sizeHint().height() < w->minimumSizeHint().height()) {
            elements.append(
                "[WIDGET BUG? sizeHint().height() < "
                "minimumSizeHint().height()]"
            );
        }
        if (!w->hasHeightForWidth()) {
            /*
            if (w->sizeHint().height() < w->minimumSize().height()) {
                elements.append("[BUG? sizeHint().height() < "
                                "minimumSize().height()]");
            }
            if (w->sizeHint().height() > w->maximumSize().height()) {
                elements.append("[BUG? sizeHint().height() > "
                                "maximumSize().height()]");
            }
            */
        }
    }

    return elements.join(", ");
}

#define ADD_WIDGET_ATTR(x) add(Qt::x, #x)

QString getWidgetAttributeInfo(const QWidget* w, const bool all)
{
    // https://doc.qt.io/qt-6.5/qt.html#WidgetAttribute-enum
    if (!w) {
        return NULL_WIDGET_STRING;
    }
    QStringList elements;
    auto add =
        [&all, &w, &elements](Qt::WidgetAttribute attr, const QString& desc) {
            const bool set = w->testAttribute(attr);
            if (all) {
                elements.append(QString("%1 %2").arg(desc, set));
            } else {
                if (set) {
                    elements.append(desc);
                }
            }
        };

    // https://doc.qt.io/qt-6/qt.html#WidgetAttribute-enum
    // ... sorted

    ADD_WIDGET_ATTR(WA_AcceptDrops);
    ADD_WIDGET_ATTR(WA_AcceptTouchEvents);
    ADD_WIDGET_ATTR(WA_AlwaysShowToolTips);
    ADD_WIDGET_ATTR(WA_AlwaysStackOnTop);
    ADD_WIDGET_ATTR(WA_ContentsMarginsRespectsSafeArea);
    ADD_WIDGET_ATTR(WA_CustomWhatsThis);
    ADD_WIDGET_ATTR(WA_DeleteOnClose);
    ADD_WIDGET_ATTR(WA_Disabled);
    ADD_WIDGET_ATTR(WA_DontCreateNativeAncestors);
    ADD_WIDGET_ATTR(WA_DontShowOnScreen);
    ADD_WIDGET_ATTR(WA_ForceDisabled);
    ADD_WIDGET_ATTR(WA_ForceUpdatesDisabled);
    ADD_WIDGET_ATTR(WA_Hover);
    ADD_WIDGET_ATTR(WA_InputMethodEnabled);
    ADD_WIDGET_ATTR(WA_KeyCompression);
    ADD_WIDGET_ATTR(WA_KeyboardFocusChange);
    ADD_WIDGET_ATTR(WA_LayoutOnEntireRect);
    ADD_WIDGET_ATTR(WA_LayoutUsesWidgetRect);
    ADD_WIDGET_ATTR(WA_MacAlwaysShowToolWindow);
    ADD_WIDGET_ATTR(WA_MacMiniSize);
    ADD_WIDGET_ATTR(WA_MacNormalSize);
    ADD_WIDGET_ATTR(WA_MacOpaqueSizeGrip);
    ADD_WIDGET_ATTR(WA_MacShowFocusRect);
    ADD_WIDGET_ATTR(WA_MacSmallSize);
    ADD_WIDGET_ATTR(WA_Mapped);
    ADD_WIDGET_ATTR(WA_MouseNoMask);
    ADD_WIDGET_ATTR(WA_MouseTracking);
    ADD_WIDGET_ATTR(WA_Moved);
    ADD_WIDGET_ATTR(WA_NativeWindow);
    ADD_WIDGET_ATTR(WA_NoChildEventsForParent);
    ADD_WIDGET_ATTR(WA_NoChildEventsFromChildren);
    ADD_WIDGET_ATTR(WA_NoMousePropagation);
    ADD_WIDGET_ATTR(WA_NoMouseReplay);
    ADD_WIDGET_ATTR(WA_NoSystemBackground);
    ADD_WIDGET_ATTR(WA_OpaquePaintEvent);
    ADD_WIDGET_ATTR(WA_OutsideWSRange);
    ADD_WIDGET_ATTR(WA_PaintOnScreen);
    ADD_WIDGET_ATTR(WA_PaintUnclipped);
    ADD_WIDGET_ATTR(WA_PendingMoveEvent);
    ADD_WIDGET_ATTR(WA_PendingResizeEvent);
    ADD_WIDGET_ATTR(WA_QuitOnClose);
    ADD_WIDGET_ATTR(WA_Resized);
    ADD_WIDGET_ATTR(WA_RightToLeft);
    ADD_WIDGET_ATTR(WA_SetCursor);
    ADD_WIDGET_ATTR(WA_SetFont);
    ADD_WIDGET_ATTR(WA_SetLocale);
    ADD_WIDGET_ATTR(WA_SetPalette);
    ADD_WIDGET_ATTR(WA_SetStyle);
    ADD_WIDGET_ATTR(WA_ShowModal);
    ADD_WIDGET_ATTR(WA_ShowWithoutActivating);
    ADD_WIDGET_ATTR(WA_StaticContents);
    ADD_WIDGET_ATTR(WA_StyleSheet);
    ADD_WIDGET_ATTR(WA_StyleSheetTarget);
    ADD_WIDGET_ATTR(WA_StyledBackground);
    ADD_WIDGET_ATTR(WA_TabletTracking);
    ADD_WIDGET_ATTR(WA_TouchPadAcceptSingleTouchEvents);
    ADD_WIDGET_ATTR(WA_TranslucentBackground);
    ADD_WIDGET_ATTR(WA_TransparentForMouseEvents);
    ADD_WIDGET_ATTR(WA_UnderMouse);
    ADD_WIDGET_ATTR(WA_UpdatesDisabled);
    ADD_WIDGET_ATTR(WA_WindowModified);
    ADD_WIDGET_ATTR(WA_WindowPropagation);
    ADD_WIDGET_ATTR(WA_X11DoNotAcceptFocus);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeCombo);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeDND);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeDesktop);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeDialog);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeDock);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeDropDownMenu);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeMenu);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeNotification);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypePopupMenu);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeSplash);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeToolBar);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeToolTip);
    ADD_WIDGET_ATTR(WA_X11NetWmWindowTypeUtility);

    return elements.join(", ");
}

QString getDynamicProperties(const QWidget* w)
{
    if (!w) {
        return NULL_WIDGET_STRING;
    }
    QStringList elements;
    const QList<QByteArray> property_names = w->dynamicPropertyNames();
    for (const QByteArray& arr : property_names) {
        const QString name(arr);
        const QVariant value = w->property(arr);
        const QString value_string
            = stringfunc::escapeString(value.toString());
        elements.append(QString("%1=%2").arg(name, value_string));
    }
    return elements.join(", ");
}

QString getLayoutInfo(const QLayout* layout)
{
    if (!layout) {
        return "null_layout";
    }
    const QMargins margins = layout->contentsMargins();
    const QSize sizehint = layout->sizeHint();
    const QSize minsize = layout->minimumSize();
    const QSize maxsize = layout->maximumSize();
    const QString name = layout->metaObject()->className();
    QWidget* parent = layout->parentWidget();
    // usually unhelpful (blank): layout->objectName()
    QStringList elements;
    elements.append(name);
    elements.append(
        QString("constraint %1").arg(toString(layout->sizeConstraint()))
    );
    elements.append(QString("minimumSize[UP] (%1 x %2)")
                        .arg(minsize.width())
                        .arg(minsize.height()));
    elements.append(QString("sizeHint[UP] (%1 x %2)")
                        .arg(sizehint.width())
                        .arg(sizehint.height()));
    elements.append(QString("maximumSize[UP] (%1 x %2)")
                        .arg(maxsize.width())
                        .arg(maxsize.height()));
    elements.append(QString("hasHeightForWidth[UP] %3")
                        .arg(toString(layout->hasHeightForWidth())));
    elements.append(QString("margin (l=%1,t=%2,r=%3,b=%4)")
                        .arg(margins.left())
                        .arg(margins.top())
                        .arg(margins.right())
                        .arg(margins.bottom()));
    elements.append(QString("spacing[UP] %1").arg(layout->spacing()));

    // Check hints are consistent
    if (sizehint.width() < minsize.width()) {
        elements.append("[BUG? sizeHint().width() < minimumSize().width()]");
    }
    if (sizehint.height() < minsize.height()) {
        elements.append("[BUG? sizeHint().height() < minimumSize().height()]");
    }
    if (sizehint.width() > maxsize.width()) {
        elements.append("[BUG? sizeHint().width() > maximumSize().width()]");
    }
    if (sizehint.height() > maxsize.height()) {
        elements.append("[BUG? sizeHint().height() > maximumSize().height()]");
    }

    // Check parent size is appropriate
    if (parent) {
        const QSize parent_size = parent->size();
        const int parent_width = parent_size.width();
        elements.append(QString("heightForWidth(%1[parent_width])[UP] %2")
                            .arg(parent_width)
                            .arg(layout->heightForWidth(parent_width)));
        elements.append(
            QString("minimumHeightForWidth(%1[parent_width])[UP] %2")
                .arg(parent_width)
                .arg(layout->minimumHeightForWidth(parent_width))
        );
        if (parent_width < minsize.width()) {
            elements.append(
                "[WARNING: parent->size().width() < "
                "minimumSize().width()]"
            );
        }
        if (parent_size.height() < minsize.height()) {
            elements.append(
                "[WARNING: parent->size().height() < "
                "minimumSize().height()]"
            );
        }
    }
    return elements.join(", ");
}

QString getSpacerInfo(QSpacerItem* si)
{
    const QRect& geom = si->geometry();
    const QSize si_hint = si->sizeHint();
    const QLayout* si_layout = si->layout();
    QStringList elements;
    elements.append("QSpacerItem");
    elements.append(QString("pos[DOWN] (%1, %2)").arg(geom.x()).arg(geom.y()));
    elements.append(
        QString("size[DOWN] (%1 x %2)").arg(geom.width()).arg(geom.height())
    );
    elements.append(QString("sizeHint (%1 x %2)")
                        .arg(si_hint.width())
                        .arg(si_hint.height()));
    elements.append(QString("sizePolicy %1").arg(toString(si->sizePolicy())));
    elements.append(QString("constraint %1 [alignment %2]")
                        .arg(
                            si_layout ? toString(si_layout->sizeConstraint())
                                      : "<no_layout>",
                            toString(si->alignment())
                        ));
    return elements.join(", ");
}

QString paddingSpaces(const int level, const int spaces_per_level)
{
    return QString(level * spaces_per_level, ' ');
}

QVector<const QWidget*> dumpLayoutAndChildren(
    QDebug& os,
    const QLayout* layout,
    const int level,
    const DumperConfig& config
)
{
    const QString padding = paddingSpaces(level, config.spaces_per_level);
    const QString next_padding
        = paddingSpaces(level + 1, config.spaces_per_level);
    QVector<const QWidget*> dumped_children;

    os << padding << "Layout: " << getLayoutInfo(layout);

    auto box_layout = dynamic_cast<const QBoxLayout*>(layout);
    if (box_layout) {
        os << ", spacing " << box_layout->spacing();
    }
    os << "\n";

    if (layout->isEmpty()) {
        os << padding << "... empty layout\n";
    } else {
        const int num_items = layout->count();
        for (int i = 0; i < num_items; i++) {
            QLayoutItem* layout_item = layout->itemAt(i);
            QLayout* child_layout = layout_item->layout();
            auto wi = dynamic_cast<QWidgetItem*>(layout_item);
            auto si = dynamic_cast<QSpacerItem*>(layout_item);
            if (wi && wi->widget()) {
                QString alignment = QString(" [alignment from layout: %1]")
                                        .arg(toString(wi->alignment()));
                dumped_children.append(dumpWidgetAndChildren(
                    os, wi->widget(), level + 1, alignment, config
                ));
            } else if (child_layout) {
                dumped_children.append(
                    dumpLayoutAndChildren(os, child_layout, level + 1, config)
                );
            } else if (si) {
                os << next_padding << getSpacerInfo(si) << "\n";
            } else {
                os << next_padding << "<unknown_QLayoutItem>\n";
            }
        }
    }
    return dumped_children;
}

QVector<const QWidget*> dumpWidgetAndChildren(
    QDebug& os,
    const QWidget* w,
    const int level,
    const QString& alignment,
    const DumperConfig& config
)
{
    const QString padding = paddingSpaces(level, config.spaces_per_level);

    os << padding << getWidgetInfo(w, config) << alignment << "\n";

    QVector<const QWidget*> dumped_children;
    dumped_children.append(w);

    QLayout* layout = w->layout();
    if (layout) {
        dumped_children.append(
            dumpLayoutAndChildren(os, layout, level + 1, config)
        );
    }

    // Scroll areas contain but aren't necessarily the parents of their widgets
    // However, they contain a 'qt_scrollarea_viewport' widget that is.
    auto scroll = dynamic_cast<const QScrollArea*>(w);
    if (scroll) {
        dumped_children.append(dumpWidgetAndChildren(
            os, scroll->viewport(), level + 1, "", config
        ));
    }

    // now output any child widgets that weren't dumped as part of the layout
    QList<QWidget*> widgets
        = w->findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
    // Search options: FindDirectChildrenOnly or FindChildrenRecursively.
    QVector<QWidget*> undumped_children;
    foreach (QWidget* child, widgets) {
        if (!dumped_children.contains(child)) {
            undumped_children.push_back(child);
        }
    }
    if (!undumped_children.empty()) {
        os << padding << "... Non-layout children of "
           << getWidgetDescriptor(w) << ":\n";
        foreach (QWidget* child, undumped_children) {
            dumped_children.append(
                dumpWidgetAndChildren(os, child, level + 1, "", config)
            );
        }
    }
    return dumped_children;
}

void dumpWidgetHierarchy(const QWidget* w, const DumperConfig& config)
{
    QDebug os = qDebug().noquote().nospace();
    os << "WIDGET HIERARCHY:\n";
    if (config.use_ultimate_parent) {
        w = ultimateParentWidget(w);
    }
    dumpWidgetAndChildren(os, w, 0, "", config);
}

const QWidget* ultimateParentWidget(const QWidget* w)
{
    while (const QWidget* parent = w->parentWidget()) {
        w = parent;
    }
    return w;
}


}  // namespace layoutdumper