From acf1ecb47fdf11413b3e781ecbc244c7fe603df3 Mon Sep 17 00:00:00 2001 From: hjk Date: Fri, 3 May 2024 15:04:54 +0200 Subject: [PATCH] LayoutBuilder: Complete experimental implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds support for inheritance to the existing experimental implementation in tests/manual/layoutbuilder/experimental and gets rid of the tight coupling and qobject_casts in the setter implementations. Plan is to use this (minus "bindings" via *::Id / id()) for utils/layoutbuilder.{h,cpp} later. The "binding" support via id() is still experimental, and in its current version not really useful. A possible idea would be to re-use the Tasking::Storage idea, but it's not quite clear how to expose that "long distance" (i.e. across multiple, unrelated top-level builders). However, this is not used in in current uses of the "old" layoutbuilder, so this is not blocking anything. Some notes: The *Interface hierarchy is not strictly needed, it could directly act on things in the QObject hierarchy but would then need #includes of all "buildable" classes, which can be avoided in the current implementation. Besides, the indirection allows us to tweak and/or add functionailty to the Qt classes in the indirecting code, that does not necessarily have to match 1:1 to the underlyings Qt classes. The std::function based callbacks are quite fat and not functionally needed and could be dropped by "inlining" the relevant bits from typical std::function implementations. However, these invariably seem to end up calling functions through pointers to (ABI-compatible, but) different types, which is for /user/ code formally undefined behavior according to C++11 §5.2.10/6. To avoid a discussion whether doing the same ourselves is tolerable or not, this uses std::function and pays the price of the overhead. Change-Id: I6d40c1bd48cf065fcf211eaff8d9a2298bca20eb Reviewed-by: Reviewed-by: Marcus Tillmanns --- tests/manual/layoutbuilder/v2/CMakeLists.txt | 17 + tests/manual/layoutbuilder/v2/lb.cpp | 935 +++++++++++++++++++ tests/manual/layoutbuilder/v2/lb.h | 511 ++++++++++ tests/manual/layoutbuilder/v2/main.cpp | 79 ++ 4 files changed, 1542 insertions(+) create mode 100644 tests/manual/layoutbuilder/v2/CMakeLists.txt create mode 100644 tests/manual/layoutbuilder/v2/lb.cpp create mode 100644 tests/manual/layoutbuilder/v2/lb.h create mode 100644 tests/manual/layoutbuilder/v2/main.cpp diff --git a/tests/manual/layoutbuilder/v2/CMakeLists.txt b/tests/manual/layoutbuilder/v2/CMakeLists.txt new file mode 100644 index 00000000000..f0d59c37681 --- /dev/null +++ b/tests/manual/layoutbuilder/v2/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.5) + +project(lb LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(lb lb.h lb.cpp main.cpp) + +target_link_libraries(lb PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) diff --git a/tests/manual/layoutbuilder/v2/lb.cpp b/tests/manual/layoutbuilder/v2/lb.cpp new file mode 100644 index 00000000000..0529909dacd --- /dev/null +++ b/tests/manual/layoutbuilder/v2/lb.cpp @@ -0,0 +1,935 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "lb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Layouting { + +// That's cut down qtcassert.{c,h} to avoid the dependency. +#define QTC_STRINGIFY_HELPER(x) #x +#define QTC_STRINGIFY(x) QTC_STRINGIFY_HELPER(x) +#define QTC_STRING(cond) qDebug("SOFT ASSERT: \"%s\" in %s: %s", cond, __FILE__, QTC_STRINGIFY(__LINE__)) +#define QTC_ASSERT(cond, action) if (Q_LIKELY(cond)) {} else { QTC_STRING(#cond); action; } do {} while (0) +#define QTC_CHECK(cond) if (cond) {} else { QTC_STRING(#cond); } do {} while (0) + + +template +XInterface::Implementation *access(const XInterface *x) +{ + return static_cast(x->ptr); +} + +// Setter implementation + +// These are free functions overloaded on the type of builder object +// and setter id. The function implementations are independent, but +// the base expectation is that they will forwards to the backend +// type's setter. + +class FlowLayout : public QLayout +{ +public: + explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1) + : QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing) + { + setContentsMargins(margin, margin, margin, margin); + } + + FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1) + : m_hSpace(hSpacing), m_vSpace(vSpacing) + { + setContentsMargins(margin, margin, margin, margin); + } + + ~FlowLayout() override + { + QLayoutItem *item; + while ((item = takeAt(0))) + delete item; + } + + void addItem(QLayoutItem *item) override { itemList.append(item); } + + int horizontalSpacing() const + { + if (m_hSpace >= 0) + return m_hSpace; + else + return smartSpacing(QStyle::PM_LayoutHorizontalSpacing); + } + + int verticalSpacing() const + { + if (m_vSpace >= 0) + return m_vSpace; + else + return smartSpacing(QStyle::PM_LayoutVerticalSpacing); + } + + Qt::Orientations expandingDirections() const override + { + return {}; + } + + bool hasHeightForWidth() const override { return true; } + + int heightForWidth(int width) const override + { + int height = doLayout(QRect(0, 0, width, 0), true); + return height; + } + + int count() const override { return itemList.size(); } + + QLayoutItem *itemAt(int index) const override + { + return itemList.value(index); + } + + QSize minimumSize() const override + { + QSize size; + for (QLayoutItem *item : itemList) + size = size.expandedTo(item->minimumSize()); + + int left, top, right, bottom; + getContentsMargins(&left, &top, &right, &bottom); + size += QSize(left + right, top + bottom); + return size; + } + + void setGeometry(const QRect &rect) override + { + QLayout::setGeometry(rect); + doLayout(rect, false); + } + + QSize sizeHint() const override + { + return minimumSize(); + } + + QLayoutItem *takeAt(int index) override + { + if (index >= 0 && index < itemList.size()) + return itemList.takeAt(index); + else + return nullptr; + } + +private: + int doLayout(const QRect &rect, bool testOnly) const + { + int left, top, right, bottom; + getContentsMargins(&left, &top, &right, &bottom); + QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + + for (QLayoutItem *item : itemList) { + QWidget *wid = item->widget(); + int spaceX = horizontalSpacing(); + if (spaceX == -1) + spaceX = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal); + int spaceY = verticalSpacing(); + if (spaceY == -1) + spaceY = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical); + int nextX = x + item->sizeHint().width() + spaceX; + if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) { + x = effectiveRect.x(); + y = y + lineHeight + spaceY; + nextX = x + item->sizeHint().width() + spaceX; + lineHeight = 0; + } + + if (!testOnly) + item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + return y + lineHeight - rect.y() + bottom; + } + + int smartSpacing(QStyle::PixelMetric pm) const + { + QObject *parent = this->parent(); + if (!parent) { + return -1; + } else if (parent->isWidgetType()) { + auto pw = static_cast(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } else { + return static_cast(parent)->spacing(); + } + } + + QList itemList; + int m_hSpace; + int m_vSpace; +}; + +/*! + \namespace Layouting + \inmodule QtCreator + + \brief The Layouting namespace contains classes for use with layout builders. +*/ + + +/*! + \class Layouting::LayoutItem + \inmodule QtCreator + + \brief The LayoutItem class represents widgets, layouts, and aggregate + items for use in conjunction with layout builders. + + Layout items are typically implicitly constructed when adding items to a + \c LayoutBuilder instance using \c LayoutBuilder::addItem() or + \c LayoutBuilder::addItems() and never stored in user code. +*/ + +/*! + Constructs a layout item instance representing an empty cell. + */ +LayoutItem::LayoutItem() = default; + +LayoutItem::~LayoutItem() = default; + +LayoutItem::LayoutItem(const LayoutInterface &inner) + : LayoutItem(access(&inner)) +{} + +LayoutItem::LayoutItem(const WidgetInterface &inner) + : LayoutItem(access(&inner)) +{} + + +/*! + \fn template LayoutItem(const T &t) + \internal + + Constructs a layout item proxy for \a t. + + T could be + \list + \li \c {QString} + \li \c {QWidget *} + \li \c {QLayout *} + \endlist +*/ + +// Helpers + + +// Object + +Object::Object(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +static QWidget *widgetForItem(QLayoutItem *item) +{ + if (QWidget *w = item->widget()) + return w; + if (item->spacerItem()) + return nullptr; + if (QLayout *l = item->layout()) { + for (int i = 0, n = l->count(); i < n; ++i) { + if (QWidget *w = widgetForItem(l->itemAt(i))) + return w; + } + } + return nullptr; +} + +static QLabel *createLabel(const QString &text) +{ + auto label = new QLabel(text); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + return label; +} + +static void addItemToBoxLayout(QBoxLayout *layout, const LayoutItem &item) +{ + if (QWidget *w = item.widget) { + layout->addWidget(w); + } else if (QLayout *l = item.layout) { + layout->addLayout(l); + } else if (item.stretch != -1) { + layout->addStretch(item.stretch); + } else if (item.space != -1) { + layout->addSpacing(item.space); + } else if (!item.text.isEmpty()) { + layout->addWidget(createLabel(item.text)); + } else if (item.empty) { + // Nothing to do, but no reason to warn, either. + } else { + QTC_CHECK(false); + } +} + +static void addItemToFlowLayout(FlowLayout *layout, const LayoutItem &item) +{ + if (QWidget *w = item.widget) { + layout->addWidget(w); + } else if (QLayout *l = item.layout) { + layout->addItem(l); +// } else if (item.stretch != -1) { +// layout->addStretch(item.stretch); +// } else if (item.space != -1) { +// layout->addSpacing(item.space); + } else if (item.empty) { + // Nothing to do, but no reason to warn, either + } else if (!item.text.isEmpty()) { + layout->addWidget(createLabel(item.text)); + } else { + QTC_CHECK(false); + } +} + +// void doAddSpace(LayoutBuilder &builder, const Space &space) +// { +// ResultItem fi; +// fi.space = space.space; +// builder.stack.last().pendingItems.append(fi); +// } + +// void doAddStretch(LayoutBuilder &builder, const Stretch &stretch) +// { +// Item fi; +// fi.stretch = stretch.stretch; +// builder.stack.last().pendingItems.append(fi); +// } + +// void doAddLayout(LayoutBuilder &builder, QLayout *layout) +// { +// builder.stack.last().pendingItems.append(Item(layout)); +// } + +// void doAddWidget(LayoutBuilder &builder, QWidget *widget) +// { +// builder.stack.last().pendingItems.append(Item(widget)); +// } + + +/*! + \class Layouting::Space + \inmodule QtCreator + + \brief The Space class represents some empty space in a layout. + */ + +/*! + \class Layouting::Stretch + \inmodule QtCreator + + \brief The Stretch class represents some stretch in a layout. + */ + +/*! + \class Layouting::LayoutBuilder + \internal + \inmodule QtCreator + + \brief The LayoutBuilder class provides a convenient way to fill \c QFormLayout + and \c QGridLayouts with contents. + + Filling a layout with items happens item-by-item, row-by-row. + + A LayoutBuilder instance is typically used locally within a function and never stored. + + \sa addItem(), addItems() +*/ + + +/*! + \internal + Destructs a layout builder. + */ + +/*! + Starts a new row containing \a items. The row can be further extended by + other items using \c addItem() or \c addItems(). + + \sa addItem(), addItems() + */ +// void LayoutItem::addRow(const LayoutItems &items) +// { +// addItem(br); +// addItems(items); +// } + +// /*! +// Adds the layout item \a item as sub items. +// */ +// void LayoutItem::addItem(const LayoutItem &item) +// { +// subItems.append(item); +// } + +// /*! +// Adds the layout items \a items as sub items. +// */ +// void LayoutItem::addItems(const LayoutItems &items) +// { +// subItems.append(items); +// } + +// /*! +// Attaches the constructed layout to the provided QWidget \a w. + +// This operation can only be performed once per LayoutBuilder instance. +// */ + +// void LayoutItem::attachTo(QWidget *w) const +// { +// LayoutBuilder builder; + +// builder.stack.append(w); +// addItemHelper(builder, *this); +// } + + +// Layout + +void LayoutInterface::span(int cols, int rows) +{ + QTC_ASSERT(!pendingItems.empty(), return); + pendingItems.back().spanCols = cols; + pendingItems.back().spanRows = rows; +} + +void LayoutInterface::noMargin() +{ + customMargin({}); +} + +void LayoutInterface::normalMargin() +{ + customMargin({9, 9, 9, 9}); +} + +void LayoutInterface::customMargin(const QMargins &margin) +{ + access(this)->setContentsMargins(margin); +} + +void LayoutInterface::addItem(const LayoutItem &item) +{ + if (item.break_) + flush(); + else + pendingItems.push_back(item); +} + +void addNestedItem(WidgetInterface *widget, const LayoutInterface &layout) +{ + widget->setLayout(layout); +} + +void addNestedItem(LayoutInterface *layout, const WidgetInterface &inner) +{ + LayoutItem item; + item.widget = access(&inner); + layout->addItem(item); +} + +void addNestedItem(LayoutInterface *layout, const LayoutItem &inner) +{ + layout->addItem(inner); +} + +void addNestedItem(LayoutInterface *layout, const LayoutInterface &inner) +{ + LayoutItem item; + item.layout = access(&inner); + layout->addItem(item); +} + +void addNestedItem(LayoutInterface *layout, const std::function &inner) +{ + LayoutItem item = inner(); + layout->addItem(item); +} + +void addNestedItem(LayoutInterface *layout, const QString &inner) +{ + LayoutItem item; + item.text = inner; + layout->addItem(item); +} + +LayoutItem empty() +{ + LayoutItem item; + item.empty = true; + return item; +} + +LayoutItem hr() +{ + LayoutItem item; + item.widget = createHr(); + return item; +} + +LayoutItem br() +{ + LayoutItem item; + item.break_ = true; + return item; +} + +LayoutItem st() +{ + LayoutItem item; + item.stretch = 1; + return item; +} + +QFormLayout *LayoutInterface::asForm() +{ + return qobject_cast(access(this)); +} + +QGridLayout *LayoutInterface::asGrid() +{ + return qobject_cast(access(this)); +} + +QBoxLayout *LayoutInterface::asBox() +{ + return qobject_cast(access(this)); +} + +void LayoutInterface::flush() +{ + if (QGridLayout *lt = asGrid()) { + for (const LayoutItem &item : std::as_const(pendingItems)) { + Qt::Alignment a = currentGridColumn == 0 ? align : Qt::Alignment(); + if (item.widget) + lt->addWidget(item.widget, currentGridRow, currentGridColumn, item.spanRows, item.spanCols, a); + else if (item.layout) + lt->addLayout(item.layout, currentGridRow, currentGridColumn, item.spanRows, item.spanCols, a); + else if (!item.text.isEmpty()) + lt->addWidget(createLabel(item.text), currentGridRow, currentGridColumn, item.spanRows, item.spanCols, a); + currentGridColumn += item.spanCols; + // Intentionally not used, use 'br'/'empty' for vertical progress. + // currentGridRow += item.spanRows; + } + ++currentGridRow; + currentGridColumn = 0; + pendingItems.clear(); + return; + } + + if (QFormLayout *fl = asForm()) { + if (pendingItems.size() > 2) { + auto hbox = new QHBoxLayout; + hbox->setContentsMargins(0, 0, 0, 0); + for (int i = 1; i < pendingItems.size(); ++i) + addItemToBoxLayout(hbox, pendingItems.at(i)); + while (pendingItems.size() > 1) + pendingItems.pop_back(); + pendingItems.push_back(LayoutItem(hbox)); + } + + if (pendingItems.size() == 1) { // Only one item given, so this spans both columns. + const LayoutItem &f0 = pendingItems.at(0); + if (auto layout = f0.layout) + fl->addRow(layout); + else if (auto widget = f0.widget) + fl->addRow(widget); + } else if (pendingItems.size() == 2) { // Normal case, both columns used. + LayoutItem &f1 = pendingItems[1]; + const LayoutItem &f0 = pendingItems.at(0); + if (!f1.widget && !f1.layout && !f1.text.isEmpty()) + f1.widget = createLabel(f1.text); + + // QFormLayout accepts only widgets or text in the first column. + // FIXME: Should we be more generous? + if (f0.widget) { + if (f1.layout) + fl->addRow(f0.widget, f1.layout); + else if (f1.widget) + fl->addRow(f0.widget, f1.widget); + } else { + if (f1.layout) + fl->addRow(createLabel(f0.text), f1.layout); + else if (f1.widget) + fl->addRow(createLabel(f0.text), f1.widget); + } + } else { + QTC_CHECK(false); + } + + // Set up label as buddy if possible. + const int lastRow = fl->rowCount() - 1; + QLayoutItem *l = fl->itemAt(lastRow, QFormLayout::LabelRole); + QLayoutItem *f = fl->itemAt(lastRow, QFormLayout::FieldRole); + if (l && f) { + if (QLabel *label = qobject_cast(l->widget())) { + if (QWidget *widget = widgetForItem(f)) + label->setBuddy(widget); + } + } + + pendingItems.clear(); + return; + } +} + +// LayoutItem withFormAlignment() +// { +// LayoutItem item; +// item.onAdd = [](LayoutBuilder &builder) { +// if (builder.stack.size() >= 2) { +// if (auto widget = builder.stack.at(builder.stack.size() - 2).widget) { +// const Qt::Alignment align(widget->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)); +// builder.stack.last().align = align; +// } +// } +// }; +// return item; +// } + +// Flow + +Flow::Flow(std::initializer_list ps) +{ + adopt(new FlowLayout); + for (auto && p : ps) + apply(p); + // for (const LayoutItem &item : std::as_const(pendingItems)) + // addItemToFlowLayout(flowLayout, item); + +} + +// Row & Column + +Row::Row(std::initializer_list ps) +{ + adopt(new QHBoxLayout); + for (auto && p : ps) + apply(p); + auto self = asBox(); + for (const LayoutItem &item : pendingItems) + addItemToBoxLayout(self, item); +} + +Column::Column(std::initializer_list ps) +{ + adopt(new QVBoxLayout); + for (auto && p : ps) + apply(p); + auto self = asBox(); + for (const LayoutItem &item : pendingItems) + addItemToBoxLayout(self, item); +} + +// Grid + +Grid::Grid(std::initializer_list ps) +{ + adopt(new QGridLayout); + for (auto && p : ps) + apply(p); + flush(); +} + +// Form + +Form::Form(std::initializer_list ps) +{ + adopt(new QFormLayout); + fieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + for (auto && p : ps) + apply(p); + flush(); +} + +void LayoutInterface::fieldGrowthPolicy(int policy) +{ + if (auto lt = asForm()) + lt->setFieldGrowthPolicy(QFormLayout::FieldGrowthPolicy(policy)); +} + +// "Widgets" + +Widget::Widget(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +void WidgetInterface::resize(int w, int h) +{ + access(this)->resize(w, h); +} + +void WidgetInterface::setLayout(const LayoutInterface &layout) +{ + access(this)->setLayout(access(&layout)); +} + +void WidgetInterface::setWindowTitle(const QString &title) +{ + access(this)->setWindowTitle(title); +} + +void WidgetInterface::setToolTip(const QString &title) +{ + access(this)->setToolTip(title); +} + +void WidgetInterface::show() +{ + access(this)->show(); +} + +void WidgetInterface::noMargin() +{ + customMargin({}); +} + +void WidgetInterface::normalMargin() +{ + customMargin({9, 9, 9, 9}); +} + +void WidgetInterface::customMargin(const QMargins &margin) +{ + access(this)->setContentsMargins(margin); +} + +QWidget *WidgetInterface::emerge() +{ + return access(this); +} + +// Label + +Label::Label(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +Label::Label(const QString &text) +{ + create(); + setText(text); +} + +void LabelInterface::setText(const QString &text) +{ + access(this)->setText(text); +} + +// Group + +Group::Group(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +void GroupInterface::setTitle(const QString &title) +{ + access(this)->setTitle(title); + access(this)->setObjectName(title); +} + +// SpinBox + +SpinBox::SpinBox(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +void SpinBoxInterface::setValue(int val) +{ + access(this)->setValue(val); +} + +void SpinBoxInterface::onTextChanged(const std::function &func) +{ + QObject::connect(access(this), &QSpinBox::textChanged, func); +} + +// TextEdit + +TextEdit::TextEdit(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +void TextEditInterface::setText(const QString &text) +{ + access(this)->setText(text); +} + +// PushButton + +PushButton::PushButton(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +void PushButtonInterface::setText(const QString &text) +{ + access(this)->setText(text); +} + +void PushButtonInterface::onClicked(const std::function &func) +{ + QObject::connect(access(this), &QAbstractButton::clicked, func); +} + +// Stack + +// We use a QStackedWidget instead of a QStackedLayout here because the latter will call +// "setVisible()" when a child is added, which can lead to the widget being spawned as a +// top-level widget. This can lead to the focus shifting away from the main application. +Stack::Stack(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +// Splitter + +Splitter::Splitter(std::initializer_list ps) +{ + create(); + access(this)->setOrientation(Qt::Vertical); + for (auto && p : ps) + apply(p); +} + +// ToolBar + +ToolBar::ToolBar(std::initializer_list ps) +{ + create(); + access(this)->setOrientation(Qt::Horizontal); + for (auto && p : ps) + apply(p); +} + +// TabWidget + +TabWidget::TabWidget(std::initializer_list ps) +{ + create(); + for (auto && p : ps) + apply(p); +} + +// // Special Tab + +// Tab::Tab(const QString &tabName, const LayoutItem &item) +// { +// onAdd = [item](LayoutBuilder &builder) { +// auto tab = new QWidget; +// builder.stack.append(tab); +// item.attachTo(tab); +// }; +// onExit = [tabName](LayoutBuilder &builder) { +// QWidget *inner = builder.stack.last().widget; +// builder.stack.pop_back(); +// auto tabWidget = qobject_cast(builder.stack.last().widget); +// QTC_ASSERT(tabWidget, return); +// tabWidget->addTab(inner, tabName); +// }; +// } + +// // Special If + +// If::If(bool condition, const LayoutItems &items, const LayoutItems &other) +// { +// subItems.append(condition ? items : other); +// } + +// "Properties" + +// LayoutItem spacing(int spacing) +// { +// return [spacing](QObject *target) { +// if (auto layout = qobject_cast(target)) { +// layout->setSpacing(spacing); +// } else { +// QTC_CHECK(false); +// } +// }; +// } + +// LayoutItem columnStretch(int column, int stretch) +// { +// return [column, stretch](QObject *target) { +// if (auto grid = qobject_cast(target)) { +// grid->setColumnStretch(column, stretch); +// } else { +// QTC_CHECK(false); +// } +// }; +// } + +QWidget *createHr(QWidget *parent) +{ + auto frame = new QFrame(parent); + frame->setFrameShape(QFrame::HLine); + frame->setFrameShadow(QFrame::Sunken); + return frame; +} + +Span::Span(int n, const LayoutItem &item) + : LayoutItem(item) +{ + spanCols = n; +} + +// void createItem(LayoutItem *item, QWidget *t) +// { +// if (auto l = qobject_cast(t)) +// l->setTextInteractionFlags(l->textInteractionFlags() | Qt::TextSelectableByMouse); + +// item->onAdd = [t](LayoutBuilder &builder) { doAddWidget(builder, t); }; +// } + + +} // Layouting diff --git a/tests/manual/layoutbuilder/v2/lb.h b/tests/manual/layoutbuilder/v2/lb.h new file mode 100644 index 00000000000..463a0e2b33f --- /dev/null +++ b/tests/manual/layoutbuilder/v2/lb.h @@ -0,0 +1,511 @@ +// Copyright (C) 2023 André Pönitz +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#pragma once + +#include +#include + +#include +#include +#include + +#if defined(UTILS_LIBRARY) +# define QTCREATOR_UTILS_EXPORT Q_DECL_EXPORT +#elif defined(UTILS_STATIC_LIBRARY) +# define QTCREATOR_UTILS_EXPORT +#else +# define QTCREATOR_UTILS_EXPORT Q_DECL_IMPORT +#endif + +QT_BEGIN_NAMESPACE +class QBoxLayout; +class QFormLayout; +class QGridLayout; +class QGroupBox; +class QLabel; +class QLayout; +class QMargins; +class QObject; +class QPushButton; +class QSpinBox; +class QSplitter; +class QStackedWidget; +class QTabWidget; +class QTextEdit; +class QToolBar; +class QWidget; +QT_END_NAMESPACE + +namespace Layouting { + +struct LayoutItem; + +// struct NestId {}; +struct NestId {}; + +template +struct IdAndArg +{ + IdAndArg(const T1 &id, const T2 &arg) : id(id), arg(arg) {} + T1 id; + T2 arg; +}; + +// The main dispatchers + +void doit(auto x, auto id, auto a); + +// BuilderItem + +template +struct BuilderItem : XInterface +{ +public: + struct I + { + // Nested child object + template + I(const Inner &p) + { + apply = [p](XInterface *x) { doit(x, NestId{}, p); }; + } + + // Property setter + template + I(const IdAndArg &p) + { + apply = [p](XInterface *x) { doit(x, p.id, p.arg); }; + } + + std::function apply; + }; + + void create() + { + XInterface::ptr = new XInterface::Implementation; + } + + void adopt(XInterface::Implementation *ptr) + { + XInterface::ptr = ptr; + } + + void apply(const I &init) + { + init.apply(this); + } + + using Id = typename XInterface::Implementation *; +}; + + +////////////////////////////////////////////// + +struct LayoutInterface; +struct WidgetInterface; + +struct QTCREATOR_UTILS_EXPORT LayoutItem +{ + LayoutItem(); + LayoutItem(QLayout *l) : layout(l), empty(!l) {} + LayoutItem(QWidget *w) : widget(w), empty(!w) {} + LayoutItem(const QString &t) : text(t) {} + LayoutItem(const LayoutInterface &inner); + LayoutItem(const WidgetInterface &inner); + ~LayoutItem(); + + QString text; + QLayout *layout = nullptr; + QWidget *widget = nullptr; + int space = -1; + int stretch = -1; + int spanCols = 1; + int spanRows = 1; + bool empty = false; + bool break_ = false; +}; + +using LayoutItems = QList; + + +// We need two classes for each user visible Builder type. +// The actual Builder classes derive from BuilderItem with two parameters, +// one is the Builder class itself for CRTP and a one "Interface" type. +// The "Interface" types act (individually, and as a hierarchy) as interface +// members of the real QObject/QWidget/... hierarchy things. This wrapper is not +// strictly needed, the Q* hierarchy could be used directly, at the +// price of #include'ing the definitions of each participating class, +// which does not scale well. + +// +// Basic +// + +struct QTCREATOR_UTILS_EXPORT ThingInterface +{ + template + T *access_() const { return static_cast(ptr); } + + void *ptr; // The product. +}; + +struct QTCREATOR_UTILS_EXPORT ObjectInterface : ThingInterface +{ + using Implementation = QObject; +}; + +struct QTCREATOR_UTILS_EXPORT Object : BuilderItem +{ + Object(std::initializer_list ps); +}; + +// +// Layouts +// + +struct QTCREATOR_UTILS_EXPORT LayoutInterface : ObjectInterface +{ + using Implementation = QLayout; + + void span(int cols, int rows); + void noMargin(); + void normalMargin(); + void customMargin(const QMargins &margin); + + void addItem(const LayoutItem &item); + + void flush(); + void fieldGrowthPolicy(int policy); + + QFormLayout *asForm(); + QGridLayout *asGrid(); + QBoxLayout *asBox(); + + std::vector pendingItems; + + // Grid-only + int currentGridColumn = 0; + int currentGridRow = 0; + Qt::Alignment align = {}; +}; + +struct QTCREATOR_UTILS_EXPORT Layout : BuilderItem +{ +}; + +struct QTCREATOR_UTILS_EXPORT Column : BuilderItem +{ + Column(std::initializer_list ps); +}; + +struct QTCREATOR_UTILS_EXPORT Row : BuilderItem +{ + Row(std::initializer_list ps); +}; + +struct QTCREATOR_UTILS_EXPORT Form : BuilderItem +{ + Form(std::initializer_list ps); +}; + +struct QTCREATOR_UTILS_EXPORT Grid : BuilderItem +{ + Grid(std::initializer_list ps); +}; + +struct QTCREATOR_UTILS_EXPORT Flow : BuilderItem +{ + Flow(std::initializer_list ps); +}; + +struct QTCREATOR_UTILS_EXPORT Stretch : LayoutItem +{ + explicit Stretch(int stretch) { this->stretch = stretch; } +}; + +struct QTCREATOR_UTILS_EXPORT Space : LayoutItem +{ + explicit Space(int space) { this->space = space; } +}; + +struct QTCREATOR_UTILS_EXPORT Span : LayoutItem +{ + Span(int n, const LayoutItem &item); +}; + +// +// Widgets +// + +struct QTCREATOR_UTILS_EXPORT WidgetInterface : ObjectInterface +{ + using Implementation = QWidget; + QWidget *emerge(); + + void show(); + void resize(int, int); + void setLayout(const LayoutInterface &layout); + void setWindowTitle(const QString &); + void setToolTip(const QString &); + void noMargin(); + void normalMargin(); + void customMargin(const QMargins &margin); +}; + +struct QTCREATOR_UTILS_EXPORT Widget : BuilderItem +{ + Widget(std::initializer_list ps); +}; + +// Label + +struct QTCREATOR_UTILS_EXPORT LabelInterface : WidgetInterface +{ + using Implementation = QLabel; + + void setText(const QString &); +}; + +struct QTCREATOR_UTILS_EXPORT Label : BuilderItem +{ + Label(std::initializer_list ps); + Label(const QString &text); +}; + +// Group + +struct QTCREATOR_UTILS_EXPORT GroupInterface : WidgetInterface +{ + using Implementation = QGroupBox; + + void setTitle(const QString &); +}; + +struct QTCREATOR_UTILS_EXPORT Group : BuilderItem +{ + Group(std::initializer_list ps); +}; + +// SpinBox + +struct QTCREATOR_UTILS_EXPORT SpinBoxInterface : WidgetInterface +{ + using Implementation = QSpinBox; + + void setValue(int); + void onTextChanged(const std::function &); +}; + +struct QTCREATOR_UTILS_EXPORT SpinBox : BuilderItem +{ + SpinBox(std::initializer_list ps); +}; + +// PushButton + +struct QTCREATOR_UTILS_EXPORT PushButtonInterface : WidgetInterface +{ + using Implementation = QPushButton; + + void setText(const QString &); + void onClicked(const std::function &); +}; + +struct QTCREATOR_UTILS_EXPORT PushButton : BuilderItem +{ + PushButton(std::initializer_list ps); +}; + +// TextEdit + +struct QTCREATOR_UTILS_EXPORT TextEditInterface : WidgetInterface +{ + using Implementation = QTextEdit; + + void setText(const QString &); +}; + +struct QTCREATOR_UTILS_EXPORT TextEdit : BuilderItem +{ + TextEdit(std::initializer_list ps); +}; + +// Splitter + +struct QTCREATOR_UTILS_EXPORT SplitterInterface : WidgetInterface +{ + using Implementation = QSplitter; +}; + +struct QTCREATOR_UTILS_EXPORT Splitter : BuilderItem +{ + Splitter(std::initializer_list items); +}; + +// Stack + +struct QTCREATOR_UTILS_EXPORT StackInterface : WidgetInterface +{ +}; + +struct QTCREATOR_UTILS_EXPORT Stack : BuilderItem +{ + Stack() : Stack({}) {} + Stack(std::initializer_list items); +}; + +// TabWidget + +struct QTCREATOR_UTILS_EXPORT TabInterface : WidgetInterface +{ +}; + +struct QTCREATOR_UTILS_EXPORT TabWidgetInterface : WidgetInterface +{ + using Implementation = QTabWidget; +}; + +struct QTCREATOR_UTILS_EXPORT Tab : BuilderItem +{ + Tab(const QString &tabName, const LayoutItem &item); +}; + +struct QTCREATOR_UTILS_EXPORT TabWidget : BuilderItem +{ + TabWidget(std::initializer_list items); +}; + +// ToolBar + +struct QTCREATOR_UTILS_EXPORT ToolBarInterface : WidgetInterface +{ + using Implementation = QToolBar; +}; + +struct QTCREATOR_UTILS_EXPORT ToolBar : BuilderItem +{ + ToolBar(std::initializer_list items); +}; + +// Special + +struct QTCREATOR_UTILS_EXPORT If : LayoutItem +{ + If(bool condition, const LayoutItems &item, const LayoutItems &other = {}); +}; + +// +// Dispatchers +// + +// We need one 'Id' (and a corresponding function wrapping arguments into a +// tuple marked by this id) per 'name' of "backend" setter member function, +// i.e. one 'text' is sufficient for QLabel::setText, QLineEdit::setText. +// The name of the Id does not have to match the backend names as it +// is mapped per-backend-type in the respective setter implementation +// but we assume that it generally makes sense to stay close to the +// wrapped API name-wise. + +// These are free functions overloaded on the type of builder object +// and setter id. The function implementations are independent, but +// the base expectation is that they will forwards to the backend +// type's setter. + +// Special dispatchers :w + + +struct BindToId {}; + +template +auto bindTo(T **x) +{ + // FIXME: Evil hack to shut up clang-tidy which does not see that the returned tuple will + // result in an assignment to *x and complains about every use of the bound value later. + // main.cpp:129:5: Called C++ object pointer is null [clang-analyzer-core.CallAndMessage] + // 1: Calling 'bindTo' in /data/dev/creator/tests/manual/layoutbuilder/v2/main.cpp:73 + // 2: Null pointer value stored to 'w' in /data/dev/creator/tests/manual/layoutbuilder/v2/lb.h:518 + // 3: Returning from 'bindTo' in /data/dev/creator/tests/manual/layoutbuilder/v2/main.cpp:73 + // 4: Called C++ object pointer is null in /data/dev/creator/tests/manual/layoutbuilder/v2/main.cpp:129 + *x = reinterpret_cast(1); + + return IdAndArg{BindToId{}, x}; +} + +template +void doit(Interface *x, BindToId, auto p) +{ + *p = static_cast(x->ptr); +} + +struct IdId {}; +auto id(auto x) { return IdAndArg{IdId{}, x}; } + +template +void doit(Interface *x, IdId, auto p) +{ + *p = static_cast(x->ptr); +} + +// Setter dispatchers + +struct SizeId {}; +auto size(auto w, auto h) { return IdAndArg{SizeId{}, std::pair{w, h}}; } +void doit(auto x, SizeId, auto p) { x->resize(p.first, p.second); } + +struct TextId {}; +auto text(auto x) { return IdAndArg{TextId{}, x}; } +void doit(auto x, TextId, auto t) { x->setText(t); } + +struct TitleId {}; +auto title(auto x) { return IdAndArg{TitleId{}, x}; } +void doit(auto x, TitleId, auto t) { x->setTitle(t); } + +struct ToolTipId {}; +auto toolTip(auto x) { return IdAndArg{ToolTipId{}, x}; } +void doit(auto x, ToolTipId, auto t) { x->setToolTip(t); } + +struct WindowTitleId {}; +auto windowTitle(auto x) { return IdAndArg{WindowTitleId{}, x}; } +void doit(auto x, WindowTitleId, auto t) { x->setWindowTitle(t); } + +struct OnTextChangedId {}; +auto onTextChanged(auto x) { return IdAndArg{OnTextChangedId{}, x}; } +void doit(auto x, OnTextChangedId, auto func) { x->onTextChanged(func); } + +struct OnClickedId {}; +auto onClicked(auto x) { return IdAndArg{OnClickedId{}, x}; } +void doit(auto x, OnClickedId, auto func) { x->onClicked(func); } + + +// Nesting dispatchers + +QTCREATOR_UTILS_EXPORT void addNestedItem(WidgetInterface *widget, const LayoutInterface &layout); +QTCREATOR_UTILS_EXPORT void addNestedItem(LayoutInterface *layout, const LayoutItem &inner); +QTCREATOR_UTILS_EXPORT void addNestedItem(LayoutInterface *layout, const LayoutInterface &inner); +QTCREATOR_UTILS_EXPORT void addNestedItem(LayoutInterface *layout, const WidgetInterface &inner); +QTCREATOR_UTILS_EXPORT void addNestedItem(LayoutInterface *layout, const QString &inner); +QTCREATOR_UTILS_EXPORT void addNestedItem(LayoutInterface *layout, const std::function &inner); +// ... can be added to anywhere later to support "user types" + +void doit(auto outer, NestId, auto inner) +{ + addNestedItem(outer, inner); +} + + +// Special layout items + +QTCREATOR_UTILS_EXPORT LayoutItem br(); +QTCREATOR_UTILS_EXPORT LayoutItem empty(); +QTCREATOR_UTILS_EXPORT LayoutItem hr(); +QTCREATOR_UTILS_EXPORT LayoutItem withFormAlignment(); +QTCREATOR_UTILS_EXPORT LayoutItem st(); + + +// Convenience + +QTCREATOR_UTILS_EXPORT QWidget *createHr(QWidget *parent = nullptr); + +} // Layouting diff --git a/tests/manual/layoutbuilder/v2/main.cpp b/tests/manual/layoutbuilder/v2/main.cpp new file mode 100644 index 00000000000..81432a30661 --- /dev/null +++ b/tests/manual/layoutbuilder/v2/main.cpp @@ -0,0 +1,79 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "lb.h" + +#include +#include +#include +#include +#include + +using namespace Layouting; + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + TextEdit::Id textId; + + QWidget *w = nullptr; + QGroupBox *g = nullptr; + QLabel *l = nullptr; + + Group { + bindTo(&w), // Works, as GroupInterface derives from WidgetInterface + // bindTo(&l), // Does (intentionally) not work, GroupInterface does not derive from LabelInterface + bindTo(&g), + size(300, 200), + title("HHHHHHH"), + Form { + "Hallo", + Group { + title("Title"), + Column { + Label { + text("World") + }, + TextEdit { + id(&textId), + text("Och noe") + } + } + }, + br, + "Col", + Column { + Row { "1", "2", "3" }, + Row { "3", "4", "6" } + }, + br, + "Grid", + Grid { + Span { 2, QString("1111111") }, "3", br, + "3", "4", "6", br, + "4", empty, "6", br, + hr, "4", "6" + }, + br, + Column { + Label { + text("Hi"), + size(30, 20) + }, + Row { + SpinBox { + onTextChanged([&](const QString &text) { textId->setText(text); }) + }, + st, + PushButton { + text("Quit"), + onClicked(QApplication::quit) + } + } + } + } + }.show(); + + return app.exec(); +}