widgets: add wrapper components and managers

This commit is contained in:
outfoxxed 2024-11-19 02:02:55 -08:00
parent 79fca3cab8
commit 401ee4cec6
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
8 changed files with 578 additions and 0 deletions

View file

@ -1,5 +1,7 @@
qt_add_library(quickshell-widgets STATIC
cliprect.cpp
wrapper.cpp
marginwrapper.cpp
)
qt_add_qml_module(quickshell-widgets
@ -8,6 +10,8 @@ qt_add_qml_module(quickshell-widgets
QML_FILES
IconImage.qml
ClippingRectangle.qml
WrapperItem.qml
WrapperRectangle.qml
)
qt6_add_shaders(quickshell-widgets "widgets-cliprect"

View file

@ -0,0 +1,44 @@
import QtQuick
import Quickshell.Widgets
///! Item that handles sizes and positioning for a single visual child.
/// This component is useful when you need to wrap a single component in
/// an item, or give a single component a margin. See [QtQuick.Layouts]
/// for positioning multiple items.
///
/// > [!NOTE] WrapperItem is a @@MarginWrapperManager based component.
/// > You should read its documentation as well.
///
/// ### Example: Adding a margin to an item
/// The snippet below adds a 10px margin to all sides of the @@QtQuick.Text item.
///
/// ```qml
/// WrapperItem {
/// margin: 10
///
/// @@QtQuick.Text { text: "Hello!" }
/// }
/// ```
///
/// > [!NOTE] The child item can be specified by writing it inline in the wrapper,
/// > as in the example above, or by using the @@child property. See
/// > @@WrapperManager.child for details.
///
/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width,
/// > @@Item.height or @@Item.anchors on the child item, as they are used
/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and
/// > @@Item.implicitHeight.
///
/// [QtQuick.Layouts]: https://doc.qt.io/qt-6/qtquicklayouts-index.html
Item {
/// The minimum margin between the child item and the WrapperItem's edges.
/// Defaults to 0.
property /*real*/alias margin: manager.margin
/// If the child item should be resized larger than its implicit size if
/// the WrapperItem is resized larger than its implicit size. Defaults to false.
property /*bool*/alias resizeChild: manager.resizeChild
/// See @@WrapperManager.child for details.
property /*Item*/alias child: manager.child
MarginWrapperManager { id: manager }
}

View file

@ -0,0 +1,35 @@
import QtQuick
import Quickshell.Widgets
///! Rectangle that handles sizes and positioning for a single visual child.
/// This component is useful for adding a border or background rectangle to
/// a child item.
///
/// > [!NOTE] WrapperRectangle is a @@MarginWrapperManager based component.
/// > You should read its documentation as well.
///
/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width,
/// > @@Item.height or @@Item.anchors on the child item, as they are used
/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and
/// > @@Item.implicitHeight.
Rectangle {
id: root
/// If true (default), the rectangle's border width will be added
/// to the margin.
property bool contentInsideBorder: true
/// The minimum margin between the child item and the WrapperRectangle's
/// edges. If @@contentInsideBorder is true, this excludes the border,
/// otherwise it includes it. Defaults to 0.
property real margin: 0
/// If the child item should be resized larger than its implicit size if
/// the WrapperRectangle is resized larger than its implicit size. Defaults to false.
property /*bool*/alias resizeChild: manager.resizeChild
/// See @@WrapperManager.child for details.
property alias child: manager.child
MarginWrapperManager {
id: manager
margin: (root.contentInsideBorder ? root.border.width : 0) + root.margin
}
}

View file

@ -0,0 +1,146 @@
#include "marginwrapper.hpp"
#include <algorithm>
#include <qobject.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "wrapper.hpp"
namespace qs::widgets {
MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(parent) {
QObject::connect(
this,
&WrapperManager::initializedChildChanged,
this,
&MarginWrapperManager::onChildChanged
);
}
void MarginWrapperManager::componentComplete() {
if (this->mWrapper) {
QObject::connect(
this->mWrapper,
&QQuickItem::widthChanged,
this,
&MarginWrapperManager::onWrapperWidthChanged
);
QObject::connect(
this->mWrapper,
&QQuickItem::heightChanged,
this,
&MarginWrapperManager::onWrapperHeightChanged
);
}
this->WrapperManager::componentComplete();
if (!this->mChild) this->updateGeometry();
}
qreal MarginWrapperManager::margin() const { return this->mMargin; }
void MarginWrapperManager::setMargin(qreal margin) {
if (margin == this->mMargin) return;
this->mMargin = margin;
this->updateGeometry();
emit this->marginChanged();
}
bool MarginWrapperManager::resizeChild() const { return this->mResizeChild; }
void MarginWrapperManager::setResizeChild(bool resizeChild) {
if (resizeChild == this->mResizeChild) return;
this->mResizeChild = resizeChild;
this->updateGeometry();
emit this->resizeChildChanged();
}
void MarginWrapperManager::onChildChanged() {
// QObject::disconnect in MarginWrapper handles disconnecting old item
if (this->mChild) {
QObject::connect(
this->mChild,
&QQuickItem::implicitWidthChanged,
this,
&MarginWrapperManager::onChildImplicitWidthChanged
);
QObject::connect(
this->mChild,
&QQuickItem::implicitHeightChanged,
this,
&MarginWrapperManager::onChildImplicitHeightChanged
);
}
this->updateGeometry();
}
qreal MarginWrapperManager::targetChildWidth() const {
auto max = this->mWrapper->width() - this->mMargin * 2;
if (this->mResizeChild) return max;
else return std::min(this->mChild->implicitWidth(), max);
}
qreal MarginWrapperManager::targetChildHeight() const {
auto max = this->mWrapper->height() - this->mMargin * 2;
if (this->mResizeChild) return max;
else return std::min(this->mChild->implicitHeight(), max);
}
qreal MarginWrapperManager::targetChildX() const {
if (this->mResizeChild) return this->mMargin;
else return this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2;
}
qreal MarginWrapperManager::targetChildY() const {
if (this->mResizeChild) return this->mMargin;
else return this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2;
}
void MarginWrapperManager::onWrapperWidthChanged() {
if (!this->mChild || !this->mWrapper) return;
this->mChild->setX(this->targetChildX());
this->mChild->setWidth(this->targetChildWidth());
}
void MarginWrapperManager::onWrapperHeightChanged() {
if (!this->mChild || !this->mWrapper) return;
this->mChild->setY(this->targetChildY());
this->mChild->setHeight(this->targetChildHeight());
}
void MarginWrapperManager::onChildImplicitWidthChanged() {
if (!this->mChild || !this->mWrapper) return;
this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2);
}
void MarginWrapperManager::onChildImplicitHeightChanged() {
if (!this->mChild || !this->mWrapper) return;
this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2);
}
void MarginWrapperManager::updateGeometry() {
if (!this->mWrapper) return;
if (this->mChild) {
this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2);
this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2);
this->mChild->setX(this->targetChildX());
this->mChild->setY(this->targetChildY());
this->mChild->setWidth(this->targetChildWidth());
this->mChild->setHeight(this->targetChildHeight());
} else {
this->mWrapper->setImplicitWidth(this->mMargin * 2);
this->mWrapper->setImplicitHeight(this->mMargin * 2);
}
}
} // namespace qs::widgets

