// 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 "layoutbuilder.h" #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) class FlowLayout final : public QLayout { Q_OBJECT 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; }; /*! \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; /*! \fn template LayoutItem(const T &t) Constructs a layout item proxy for \a t. T could be \list \li \c {QString} \li \c {QWidget *} \li \c {QLayout *} \endlist */ struct ResultItem { ResultItem() = default; explicit ResultItem(QLayout *l) : layout(l) {} explicit ResultItem(QWidget *w) : widget(w) {} QString text; QLayout *layout = nullptr; QWidget *widget = nullptr; int space = -1; int stretch = -1; int span = 1; }; struct Slice { Slice() = default; Slice(QLayout *l) : layout(l) {} Slice(QWidget *w) : widget(w) {} QLayout *layout = nullptr; QWidget *widget = nullptr; void flush(); // Grid-specific int currentGridColumn = 0; int currentGridRow = 0; bool isFormAlignment = false; Qt::Alignment align = {}; // Can be changed to // Grid or Form QList pendingItems; }; static QWidget *widgetForItem(QLayoutItem *item) { if (QWidget *w = item->widget()) return w; if (item->spacerItem()) return nullptr; QLayout *l = item->layout(); if (!l) return nullptr; 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 ResultItem &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 { QTC_CHECK(false); } } static void addItemToFlowLayout(FlowLayout *layout, const ResultItem &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.text.isEmpty()) { layout->addWidget(createLabel(item.text)); } else { QTC_CHECK(false); } } void Slice::flush() { if (pendingItems.empty()) return; if (auto formLayout = qobject_cast(layout)) { // If there are more than two items, we cram the last ones in one hbox. 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.append(ResultItem(hbox)); } if (pendingItems.size() == 1) { // One one item given, so this spans both columns. const ResultItem &f0 = pendingItems.at(0); if (auto layout = f0.layout) formLayout->addRow(layout); else if (auto widget = f0.widget) formLayout->addRow(widget); } else if (pendingItems.size() == 2) { // Normal case, both columns used. ResultItem &f1 = pendingItems[1]; const ResultItem &f0 = pendingItems.at(0); if (!f1.widget && !f1.layout && !f1.text.isEmpty()) f1.widget = createLabel(f1.text); if (f0.widget) { if (f1.layout) formLayout->addRow(f0.widget, f1.layout); else if (f1.widget) formLayout->addRow(f0.widget, f1.widget); } else { if (f1.layout) formLayout->addRow(f0.text, f1.layout); else if (f1.widget) formLayout->addRow(f0.text, f1.widget); } } else { QTC_CHECK(false); } // Set up label as buddy if possible. const int lastRow = formLayout->rowCount() - 1; QLayoutItem *l = formLayout->itemAt(lastRow, QFormLayout::LabelRole); QLayoutItem *f = formLayout->itemAt(lastRow, QFormLayout::FieldRole); if (l && f) { if (QLabel *label = qobject_cast(l->widget())) { if (QWidget *widget = widgetForItem(f)) label->setBuddy(widget); } } } else if (auto gridLayout = qobject_cast(layout)) { for (const ResultItem &item : std::as_const(pendingItems)) { Qt::Alignment a = currentGridColumn == 0 ? align : Qt::Alignment(); if (item.widget) gridLayout->addWidget(item.widget, currentGridRow, currentGridColumn, 1, item.span, a); else if (item.layout) gridLayout->addLayout(item.layout, currentGridRow, currentGridColumn, 1, item.span, a); else if (!item.text.isEmpty()) gridLayout->addWidget(createLabel(item.text), currentGridRow, currentGridColumn, 1, 1, a); currentGridColumn += item.span; } ++currentGridRow; currentGridColumn = 0; } else if (auto boxLayout = qobject_cast(layout)) { for (const ResultItem &item : std::as_const(pendingItems)) addItemToBoxLayout(boxLayout, item); } else if (auto flowLayout = qobject_cast(layout)) { for (const ResultItem &item : std::as_const(pendingItems)) addItemToFlowLayout(flowLayout, item); } else if (auto stackLayout = qobject_cast(layout)) { for (const ResultItem &item : std::as_const(pendingItems)) { if (item.widget) stackLayout->addWidget(item.widget); else QTC_CHECK(false); } } else { QTC_CHECK(false); } pendingItems.clear(); } // LayoutBuilder class LayoutBuilder { Q_DISABLE_COPY_MOVE(LayoutBuilder) public: LayoutBuilder(); ~LayoutBuilder(); void addItem(const LayoutItem &item); void addItems(const LayoutItems &items); QList stack; }; static void addItemHelper(LayoutBuilder &builder, const LayoutItem &item) { if (item.onAdd) item.onAdd(builder); if (item.setter) { if (QWidget *widget = builder.stack.last().widget) item.setter(widget); else if (QLayout *layout = builder.stack.last().layout) item.setter(layout); else QTC_CHECK(false); } for (const LayoutItem &subItem : item.subItems) addItemHelper(builder, subItem); if (item.onExit) item.onExit(builder); } void doAddText(LayoutBuilder &builder, const QString &text) { ResultItem fi; fi.text = text; builder.stack.last().pendingItems.append(fi); } 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) { ResultItem fi; fi.stretch = stretch.stretch; builder.stack.last().pendingItems.append(fi); } void doAddLayout(LayoutBuilder &builder, QLayout *layout) { builder.stack.last().pendingItems.append(ResultItem(layout)); } void doAddWidget(LayoutBuilder &builder, QWidget *widget) { builder.stack.last().pendingItems.append(ResultItem(widget)); } /*! \class Layouting::Space \inmodule QtCreator \brief The Layouting::Space class represents some empty space in a layout. */ /*! \class Layouting::Stretch \inmodule QtCreator \brief The Layouting::Stretch class represents some stretch in a layout. */ /*! \class LayoutBuilder \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() */ LayoutBuilder::LayoutBuilder() = default; /*! Destructs a layout builder. */ LayoutBuilder::~LayoutBuilder() = default; void LayoutBuilder::addItem(const LayoutItem &item) { addItemHelper(*this, item); } void LayoutBuilder::addItems(const LayoutItems &items) { for (const LayoutItem &item : items) addItemHelper(*this, item); } /*! This 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); } /*! Attach the constructed layout to the provided \c QWidget \a parent. 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); } QWidget *LayoutItem::emerge() { auto w = new QWidget; attachTo(w); return w; } static void layoutExit(LayoutBuilder &builder) { builder.stack.last().flush(); QLayout *layout = builder.stack.last().layout; builder.stack.pop_back(); if (QWidget *widget = builder.stack.last().widget) widget->setLayout(layout); else builder.stack.last().pendingItems.append(ResultItem(layout)); } static void widgetExit(LayoutBuilder &builder) { QWidget *widget = builder.stack.last().widget; builder.stack.pop_back(); builder.stack.last().pendingItems.append(ResultItem(widget)); } Column::Column(std::initializer_list items) { subItems = items; onAdd = [](LayoutBuilder &builder) { builder.stack.append(new QVBoxLayout); }; onExit = layoutExit; } Row::Row(std::initializer_list items) { subItems = items; onAdd = [](LayoutBuilder &builder) { builder.stack.append(new QHBoxLayout); }; onExit = layoutExit; } Flow::Flow(std::initializer_list items) { subItems = items; onAdd = [](LayoutBuilder &builder) { builder.stack.append(new FlowLayout); }; onExit = layoutExit; } Grid::Grid(std::initializer_list items) { subItems = items; onAdd = [](LayoutBuilder &builder) { builder.stack.append(new QGridLayout); }; onExit = layoutExit; } static QFormLayout *newFormLayout() { auto formLayout = new QFormLayout; formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); return formLayout; } Form::Form(std::initializer_list items) { subItems = items; onAdd = [](LayoutBuilder &builder) { builder.stack.append(newFormLayout()); }; onExit = layoutExit; } Stack::Stack(std::initializer_list items) { subItems = items; onAdd = [](LayoutBuilder &builder) { builder.stack.append(new QStackedLayout); }; onExit = layoutExit; } LayoutItem br() { LayoutItem item; item.onAdd = [](LayoutBuilder &builder) { builder.stack.last().flush(); }; return item; } LayoutItem empty() { LayoutItem item; item.onAdd = [](LayoutBuilder &builder) { ResultItem ri; ri.span = 1; builder.stack.last().pendingItems.append(ResultItem()); }; return item; } LayoutItem hr() { LayoutItem item; item.onAdd = [](LayoutBuilder &builder) { doAddWidget(builder, createHr()); }; return item; } LayoutItem st() { LayoutItem item; item.onAdd = [](LayoutBuilder &builder) { doAddStretch(builder, Stretch(1)); }; return item; } LayoutItem noMargin() { LayoutItem item; item.onAdd = [](LayoutBuilder &builder) { if (auto layout = builder.stack.last().layout) layout->setContentsMargins(0, 0, 0, 0); else if (auto widget = builder.stack.last().widget) widget->setContentsMargins(0, 0, 0, 0); }; return item; } LayoutItem normalMargin() { LayoutItem item; item.onAdd = [](LayoutBuilder &builder) { if (auto layout = builder.stack.last().layout) layout->setContentsMargins(9, 9, 9, 9); else if (auto widget = builder.stack.last().widget) widget->setContentsMargins(9, 9, 9, 9); }; return item; } 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; } // "Widgets" template void setupWidget(LayoutItem *item) { item->onAdd = [](LayoutBuilder &builder) { builder.stack.append(new T); }; item->onExit = widgetExit; }; Widget::Widget(std::initializer_list items) { this->subItems = items; setupWidget(this); } Group::Group(std::initializer_list items) { this->subItems = items; setupWidget(this); } PushButton::PushButton(std::initializer_list items) { this->subItems = items; setupWidget(this); } SpinBox::SpinBox(std::initializer_list items) { this->subItems = items; setupWidget(this); } TextEdit::TextEdit(std::initializer_list items) { this->subItems = items; setupWidget(this); } Splitter::Splitter(std::initializer_list items) { subItems = items; onAdd = [](LayoutBuilder &builder) { auto splitter = new QSplitter; splitter->setOrientation(Qt::Vertical); builder.stack.append(splitter); }; onExit = [](LayoutBuilder &builder) { const Slice slice = builder.stack.last(); QSplitter *splitter = qobject_cast(slice.widget); for (const ResultItem &ri : slice.pendingItems) { if (ri.widget) splitter->addWidget(ri.widget); } builder.stack.pop_back(); builder.stack.last().pendingItems.append(ResultItem(splitter)); }; } TabWidget::TabWidget(std::initializer_list items) { this->subItems = items; setupWidget(this); } // 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 Application Application::Application(std::initializer_list items) { subItems = items; setupWidget(this); onExit = {}; // Hack: Don't dropp the last slice, we need the resulting widget. } int Application::exec(int &argc, char *argv[]) { QApplication app(argc, argv); LayoutBuilder builder; addItemHelper(builder, *this); if (QWidget *widget = builder.stack.last().widget) widget->show(); return app.exec(); } // "Properties" LayoutItem title(const QString &title) { return [title](QObject *target) { if (auto groupBox = qobject_cast(target)) { groupBox->setTitle(title); groupBox->setObjectName(title); } else if (auto widget = qobject_cast(target)) { widget->setWindowTitle(title); } else { QTC_CHECK(false); } }; } LayoutItem text(const QString &text) { return [text](QObject *target) { if (auto button = qobject_cast(target)) { button->setText(text); } else if (auto textEdit = qobject_cast(target)) { textEdit->setText(text); } else { QTC_CHECK(false); } }; } LayoutItem tooltip(const QString &toolTip) { return [toolTip](QObject *target) { if (auto widget = qobject_cast(target)) { widget->setToolTip(toolTip); } else { QTC_CHECK(false); } }; } LayoutItem spacing(int spacing) { return [spacing](QObject *target) { if (auto layout = qobject_cast(target)) { layout->setSpacing(spacing); } else { QTC_CHECK(false); } }; } LayoutItem resize(int w, int h) { return [w, h](QObject *target) { if (auto widget = qobject_cast(target)) { widget->resize(w, h); } 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); } }; } // Id based setters LayoutItem id(ID &out) { return [&out](QObject *target) { out.ob = target; }; } void setText(ID id, const QString &text) { if (auto textEdit = qobject_cast(id.ob)) textEdit->setText(text); } // Signals LayoutItem onClicked(const std::function &func, QObject *guard) { return [func, guard](QObject *target) { if (auto button = qobject_cast(target)) { QObject::connect(button, &QAbstractButton::clicked, guard ? guard : target, func); } else { QTC_CHECK(false); } }; } LayoutItem onTextChanged(const std::function &func, QObject *guard) { return [func, guard](QObject *target) { if (auto button = qobject_cast(target)) { QObject::connect(button, &QSpinBox::textChanged, guard ? guard : target, func); } else { QTC_CHECK(false); } }; } LayoutItem onValueChanged(const std::function &func, QObject *guard) { return [func, guard](QObject *target) { if (auto button = qobject_cast(target)) { QObject::connect(button, &QSpinBox::valueChanged, guard ? guard : target, func); } else { QTC_CHECK(false); } }; } // Convenience QWidget *createHr(QWidget *parent) { auto frame = new QFrame(parent); frame->setFrameShape(QFrame::HLine); frame->setFrameShadow(QFrame::Sunken); return frame; } // Singletons. LayoutItem::LayoutItem(const LayoutItem &t) { operator=(t); } void createItem(LayoutItem *item, LayoutItem(*t)()) { *item = t(); } void createItem(LayoutItem *item, const std::function &t) { item->setter = t; } void createItem(LayoutItem *item, QWidget *t) { item->onAdd = [t](LayoutBuilder &builder) { doAddWidget(builder, t); }; } void createItem(LayoutItem *item, QLayout *t) { item->onAdd = [t](LayoutBuilder &builder) { doAddLayout(builder, t); }; } void createItem(LayoutItem *item, const QString &t) { item->onAdd = [t](LayoutBuilder &builder) { doAddText(builder, t); }; } void createItem(LayoutItem *item, const Space &t) { item->onAdd = [t](LayoutBuilder &builder) { doAddSpace(builder, t); }; } void createItem(LayoutItem *item, const Stretch &t) { item->onAdd = [t](LayoutBuilder &builder) { doAddStretch(builder, t); }; } void createItem(LayoutItem *item, const Span &t) { item->onAdd = [t](LayoutBuilder &builder) { addItemHelper(builder, t.item); builder.stack.last().pendingItems.last().span = t.span; }; } } // Layouting #include "layoutbuilder.moc"