From 2d733384f030bb4354be61e054bb68bdf9ecfb85 Mon Sep 17 00:00:00 2001 From: reionwong Date: Thu, 4 Nov 2021 00:35:58 +0800 Subject: [PATCH] Add drag and drop plugins --- CMakeLists.txt | 4 + draganddrop/declarativedragdropevent.cpp | 55 ++++++++ draganddrop/declarativedragdropevent.h | 121 ++++++++++++++++++ draganddrop/declarativedroparea.cpp | 148 ++++++++++++++++++++++ draganddrop/declarativedroparea.h | 95 ++++++++++++++ draganddrop/declarativemimedata.cpp | 153 +++++++++++++++++++++++ draganddrop/declarativemimedata.h | 100 +++++++++++++++ main.cpp | 14 +++ qml/Dialogs/PropertiesDialog.qml | 8 +- 9 files changed, 694 insertions(+), 4 deletions(-) create mode 100644 draganddrop/declarativedragdropevent.cpp create mode 100644 draganddrop/declarativedragdropevent.h create mode 100644 draganddrop/declarativedroparea.cpp create mode 100644 draganddrop/declarativedroparea.h create mode 100644 draganddrop/declarativemimedata.cpp create mode 100644 draganddrop/declarativemimedata.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a92f788..ab9a8d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,10 @@ add_executable(cutefish-filemanager window.cpp dbusinterface.cpp + draganddrop/declarativedroparea.cpp + draganddrop/declarativedragdropevent.cpp + draganddrop/declarativemimedata.cpp + model/foldermodel.cpp model/placesmodel.cpp model/placesitem.cpp diff --git a/draganddrop/declarativedragdropevent.cpp b/draganddrop/declarativedragdropevent.cpp new file mode 100644 index 0000000..ef0d398 --- /dev/null +++ b/draganddrop/declarativedragdropevent.cpp @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2010 BetterInbox + SPDX-FileContributor: Gregory Schlomoff + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: MIT +*/ + +#include "declarativedragdropevent.h" + +DeclarativeDragDropEvent::DeclarativeDragDropEvent(QDropEvent *e, DeclarativeDropArea *parent) + : QObject(parent) + , m_x(e->pos().x()) + , m_y(e->pos().y()) + , m_buttons(e->mouseButtons()) + , m_modifiers(e->keyboardModifiers()) + , m_data(nullptr) + , m_event(e) +{ +} + +DeclarativeDragDropEvent::DeclarativeDragDropEvent(QDragLeaveEvent *e, DeclarativeDropArea *parent) + : QObject(parent) + , m_x(0) + , m_y(0) + , m_buttons(Qt::NoButton) + , m_modifiers(Qt::NoModifier) + , m_data(nullptr) + , m_event(nullptr) +{ + Q_UNUSED(e); +} + +void DeclarativeDragDropEvent::accept(int action) +{ + m_event->setDropAction(static_cast(action)); + // qDebug() << "-----> Accepting event: " << this << m_data.urls() << m_data.text() << m_data.html() << ( m_data.hasColor() ? m_data.color().name() : " + // no color"); + m_event->accept(); +} + +void DeclarativeDragDropEvent::ignore() +{ + m_event->ignore(); +} + +DeclarativeMimeData *DeclarativeDragDropEvent::mimeData() +{ + if (!m_data && m_event) { + // TODO This should be using MimeDataWrapper eventually, although this is an API break, + // so will need to be done carefully. + m_data.reset(new DeclarativeMimeData(m_event->mimeData())); + } + return m_data.data(); +} diff --git a/draganddrop/declarativedragdropevent.h b/draganddrop/declarativedragdropevent.h new file mode 100644 index 0000000..c453f3c --- /dev/null +++ b/draganddrop/declarativedragdropevent.h @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2010 BetterInbox + SPDX-FileContributor: Gregory Schlomoff + + SPDX-License-Identifier: MIT +*/ + +#ifndef DECLARATIVEDRAGDROPEVENT_H +#define DECLARATIVEDRAGDROPEVENT_H + +#include "declarativedroparea.h" +#include "declarativemimedata.h" +#include + +class DeclarativeDragDropEvent : public QObject +{ + Q_OBJECT + + /** + * The mouse X position of the event relative to the DropArea that is receiving the event. + */ + Q_PROPERTY(int x READ x) + + /** + * The mouse Y position of the event relative to the DropArea that is receiving the event. + */ + Q_PROPERTY(int y READ y) + + /** + * The pressed mouse buttons. + * A combination of: + * Qt.NoButton The button state does not refer to any button (see QMouseEvent::button()). + * Qt.LeftButton The left button is pressed, or an event refers to the left button. (The left button may be the right button on left-handed mice.) + * Qt.RightButton The right button. + * Qt.MidButton The middle button. + * Qt.MiddleButton MidButton The middle button. + * Qt.XButton1 The first X button. + * Qt.XButton2 The second X button. + */ + Q_PROPERTY(int buttons READ buttons) + + /** + * Pressed keyboard modifiers, a combination of: + * Qt.NoModifier No modifier key is pressed. + * Qt.ShiftModifier A Shift key on the keyboard is pressed. + * Qt.ControlModifier A Ctrl key on the keyboard is pressed. + * Qt.AltModifier An Alt key on the keyboard is pressed. + * Qt.MetaModifier A Meta key on the keyboard is pressed. + * Qt.KeypadModifier A keypad button is pressed. + * Qt.GroupSwitchModifier X11 only. A Mode_switch key on the keyboard is pressed. + */ + Q_PROPERTY(int modifiers READ modifiers) + + /** + * The mime data of this operation + * @see DeclarativeMimeData + */ + Q_PROPERTY(DeclarativeMimeData *mimeData READ mimeData) + + /** + * The possible different kind of action that can be done in the drop, is a combination of: + * Qt.CopyAction 0x1 Copy the data to the target. + * Qt.MoveAction 0x2 Move the data from the source to the target. + * Qt.LinkAction 0x4 Create a link from the source to the target. + * Qt.ActionMask 0xff + * Qt.IgnoreAction 0x0 Ignore the action (do nothing with the data). + * Qt.TargetMoveAction 0x8002 On Windows, this value is used when the ownership of the D&D data should be taken over by the target application, i.e., the + * source application should not delete the data. On X11 this value is used to do a move. TargetMoveAction is not used on the Mac. + */ + Q_PROPERTY(Qt::DropActions possibleActions READ possibleActions) + + /** + * Default action + * @see possibleActions + */ + Q_PROPERTY(Qt::DropAction proposedAction READ proposedAction) + +public: + DeclarativeDragDropEvent(QDropEvent *e, DeclarativeDropArea *parent = nullptr); + DeclarativeDragDropEvent(QDragLeaveEvent *e, DeclarativeDropArea *parent = nullptr); + + int x() const + { + return m_x; + } + int y() const + { + return m_y; + } + int buttons() const + { + return m_buttons; + } + int modifiers() const + { + return m_modifiers; + } + DeclarativeMimeData *mimeData(); + Qt::DropAction proposedAction() const + { + return m_event->proposedAction(); + } + Qt::DropActions possibleActions() const + { + return m_event->possibleActions(); + } + +public Q_SLOTS: + void accept(int action); + void ignore(); + +private: + int m_x; + int m_y; + Qt::MouseButtons m_buttons; + Qt::KeyboardModifiers m_modifiers; + QScopedPointer m_data; + QDropEvent *m_event; +}; + +#endif // DECLARATIVEDRAGDROPEVENT_H diff --git a/draganddrop/declarativedroparea.cpp b/draganddrop/declarativedroparea.cpp new file mode 100644 index 0000000..7bab563 --- /dev/null +++ b/draganddrop/declarativedroparea.cpp @@ -0,0 +1,148 @@ +/* + SPDX-FileCopyrightText: 2010 BetterInbox + SPDX-FileContributor: Gregory Schlomoff + + SPDX-License-Identifier: MIT +*/ + +#include "declarativedroparea.h" +#include "declarativedragdropevent.h" + +DeclarativeDropArea::DeclarativeDropArea(QQuickItem *parent) + : QQuickItem(parent) + , m_enabled(true) + , m_preventStealing(false) + , m_temporaryInhibition(false) + , m_containsDrag(false) +{ + setFlag(ItemAcceptsDrops, m_enabled); +} + +void DeclarativeDropArea::temporaryInhibitParent(bool inhibit) +{ + QQuickItem *candidate = parentItem(); + + while (candidate) { + if (DeclarativeDropArea *da = qobject_cast(candidate)) { + da->m_temporaryInhibition = inhibit; + if (inhibit) { + Q_EMIT da->dragLeaveEvent(nullptr); + } + } + candidate = candidate->parentItem(); + } +} + +void DeclarativeDropArea::dragEnterEvent(QDragEnterEvent *event) +{ + if (!m_enabled || m_temporaryInhibition) { + return; + } + + DeclarativeDragDropEvent dde(event, this); + event->accept(); + + Q_EMIT dragEnter(&dde); + + if (!event->isAccepted()) { + return; + } + + if (m_preventStealing) { + temporaryInhibitParent(true); + } + + m_oldDragMovePos = event->pos(); + setContainsDrag(true); +} + +void DeclarativeDropArea::dragLeaveEvent(QDragLeaveEvent *event) +{ + // do it anyways, in the unlikely case m_preventStealing + // was changed while drag + temporaryInhibitParent(false); + + m_oldDragMovePos = QPoint(-1, -1); + DeclarativeDragDropEvent dde(event, this); + Q_EMIT dragLeave(&dde); + setContainsDrag(false); +} + +void DeclarativeDropArea::dragMoveEvent(QDragMoveEvent *event) +{ + if (!m_enabled || m_temporaryInhibition) { + event->ignore(); + return; + } + event->accept(); + // if the position we export didn't change, don't generate the move event + if (event->pos() == m_oldDragMovePos) { + return; + } + + m_oldDragMovePos = event->pos(); + DeclarativeDragDropEvent dde(event, this); + Q_EMIT dragMove(&dde); +} + +void DeclarativeDropArea::dropEvent(QDropEvent *event) +{ + // do it anyways, in the unlikely case m_preventStealing + // was changed while drag, do it after a loop, + // so the parent dropevent doesn't get delivered + metaObject()->invokeMethod(this, "temporaryInhibitParent", Qt::QueuedConnection, Q_ARG(bool, false)); + + m_oldDragMovePos = QPoint(-1, -1); + + if (!m_enabled || m_temporaryInhibition) { + return; + } + + DeclarativeDragDropEvent dde(event, this); + Q_EMIT drop(&dde); + setContainsDrag(false); +} + +bool DeclarativeDropArea::isEnabled() const +{ + return m_enabled; +} + +void DeclarativeDropArea::setEnabled(bool enabled) +{ + if (enabled == m_enabled) { + return; + } + + m_enabled = enabled; + setFlag(ItemAcceptsDrops, m_enabled); + Q_EMIT enabledChanged(); +} + +bool DeclarativeDropArea::preventStealing() const +{ + return m_preventStealing; +} + +void DeclarativeDropArea::setPreventStealing(bool prevent) +{ + if (prevent == m_preventStealing) { + return; + } + + m_preventStealing = prevent; + Q_EMIT preventStealingChanged(); +} + +void DeclarativeDropArea::setContainsDrag(bool dragging) +{ + if (m_containsDrag != dragging) { + m_containsDrag = dragging; + Q_EMIT containsDragChanged(m_containsDrag); + } +} + +bool DeclarativeDropArea::containsDrag() const +{ + return m_containsDrag; +} diff --git a/draganddrop/declarativedroparea.h b/draganddrop/declarativedroparea.h new file mode 100644 index 0000000..265c893 --- /dev/null +++ b/draganddrop/declarativedroparea.h @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2010 BetterInbox + SPDX-FileContributor: Gregory Schlomoff + + SPDX-License-Identifier: MIT +*/ + +#ifndef DECLARATIVEDROPAREA_H +#define DECLARATIVEDROPAREA_H + +#include + +class DeclarativeDragDropEvent; + +class DeclarativeDropArea : public QQuickItem +{ + Q_OBJECT + + /** + * If false the area will receive no drop events + */ + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + + /** + * + */ + Q_PROPERTY(bool preventStealing READ preventStealing WRITE setPreventStealing NOTIFY preventStealingChanged) + + Q_PROPERTY(bool containsDrag READ containsDrag NOTIFY containsDragChanged) + +public: + DeclarativeDropArea(QQuickItem *parent = nullptr); + bool isEnabled() const; + void setEnabled(bool enabled); + + bool preventStealing() const; + void setPreventStealing(bool prevent); + bool containsDrag() const; + +Q_SIGNALS: + /** + * Emitted when the mouse cursor dragging something enters in the drag area + * @param event description of the dragged content + * @see DeclarativeDragDropEvent + */ + void dragEnter(DeclarativeDragDropEvent *event); + + /** + * Emitted when the mouse cursor dragging something leaves the drag area + * @param event description of the dragged content + * @see DeclarativeDragDropEvent + */ + void dragLeave(DeclarativeDragDropEvent *event); + + /** + * Emitted when the mouse cursor dragging something moves over the drag area + * @param event description of the dragged content + * @see DeclarativeDragDropEvent + */ + void dragMove(DeclarativeDragDropEvent *event); + + /** + * Emitted when the user drops something in the area + * @param event description of the dragged content + * @see DeclarativeDragDropEvent + */ + void drop(DeclarativeDragDropEvent *event); + + // Notifiers + void enabledChanged(); + + void preventStealingChanged(); + + void containsDragChanged(bool contained); + +protected: + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + +private Q_SLOTS: + void temporaryInhibitParent(bool inhibit); + +private: + void setContainsDrag(bool dragging); + + bool m_enabled : 1; + bool m_preventStealing : 1; + bool m_temporaryInhibition : 1; + bool m_containsDrag : 1; + QPoint m_oldDragMovePos; +}; + +#endif diff --git a/draganddrop/declarativemimedata.cpp b/draganddrop/declarativemimedata.cpp new file mode 100644 index 0000000..138b156 --- /dev/null +++ b/draganddrop/declarativemimedata.cpp @@ -0,0 +1,153 @@ +/* + SPDX-FileCopyrightText: 2010 BetterInbox + SPDX-FileContributor: Gregory Schlomoff + + SPDX-License-Identifier: MIT +*/ + +#include "declarativemimedata.h" + +/*! + \qmlclass MimeData DeclarativeMimeData + + This is a wrapper class around QMimeData, with a few extensions to provide better support for in-qml drag & drops. +*/ + +DeclarativeMimeData::DeclarativeMimeData() + : QMimeData() + , m_source(nullptr) +{ +} + +/*! + \internal + \class DeclarativeMimeData + + Creates a new DeclarativeMimeData by cloning the QMimeData passed as parameter. + This is useful for two reasons : + - In DragArea, we want to clone our "working copy" of the DeclarativeMimeData instance, as Qt will automatically + delete it after the drag and drop operation. + - In the drop events, the QMimeData is const, and we have troubles passing const to QML. So we clone it to + remove the "constness" + + This method will try to cast the QMimeData to DeclarativeMimeData, and will clone our extensions to QMimeData as well +*/ +DeclarativeMimeData::DeclarativeMimeData(const QMimeData *copy) + : QMimeData() + , m_source(nullptr) +{ + // Copy the standard MIME data + const auto formats = copy->formats(); + for (const QString &format : formats) { + QMimeData::setData(format, copy->data(format)); + } + + // If the object we are copying actually is a DeclarativeMimeData, copy our extended properties as well + const DeclarativeMimeData *declarativeMimeData = qobject_cast(copy); + if (declarativeMimeData) { + this->setSource(declarativeMimeData->source()); + } +} + +/*! + \qmlproperty url MimeData::url + + Returns the first URL from the urls property of QMimeData + TODO: We should use QDeclarativeListProperty to return the whole list instead of only the first element. +*/ +QUrl DeclarativeMimeData::url() const +{ + if (this->hasUrls() && !this->urls().isEmpty()) { + return QMimeData::urls().first(); + } + return QUrl(); +} +void DeclarativeMimeData::setUrl(const QUrl &url) +{ + if (this->url() == url) + return; + + QList urlList; + urlList.append(url); + QMimeData::setUrls(urlList); + Q_EMIT urlChanged(); +} + +QJsonArray DeclarativeMimeData::urls() const +{ + QJsonArray varUrls; + const auto lstUrls = QMimeData::urls(); + for (const QUrl &url : lstUrls) { + varUrls.append(url.toString()); + } + return varUrls; +} + +void DeclarativeMimeData::setUrls(const QJsonArray &urls) +{ + QList urlList; + urlList.reserve(urls.size()); + for (const QVariant &varUrl : urls) { + urlList << varUrl.toUrl(); + } + QMimeData::setUrls(urlList); + Q_EMIT urlsChanged(); +} + +// color +QColor DeclarativeMimeData::color() const +{ + if (this->hasColor()) { + return qvariant_cast(this->colorData()); + } + return QColor(); +} + +bool DeclarativeMimeData::hasColor() const +{ + // qDebug() << " hasColor " << (QMimeData::hasColor() ? color().name() : "false"); + return QMimeData::hasColor(); +} + +void DeclarativeMimeData::setColor(const QColor &color) +{ + if (this->color() != color) { + this->setColorData(color); + Q_EMIT colorChanged(); + } +} + +void DeclarativeMimeData::setData(const QString &mimeType, const QVariant &data) +{ + if (data.type() == QVariant::ByteArray) { + QMimeData::setData(mimeType, data.toByteArray()); + } else if (data.canConvert(QVariant::String)) { + QMimeData::setData(mimeType, data.toString().toLatin1()); + } +} + +/*! + \qmlproperty item MimeData::source + + Setting source to any existing qml item will enable the receiver of the drag and drop operation to know in which item + the operation originated. + + In the case of inter-application drag and drop operations, the source will not be available, and will be 0. + Be sure to test it in your QML code, before using it, or it will generate errors in the console. +*/ +QQuickItem *DeclarativeMimeData::source() const +{ + return m_source; +} +void DeclarativeMimeData::setSource(QQuickItem *source) +{ + if (m_source != source) { + m_source = source; + Q_EMIT sourceChanged(); + } +} + +QByteArray DeclarativeMimeData::getDataAsByteArray(const QString &format) +{ + return data(format); +} diff --git a/draganddrop/declarativemimedata.h b/draganddrop/declarativemimedata.h new file mode 100644 index 0000000..09e6800 --- /dev/null +++ b/draganddrop/declarativemimedata.h @@ -0,0 +1,100 @@ +/* + SPDX-FileCopyrightText: 2010 BetterInbox + SPDX-FileContributor: Gregory Schlomoff + + SPDX-License-Identifier: MIT +*/ + +#ifndef DECLARATIVEMIMEDATA_H +#define DECLARATIVEMIMEDATA_H + +#include +#include +#include +#include +#include + +class DeclarativeMimeData : public QMimeData +{ + Q_OBJECT + + /** + * A plain text (MIME type text/plain) representation of the data. + */ + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + + /** + * A string if the data stored in the object is HTML (MIME type text/html); otherwise returns an empty string. + */ + Q_PROPERTY(QString html READ html WRITE setHtml NOTIFY htmlChanged) + + /** + * Url contained in the mimedata + */ + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + + /** + * A list of URLs contained within the MIME data object. + * URLs correspond to the MIME type text/uri-list. + */ + Q_PROPERTY(QJsonArray urls READ urls WRITE setUrls NOTIFY urlsChanged) + + /** + * A color if the data stored in the object represents a color (MIME type application/x-color); otherwise QColor(). + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + + /** + * The graphical item on the scene that started the drag event. It may be null. + */ + Q_PROPERTY(QQuickItem *source READ source WRITE setSource NOTIFY sourceChanged) + + /** @see QMimeData::hasUrls */ + Q_PROPERTY(bool hasUrls READ hasUrls NOTIFY urlsChanged) + // TODO: Image property + + /** + * @sa QMimeData::formats + */ + Q_PROPERTY(QStringList formats READ formats) +public: + DeclarativeMimeData(); + DeclarativeMimeData(const QMimeData *copy); + + QUrl url() const; + void setUrl(const QUrl &url); + + QJsonArray urls() const; + void setUrls(const QJsonArray &urls); + + QColor color() const; + void setColor(const QColor &color); + Q_INVOKABLE bool hasColor() const; + + Q_INVOKABLE void setData(const QString &mimeType, const QVariant &data); + + QQuickItem *source() const; + void setSource(QQuickItem *source); + + Q_INVOKABLE QByteArray getDataAsByteArray(const QString &format); + + /* + QString text() const; //TODO: Reimplement this to issue the onChanged signals + void setText(const QString &text); + QString html() const; + void setHtml(const QString &html); + */ + +Q_SIGNALS: + void textChanged(); // FIXME not being used + void htmlChanged(); // FIXME not being used + void urlChanged(); + void urlsChanged(); + void colorChanged(); + void sourceChanged(); + +private: + QQuickItem *m_source; +}; + +#endif // DECLARATIVEMIMEDATA_H diff --git a/main.cpp b/main.cpp index 1d8ed04..19371aa 100644 --- a/main.cpp +++ b/main.cpp @@ -31,6 +31,12 @@ #include "helper/fm.h" #include "helper/shortcut.h" +#include "draganddrop/declarativedragdropevent.h" +#include "draganddrop/declarativedroparea.h" +#include "draganddrop/declarativemimedata.h" + +#include + int main(int argc, char *argv[]) { //QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); @@ -38,6 +44,8 @@ int main(int argc, char *argv[]) // Register QML Type. const char *uri = "Cutefish.FileManager"; + const char *dragandrop_uri = "Cutefish.DragDrop"; + qmlRegisterType(uri, 1, 0, "PlacesModel"); qmlRegisterType(uri, 1, 0, "FolderModel"); qmlRegisterType(uri, 1, 0, "PathBarModel"); @@ -50,10 +58,16 @@ int main(int argc, char *argv[]) #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) qmlRegisterType(); + qmlRegisterType(); #else qmlRegisterAnonymousType(uri, 1); + qmlRegisterAnonymousType(dragandrop_uri, 1); #endif + qmlRegisterType(dragandrop_uri, 1, 0, "DropArea"); + qmlRegisterUncreatableType(dragandrop_uri, 1, 0, "MimeData", QStringLiteral("MimeData cannot be created from QML.")); + qmlRegisterUncreatableType(dragandrop_uri, 2, 0, "DragDropEvent", QStringLiteral("DragDropEvent cannot be created from QML.")); + Application app(argc, argv); return app.run(); } diff --git a/qml/Dialogs/PropertiesDialog.qml b/qml/Dialogs/PropertiesDialog.qml index 35d0b02..e6908c5 100644 --- a/qml/Dialogs/PropertiesDialog.qml +++ b/qml/Dialogs/PropertiesDialog.qml @@ -26,8 +26,8 @@ import FishUI 1.0 as FishUI Item { id: control - property int widthValue: _mainLayout.implicitWidth + FishUI.Units.largeSpacing * 4 - property int heightValue: _mainLayout.implicitHeight + FishUI.Units.largeSpacing * 2 + property int widthValue: _mainLayout.implicitWidth + FishUI.Units.largeSpacing * 3 + property int heightValue: _mainLayout.implicitHeight + FishUI.Units.largeSpacing * 3 width: widthValue height: heightValue @@ -62,8 +62,8 @@ Item { ColumnLayout { id: _mainLayout anchors.fill: parent - anchors.leftMargin: FishUI.Units.largeSpacing * 2 - anchors.rightMargin: FishUI.Units.largeSpacing * 2 + anchors.leftMargin: FishUI.Units.largeSpacing * 1.5 + anchors.rightMargin: FishUI.Units.largeSpacing * 1.5 anchors.topMargin: FishUI.Units.smallSpacing anchors.bottomMargin: FishUI.Units.largeSpacing * 1.5 spacing: FishUI.Units.largeSpacing