View file

@ -0,0 +1,75 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "wrapper.hpp"
namespace qs::widgets {
///! Helper object for applying sizes and margins to a single child item.
/// > [!NOTE] MarginWrapperManager is an extension of @@WrapperManager.
/// > You should read its documentation to understand wrapper types.
///
/// MarginWrapperManager can be used to apply margins to a child item,
/// in addition to handling the size / implicit size relationship
/// between the parent and the child. @@WrapperItem and @@WrapperRectangle
/// exist for Item and Rectangle implementations respectively.
///
/// > [!WARNING] MarginWrapperManager based types set the child item's
/// > @@QtQuick.Item.x, @@QtQuick.Item.y, @@QtQuick.Item.width, @@QtQuick.Item.height
/// > or @@QtQuick.Item.anchors properties. Do not set them yourself,
/// > instead set @@Item.implicitWidth and @@Item.implicitHeight.
///
/// ### Implementing a margin wrapper type
/// Follow the directions in @@WrapperManager$'s documentation, and or
/// alias the @@margin property if you wish to expose it.
class MarginWrapperManager: public WrapperManager {
Q_OBJECT;
// clang-format off
/// The minimum margin between the child item and the parent item's edges.
/// Defaults to 0.
Q_PROPERTY(qreal margin READ margin WRITE setMargin NOTIFY marginChanged FINAL);
/// If the child item should be resized larger than its implicit size if
/// the parent is resized larger than its implicit size. Defaults to false.
Q_PROPERTY(bool resizeChild READ resizeChild WRITE setResizeChild NOTIFY resizeChildChanged FINAL);
// clang-format on
QML_ELEMENT;
public:
explicit MarginWrapperManager(QObject* parent = nullptr);
void componentComplete() override;
[[nodiscard]] qreal margin() const;
void setMargin(qreal margin);
[[nodiscard]] bool resizeChild() const;
void setResizeChild(bool resizeChild);
signals:
void marginChanged();
void resizeChildChanged();
private slots:
void onChildChanged();
void onWrapperWidthChanged();
void onWrapperHeightChanged();
void onChildImplicitWidthChanged();
void onChildImplicitHeightChanged();
private:
void updateGeometry();
[[nodiscard]] qreal targetChildX() const;
[[nodiscard]] qreal targetChildY() const;
[[nodiscard]] qreal targetChildWidth() const;
[[nodiscard]] qreal targetChildHeight() const;
qreal mMargin = 0;
bool mResizeChild = false;
};
} // namespace qs::widgets

