LayoutBuilder: Complete experimental implementation

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: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
hjk
2024-05-03 15:04:54 +02:00
parent 8d0025fe09
commit acf1ecb47f
4 changed files with 1542 additions and 0 deletions

View File

@@ -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)

View File

@@ -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 <QApplication>
#include <QDebug>
#include <QFormLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QPushButton>
#include <QSpacerItem>
#include <QSpinBox>
#include <QSplitter>
#include <QStackedLayout>
#include <QStackedWidget>
#include <QStyle>
#include <QTabWidget>
#include <QTextEdit>
#include <QToolBar>
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 <typename XInterface>
XInterface::Implementation *access(const XInterface *x)
{
return static_cast<XInterface::Implementation *>(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<QWidget *>(parent);
return pw->style()->pixelMetric(pm, nullptr, pw);
} else {
return static_cast<QLayout *>(parent)->spacing();
}
}
QList<QLayoutItem *> 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 <class T> 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<I> 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<LayoutItem()> &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<QFormLayout *>(access(this));
}
QGridLayout *LayoutInterface::asGrid()
{
return qobject_cast<QGridLayout *>(access(this));
}
QBoxLayout *LayoutInterface::asBox()
{
return qobject_cast<QBoxLayout *>(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<QLabel *>(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<I> 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<I> 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<I> 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<I> ps)
{
adopt(new QGridLayout);
for (auto && p : ps)
apply(p);
flush();
}
// Form
Form::Form(std::initializer_list<I> 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<I> 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<I> 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<I> 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<I> ps)
{
create();
for (auto && p : ps)
apply(p);
}
void SpinBoxInterface::setValue(int val)
{
access(this)->setValue(val);
}
void SpinBoxInterface::onTextChanged(const std::function<void (QString)> &func)
{
QObject::connect(access(this), &QSpinBox::textChanged, func);
}
// TextEdit
TextEdit::TextEdit(std::initializer_list<I> ps)
{
create();
for (auto && p : ps)
apply(p);
}
void TextEditInterface::setText(const QString &text)
{
access(this)->setText(text);
}
// PushButton
PushButton::PushButton(std::initializer_list<I> ps)
{
create();
for (auto && p : ps)
apply(p);
}
void PushButtonInterface::setText(const QString &text)
{
access(this)->setText(text);
}
void PushButtonInterface::onClicked(const std::function<void ()> &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<I> ps)
{
create();
for (auto && p : ps)
apply(p);
}
// Splitter
Splitter::Splitter(std::initializer_list<I> ps)
{
create();
access(this)->setOrientation(Qt::Vertical);
for (auto && p : ps)
apply(p);
}
// ToolBar
ToolBar::ToolBar(std::initializer_list<I> ps)
{
create();
access(this)->setOrientation(Qt::Horizontal);
for (auto && p : ps)
apply(p);
}
// TabWidget
TabWidget::TabWidget(std::initializer_list<I> 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<QTabWidget *>(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<QLayout *>(target)) {
// layout->setSpacing(spacing);
// } else {
// QTC_CHECK(false);
// }
// };
// }
// LayoutItem columnStretch(int column, int stretch)
// {
// return [column, stretch](QObject *target) {
// if (auto grid = qobject_cast<QGridLayout *>(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<QLabel *>(t))
// l->setTextInteractionFlags(l->textInteractionFlags() | Qt::TextSelectableByMouse);
// item->onAdd = [t](LayoutBuilder &builder) { doAddWidget(builder, t); };
// }
} // Layouting

View File

@@ -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 <QList>
#include <QString>
#include <array>
#include <cstring>
#include <initializer_list>
#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 <typename T1, typename T2>
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 <typename X, typename XInterface>
struct BuilderItem : XInterface
{
public:
struct I
{
// Nested child object
template <typename Inner>
I(const Inner &p)
{
apply = [p](XInterface *x) { doit(x, NestId{}, p); };
}
// Property setter
template <typename Id, typename Arg1>
I(const IdAndArg<Id, Arg1> &p)
{
apply = [p](XInterface *x) { doit(x, p.id, p.arg); };
}
std::function<void(XInterface *)> 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<LayoutItem>;
// 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 <typename T>
T *access_() const { return static_cast<T *>(ptr); }
void *ptr; // The product.
};
struct QTCREATOR_UTILS_EXPORT ObjectInterface : ThingInterface
{
using Implementation = QObject;
};
struct QTCREATOR_UTILS_EXPORT Object : BuilderItem<Object, ObjectInterface>
{
Object(std::initializer_list<I> 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<LayoutItem> pendingItems;
// Grid-only
int currentGridColumn = 0;
int currentGridRow = 0;
Qt::Alignment align = {};
};
struct QTCREATOR_UTILS_EXPORT Layout : BuilderItem<Layout, LayoutInterface>
{
};
struct QTCREATOR_UTILS_EXPORT Column : BuilderItem<Column, LayoutInterface>
{
Column(std::initializer_list<I> ps);
};
struct QTCREATOR_UTILS_EXPORT Row : BuilderItem<Row, LayoutInterface>
{
Row(std::initializer_list<I> ps);
};
struct QTCREATOR_UTILS_EXPORT Form : BuilderItem<Form, LayoutInterface>
{
Form(std::initializer_list<I> ps);
};
struct QTCREATOR_UTILS_EXPORT Grid : BuilderItem<Grid, LayoutInterface>
{
Grid(std::initializer_list<I> ps);
};
struct QTCREATOR_UTILS_EXPORT Flow : BuilderItem<Flow, LayoutInterface>
{
Flow(std::initializer_list<I> 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, WidgetInterface>
{
Widget(std::initializer_list<I> ps);
};
// Label
struct QTCREATOR_UTILS_EXPORT LabelInterface : WidgetInterface
{
using Implementation = QLabel;
void setText(const QString &);
};
struct QTCREATOR_UTILS_EXPORT Label : BuilderItem<Label, LabelInterface>
{
Label(std::initializer_list<I> 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, GroupInterface>
{
Group(std::initializer_list<I> ps);
};
// SpinBox
struct QTCREATOR_UTILS_EXPORT SpinBoxInterface : WidgetInterface
{
using Implementation = QSpinBox;
void setValue(int);
void onTextChanged(const std::function<void(QString)> &);
};
struct QTCREATOR_UTILS_EXPORT SpinBox : BuilderItem<SpinBox, SpinBoxInterface>
{
SpinBox(std::initializer_list<I> ps);
};
// PushButton
struct QTCREATOR_UTILS_EXPORT PushButtonInterface : WidgetInterface
{
using Implementation = QPushButton;
void setText(const QString &);
void onClicked(const std::function<void()> &);
};
struct QTCREATOR_UTILS_EXPORT PushButton : BuilderItem<PushButton, PushButtonInterface>
{
PushButton(std::initializer_list<I> ps);
};
// TextEdit
struct QTCREATOR_UTILS_EXPORT TextEditInterface : WidgetInterface
{
using Implementation = QTextEdit;
void setText(const QString &);
};
struct QTCREATOR_UTILS_EXPORT TextEdit : BuilderItem<TextEdit, TextEditInterface>
{
TextEdit(std::initializer_list<I> ps);
};
// Splitter
struct QTCREATOR_UTILS_EXPORT SplitterInterface : WidgetInterface
{
using Implementation = QSplitter;
};
struct QTCREATOR_UTILS_EXPORT Splitter : BuilderItem<Splitter, SplitterInterface>
{
Splitter(std::initializer_list<I> items);
};
// Stack
struct QTCREATOR_UTILS_EXPORT StackInterface : WidgetInterface
{
};
struct QTCREATOR_UTILS_EXPORT Stack : BuilderItem<Stack, StackInterface>
{
Stack() : Stack({}) {}
Stack(std::initializer_list<I> items);
};
// TabWidget
struct QTCREATOR_UTILS_EXPORT TabInterface : WidgetInterface
{
};
struct QTCREATOR_UTILS_EXPORT TabWidgetInterface : WidgetInterface
{
using Implementation = QTabWidget;
};
struct QTCREATOR_UTILS_EXPORT Tab : BuilderItem<Tab, TabInterface>
{
Tab(const QString &tabName, const LayoutItem &item);
};
struct QTCREATOR_UTILS_EXPORT TabWidget : BuilderItem<TabWidget, TabWidgetInterface>
{
TabWidget(std::initializer_list<I> items);
};
// ToolBar
struct QTCREATOR_UTILS_EXPORT ToolBarInterface : WidgetInterface
{
using Implementation = QToolBar;
};
struct QTCREATOR_UTILS_EXPORT ToolBar : BuilderItem<ToolBar, ToolBarInterface>
{
ToolBar(std::initializer_list<I> 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 <typename T>
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<QWidget>' 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<QWidget>' 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<T*>(1);
return IdAndArg{BindToId{}, x};
}
template <typename Interface>
void doit(Interface *x, BindToId, auto p)
{
*p = static_cast<Interface::Implementation *>(x->ptr);
}
struct IdId {};
auto id(auto x) { return IdAndArg{IdId{}, x}; }
template <typename Interface>
void doit(Interface *x, IdId, auto p)
{
*p = static_cast<Interface::Implementation *>(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<LayoutItem()> &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

View File

@@ -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 <QApplication>
#include <QWidget>
#include <QLabel>
#include <QGroupBox>
#include <QTextEdit>
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();
}