Files
qt-creator/tests/manual/layoutbuilder/v2/lb.cpp
hjk acf1ecb47f 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>
2024-05-16 12:06:06 +00:00

936 lines
22 KiB
C++

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