View file

@ -1,7 +1,15 @@
name = "Quickshell.Widgets"
description = "Bundled widgets"
headers = [
"wrapper.hpp",
"marginwrapper.hpp",
]
qml_files = [
"IconImage.qml",
"ClippingRectangle.qml",
"WrapperItem.qml",
"WrapperRectangle.qml",
]
-----

127
src/widgets/wrapper.cpp Normal file
View file

@ -0,0 +1,127 @@
#include "wrapper.hpp"
#include <QtQml/qqmlinfo.h>
#include <QtQml/qqmllist.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
namespace qs::widgets {
void WrapperManager::componentComplete() {
this->mWrapper = qobject_cast<QQuickItem*>(this->parent());
if (!this->mWrapper) {
QString pstr;
QDebug(&pstr) << this->parent();
qmlWarning(this) << "Parent of WrapperManager is not a QQuickItem. Parent: " << pstr;
return;
}
QQuickItem* child = this->mChild;
this->mChild = nullptr; // avoids checks for the old item in setChild.
const auto& childItems = this->mWrapper->childItems();
if (childItems.length() == 1) {
this->mDefaultChild = childItems.first();
} else if (childItems.length() != 0) {
this->flags.setFlag(WrapperManager::HasMultipleChildren);
if (!child && !this->flags.testFlags(WrapperManager::NullChild)) {
this->printChildCountWarning();
}
}
for (auto* item: childItems) {
if (item != child) item->setParentItem(nullptr);
}
if (child && !this->flags.testFlag(WrapperManager::NullChild)) {
this->setChild(child);
}
}
QQuickItem* WrapperManager::child() const { return this->mChild; }
void WrapperManager::setChild(QQuickItem* child) {
if (child && child == this->mChild) return;
if (this->mChild != nullptr) {
QObject::disconnect(this->mChild, nullptr, this, nullptr);
if (this->mChild->parentItem() == this->mWrapper) {
this->mChild->setParentItem(nullptr);
}
}
this->mChild = child;
this->flags.setFlag(WrapperManager::NullChild, child == nullptr);
if (child) {
QObject::connect(
child,
&QObject::destroyed,
this,
&WrapperManager::onChildDestroyed,
Qt::UniqueConnection
);
if (auto* wrapper = this->mWrapper) {
child->setParentItem(wrapper);
}
}
emit this->initializedChildChanged();
emit this->childChanged();
}
void WrapperManager::setProspectiveChild(QQuickItem* child) {
if (child && child == this->mChild) return;
if (!this->mWrapper) {
if (this->mChild) {
QObject::disconnect(this->mChild, nullptr, this, nullptr);
}
this->mChild = child;
this->flags.setFlag(WrapperManager::NullChild, child == nullptr);
if (child) {
QObject::connect(child, &QObject::destroyed, this, &WrapperManager::onChildDestroyed);
}
} else {
this->setChild(child);
}
}
void WrapperManager::unsetChild() {
if (!this->mWrapper) {
this->setProspectiveChild(nullptr);
} else {
this->setChild(this->mDefaultChild);
if (!this->mDefaultChild && this->flags.testFlag(WrapperManager::HasMultipleChildren)) {
this->printChildCountWarning();
}
}
this->flags.setFlag(WrapperManager::NullChild, false);
}
void WrapperManager::onChildDestroyed() {
this->mChild = nullptr;
this->unsetChild();
emit this->childChanged();
}
void WrapperManager::printChildCountWarning() const {
qmlWarning(this->mWrapper) << "Wrapper component cannot have more than one visual child.";
qmlWarning(this->mWrapper) << "Remove all additional children, or pick a specific component "
"to wrap using the child property.";
}
} // namespace qs::widgets

139
src/widgets/wrapper.hpp Normal file
View file

@ -0,0 +1,139 @@
#pragma once
#include <qflags.h>
#include <qobject.h>
#include <qpointer.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qqmlparserstatus.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/doc.hpp"
namespace qs::widgets {
///! Helper object for creating components with a single visual child.
/// WrapperManager determines which child of an Item should be its visual
/// child, and exposes it for further operations. See @@MarginWrapperManager
/// for a subclass that implements automatic sizing and margins.
///
/// ### Using wrapper types
/// WrapperManager based types have a single visual child item.
/// You can specify the child item using the default property, or by
/// setting the @@child property. You must use the @@child property if
/// the widget has more than one @@QtQuick.Item based child.
///
/// #### Example using the default property
/// ```qml
/// WrapperWidget { // a widget that uses WrapperManager
/// // Putting the item inline uses the default property of WrapperWidget.
/// @@QtQuick.Text { text: "Hello" }
///
/// // Scope does not extend Item, so it can be placed in the
/// // default property without issue.
/// @@Quickshell.Scope {}
/// }
/// ```
///
/// #### Example using the child property
/// ```qml
/// WrapperWidget {
/// @@QtQuick.Text {
/// id: text
/// text: "Hello"
/// }
///
/// @@QtQuick.Text {
/// id: otherText
/// text: "Other Text"
/// }
///
/// // Both text and otherText extend Item, so one must be specified.
/// child: text
/// }
/// ```
///
/// See @@child for more details on how the child property can be used.
///
/// ### Implementing wrapper types
/// In addition to the bundled wrapper types, you can make your own using
/// WrapperManager. To implement a wrapper, create a WrapperManager inside
/// your wrapper component 's default property, then alias a new property
/// to the WrapperManager's @@child property.
///
/// #### Example
/// ```qml
/// Item { // your wrapper component
/// WrapperManager { id: wrapperManager }
///
/// // Allows consumers of your wrapper component to use the child property.
/// property alias child: wrapperManager.child
///
/// // The rest of your component logic. You can use
/// // `wrapperManager.child` or `this.child` to refer to the selected child.
/// }
/// ```
///
/// ### See also
/// - @@WrapperItem - A @@MarginWrapperManager based component that sizes itself
/// to its child.
/// - @@WrapperRectangle - A @@MarginWrapperManager based component that sizes
/// itself to its child, and provides an option to use its border as an inset.
class WrapperManager
: public QObject
, public QQmlParserStatus {
Q_OBJECT;
// clang-format off
/// The wrapper component's selected child.
///
/// Setting this property override's WrapperManager's default selection,
/// and resolve ambiguity when more than one visual child is present.
/// The property can additionally be defined inline or reference a component
/// that is not already a child of the wrapper, in which case it will be
/// reparented to the wrapper. Setting child to `null` will select no child,
/// and `undefined` will restore the default child.
///
/// When read, `child` will always return the (potentially null) selected child,
/// and not `undefined`.
Q_PROPERTY(QQuickItem* child READ child WRITE setProspectiveChild RESET unsetChild NOTIFY childChanged FINAL);
// clang-format on
QML_ELEMENT;
public:
explicit WrapperManager(QObject* parent = nullptr): QObject(parent) {}
void classBegin() override {}
void componentComplete() override;
[[nodiscard]] QQuickItem* child() const;
void setChild(QQuickItem* child);
void setProspectiveChild(QQuickItem* child);
void unsetChild();
signals:
void childChanged();
QSDOC_HIDE void initializedChildChanged();
private slots:
void onChildDestroyed();
protected:
enum Flag : quint8 {
NoFlags = 0x0,
NullChild = 0x1,
HasMultipleChildren = 0x2,
};
Q_DECLARE_FLAGS(Flags, Flag);
void printChildCountWarning() const;
void updateGeometry();
QQuickItem* mWrapper = nullptr;
QPointer<QQuickItem> mDefaultChild;
QQuickItem* mChild = nullptr;
Flags flags;
};
} // namespace qs::widgets