diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 80821248e..b6f451f68 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -1,6 +1,7 @@ add_definitions(-DTRANSLATION_DOMAIN=\"plasmashellprivateplugin\") install(DIRECTORY workspace/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/workspace/components) +add_subdirectory(containmentlayoutmanager) add_subdirectory(shellprivate) add_subdirectory(keyboardlayout) add_subdirectory(sessionsprivate) diff --git a/components/containmentlayoutmanager/CMakeLists.txt b/components/containmentlayoutmanager/CMakeLists.txt new file mode 100644 index 000000000..db656d693 --- /dev/null +++ b/components/containmentlayoutmanager/CMakeLists.txt @@ -0,0 +1,26 @@ + + +set(containmentlayoutmanagerplugin_SRCS + containmentlayoutmanagerplugin.cpp + appletcontainer.cpp + configoverlay.cpp + appletslayout.cpp + abstractlayoutmanager.cpp + gridlayoutmanager.cpp + itemcontainer.cpp + resizehandle.cpp + ) + +add_library(containmentlayoutmanagerplugin ${containmentlayoutmanagerplugin_SRCS}) + +target_link_libraries(containmentlayoutmanagerplugin + PUBLIC + Qt5::Core + PRIVATE + Qt5::Qml Qt5::Quick + KF5::Plasma KF5::PlasmaQuick + ) + +install(TARGETS containmentlayoutmanagerplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/containmentlayoutmanager) + +install(DIRECTORY qml/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/containmentlayoutmanager) diff --git a/components/containmentlayoutmanager/abstractlayoutmanager.cpp b/components/containmentlayoutmanager/abstractlayoutmanager.cpp new file mode 100644 index 000000000..a6e4232a0 --- /dev/null +++ b/components/containmentlayoutmanager/abstractlayoutmanager.cpp @@ -0,0 +1,143 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "abstractlayoutmanager.h" +#include "appletslayout.h" +#include "itemcontainer.h" + +#include + +AbstractLayoutManager::AbstractLayoutManager(AppletsLayout *layout) + : QObject(layout), + m_layout(layout) +{ +} + +AbstractLayoutManager::~AbstractLayoutManager() +{ +} + +AppletsLayout *AbstractLayoutManager::layout() const +{ + return m_layout; +} + +QSizeF AbstractLayoutManager::cellSize() const +{ + return m_cellSize; +} + +QSizeF AbstractLayoutManager::cellAlignedContainingSize(const QSizeF &size) const +{ + return QSizeF(m_cellSize.width() * ceil(size.width() / m_cellSize.width()), + m_cellSize.height() * ceil(size.height() / m_cellSize.height())); +} + +void AbstractLayoutManager::setCellSize(const QSizeF &size) +{ + m_cellSize = size; +} + +QRectF AbstractLayoutManager::candidateGeometry(ItemContainer *item) const +{ + const QRectF originalItemRect = QRectF(item->x(), item->y(), item->width(), item->height()); + + //TODO: a default minimum size + QSizeF minimumSize = QSize(m_layout->minimumItemWidth(), m_layout->minimumItemHeight()); + if (item->layoutAttached()) { + minimumSize = QSizeF(qMax(minimumSize.width(), item->layoutAttached()->property("minimumWidth").toReal()), + qMax(minimumSize.height(), item->layoutAttached()->property("minimumHeight").toReal())); + } + + const QRectF ltrRect = nextAvailableSpace(item, minimumSize, AppletsLayout::LeftToRight); + const QRectF rtlRect = nextAvailableSpace(item, minimumSize, AppletsLayout::RightToLeft); + const QRectF ttbRect = nextAvailableSpace(item, minimumSize, AppletsLayout::TopToBottom); + const QRectF bttRect = nextAvailableSpace(item, minimumSize, AppletsLayout::BottomToTop); + + // Take the closest rect, unless the item prefers a particular positioning strategy + QMap distances; + if (!ltrRect.isEmpty()) { + const int dist = item->preferredLayoutDirection() == AppletsLayout::LeftToRight ? 0 : QPointF(originalItemRect.center() - ltrRect.center()).manhattanLength(); + distances[dist] = ltrRect; + } + if (!rtlRect.isEmpty()) { + const int dist = item->preferredLayoutDirection() == AppletsLayout::RightToLeft ? 0 : QPointF(originalItemRect.center() - rtlRect.center()).manhattanLength(); + distances[dist] = rtlRect; + } + if (!ttbRect.isEmpty()) { + const int dist = item->preferredLayoutDirection() == AppletsLayout::TopToBottom ? 0 : QPointF(originalItemRect.center() - ttbRect.center()).manhattanLength(); + distances[dist] = ttbRect; + } + if (!bttRect.isEmpty()) { + const int dist = item->preferredLayoutDirection() == AppletsLayout::BottomToTop ? 0 : QPointF(originalItemRect.center() - bttRect.center()).manhattanLength(); + distances[dist] = bttRect; + } + + if (distances.isEmpty()) { + // Failure to layout, completely full + return originalItemRect; + } else { + return distances.first(); + } +} + +void AbstractLayoutManager::positionItem(ItemContainer *item) +{ + // Give it a sane size if uninitialized: this may change size hints + if (item->width() <= 0 || item->height() <= 0) { + item->setSize(QSizeF(qMax(m_layout->minimumItemWidth(), m_layout->defaultItemWidth()), + qMax(m_layout->minimumItemHeight(), m_layout->defaultItemHeight()))); + } + + QRectF candidate = candidateGeometry(item); + item->setPosition(candidate.topLeft()); + item->setSize(candidate.size()); +} + +void AbstractLayoutManager::positionItemAndAssign(ItemContainer *item) +{ + releaseSpace(item); + positionItem(item); + assignSpace(item); +} + +bool AbstractLayoutManager::assignSpace(ItemContainer *item) +{ + if (assignSpaceImpl(item)) { + emit layoutNeedsSaving(); + return true; + } else { + return false; + } +} + +void AbstractLayoutManager::releaseSpace(ItemContainer *item) +{ + releaseSpaceImpl(item); + emit layoutNeedsSaving(); +} + +void AbstractLayoutManager::layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { + Q_UNUSED(newGeometry); + Q_UNUSED(oldGeometry); + // NOTE: Empty base implementation, don't put anything here +} + +#include "moc_abstractlayoutmanager.cpp" diff --git a/components/containmentlayoutmanager/abstractlayoutmanager.h b/components/containmentlayoutmanager/abstractlayoutmanager.h new file mode 100644 index 000000000..e77524e36 --- /dev/null +++ b/components/containmentlayoutmanager/abstractlayoutmanager.h @@ -0,0 +1,136 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include +#include "appletslayout.h" + +class ItemContainer; + +class AbstractLayoutManager : public QObject +{ + Q_OBJECT + +public: + AbstractLayoutManager(AppletsLayout *layout); + ~AbstractLayoutManager(); + + AppletsLayout *layout() const; + + void setCellSize(const QSizeF &size); + QSizeF cellSize() const; + + /** + * A size aligned to the gid that fully contains the given size + */ + QSizeF cellAlignedContainingSize(const QSizeF &size) const; + + /** + * Positions the item, does *not* assign the space as taken + */ + void positionItem(ItemContainer *item); + + /** + * Positions the item and assigns the space as taken by this item + */ + void positionItemAndAssign(ItemContainer *item); + + /** + * Set the space of item's rect as occupied by item. + * The operation may fail if some space of the item's geometry is already occupied. + * @returns true if the operation succeeded + */ + bool assignSpace(ItemContainer *item); + + /** + * If item is occupying space, set it as available + */ + void releaseSpace(ItemContainer *item); + +// VIRTUALS + virtual QString serializeLayout() const = 0; + + virtual void parseLayout(const QString &savedLayout) = 0; + + virtual void layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry); + + /** + * true if the item is managed by the grid + */ + virtual bool itemIsManaged(ItemContainer *item) = 0; + + /** + * Forget about layout information and relayout all items based solely on their current geometry + */ + virtual void resetLayout() = 0; + + /** + * Forget about layout information and relayout all items based on their stored geometry first, and if that fails from their current geometry + */ + virtual void resetLayoutFromConfig() = 0; + + /** + * Restores an item geometry from the serialized config + * parseLayout needs to be called before this + * @returns true if the item was stored in the config + * and the restore has been performed. + * Otherwise, the item is not touched and returns false + */ + virtual bool restoreItem(ItemContainer *item) = 0; + + /** + * @returns true if the given rectangle is all free space + */ + virtual bool isRectAvailable(const QRectF &rect) = 0; + +Q_SIGNALS: + /** + * Emitted when the layout has been changed and now needs saving + */ + void layoutNeedsSaving(); + +protected: + /** + * Subclasses implement their assignSpace logic here + */ + virtual bool assignSpaceImpl(ItemContainer *item) = 0; + + /** + * Subclasses implement their releasespace logic here + */ + virtual void releaseSpaceImpl(ItemContainer *item) = 0; + + /** + * @returns a rectangle big at least as minimumSize, trying to be as near as possible to the current item's geometry, displaced in the direction we asked, forwards or backwards + * @param rect the rect we want to place an item in + * @param minimumSize the minimum size we need to make sure is available + */ + virtual QRectF nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const = 0; + +private: + QRectF candidateGeometry(ItemContainer *item) const; + + AppletsLayout *m_layout; + + // size in pixels of a crid cell + QSizeF m_cellSize; +}; + diff --git a/components/containmentlayoutmanager/appletcontainer.cpp b/components/containmentlayoutmanager/appletcontainer.cpp new file mode 100644 index 000000000..83200b8dc --- /dev/null +++ b/components/containmentlayoutmanager/appletcontainer.cpp @@ -0,0 +1,109 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "appletcontainer.h" + +#include +#include + +#include +#include + +AppletContainer::AppletContainer(QQuickItem *parent) + : ItemContainer(parent) +{ + connect(this, &AppletContainer::contentItemChanged, this, [this]() { + if (m_appletItem) { + disconnect(m_appletItem->applet(), &Plasma::Applet::busyChanged, this, nullptr); + } + m_appletItem = qobject_cast(contentItem()); + + connectBusyIndicator(); + + emit appletChanged(); + }); +} + +AppletContainer::~AppletContainer() +{ +} + +void AppletContainer::componentComplete() +{ + connectBusyIndicator(); + ItemContainer::componentComplete(); +} + +PlasmaQuick::AppletQuickItem *AppletContainer::applet() +{ + return m_appletItem; +} + +QQmlComponent *AppletContainer::busyIndicatorComponent() const +{ + return m_busyIndicatorComponent; +} + +void AppletContainer::setBusyIndicatorComponent(QQmlComponent *component) +{ + if (m_busyIndicatorComponent == component) { + return; + } + + m_busyIndicatorComponent = component; + + if (m_busyIndicatorItem) { + m_busyIndicatorItem->deleteLater(); + m_busyIndicatorItem = nullptr; + } + + emit busyIndicatorComponentChanged(); +} + +void AppletContainer::connectBusyIndicator() +{ + if (m_appletItem && !m_busyIndicatorItem) { + Q_ASSERT(m_appletItem->applet()); + connect(m_appletItem->applet(), &Plasma::Applet::busyChanged, this, [this] () { + if (!m_busyIndicatorComponent || !m_appletItem->applet()->isBusy() || m_busyIndicatorItem) { + return; + } + + QQmlContext *context = QQmlEngine::contextForObject(this); + Q_ASSERT(context); + QObject *instance = m_busyIndicatorComponent->beginCreate(context); + m_busyIndicatorItem = qobject_cast(instance); + + if (!m_busyIndicatorItem) { + qWarning() << "Error: busyIndicatorComponent not of Item type"; + if (instance) { + instance->deleteLater(); + } + return; + } + + m_busyIndicatorItem->setParentItem(this); + m_busyIndicatorItem->setZ(999); + m_busyIndicatorComponent->completeCreate(); + }); + } +} + +#include "moc_appletcontainer.cpp" diff --git a/components/containmentlayoutmanager/appletcontainer.h b/components/containmentlayoutmanager/appletcontainer.h new file mode 100644 index 000000000..0b701079e --- /dev/null +++ b/components/containmentlayoutmanager/appletcontainer.h @@ -0,0 +1,65 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include "itemcontainer.h" + +#include +#include + + +namespace PlasmaQuick { + class AppletQuickItem; +} + +class AppletContainer: public ItemContainer +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(PlasmaQuick::AppletQuickItem *applet READ applet NOTIFY appletChanged) + + Q_PROPERTY(QQmlComponent *busyIndicatorComponent READ busyIndicatorComponent WRITE setBusyIndicatorComponent NOTIFY busyIndicatorComponentChanged) + +public: + AppletContainer(QQuickItem *parent = nullptr); + ~AppletContainer(); + + PlasmaQuick::AppletQuickItem *applet(); + + QQmlComponent *busyIndicatorComponent() const; + void setBusyIndicatorComponent(QQmlComponent *comp); + +protected: + void componentComplete() override; + +Q_SIGNALS: + void appletChanged(); + void busyIndicatorComponentChanged(); + +private: + void connectBusyIndicator(); + + QPointer m_appletItem; + QPointer m_busyIndicatorComponent; + QQuickItem *m_busyIndicatorItem = nullptr; +}; + diff --git a/components/containmentlayoutmanager/appletslayout.cpp b/components/containmentlayoutmanager/appletslayout.cpp new file mode 100644 index 000000000..99e8b5fce --- /dev/null +++ b/components/containmentlayoutmanager/appletslayout.cpp @@ -0,0 +1,667 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "appletslayout.h" +#include "appletcontainer.h" +#include "gridlayoutmanager.h" + +#include +#include +#include +#include + +// Plasma +#include +#include +#include + +AppletsLayout::AppletsLayout(QQuickItem *parent) + : QQuickItem(parent) +{ + m_layoutManager = new GridLayoutManager(this); + + setFlags(QQuickItem::ItemIsFocusScope); + setAcceptedMouseButtons(Qt::LeftButton); + + m_saveLayoutTimer = new QTimer(this); + m_saveLayoutTimer->setSingleShot(true); + m_saveLayoutTimer->setInterval(100); + connect(m_layoutManager, &AbstractLayoutManager::layoutNeedsSaving, m_saveLayoutTimer, QOverload<>::of(&QTimer::start)); + connect(m_saveLayoutTimer, &QTimer::timeout, this, [this] () { + if (!m_configKey.isEmpty() && m_containment && m_containment->corona()->isStartupCompleted()) { + const QString serializedConfig = m_layoutManager->serializeLayout(); + m_containment->config().writeEntry(m_configKey, serializedConfig); + //FIXME: something more efficient + m_layoutManager->parseLayout(serializedConfig); + m_savedSize = size(); + m_containment->corona()->requireConfigSync(); + } + }); + + m_configKeyChangeTimer = new QTimer(this); + m_configKeyChangeTimer->setSingleShot(true); + m_configKeyChangeTimer->setInterval(100); + connect(m_configKeyChangeTimer, &QTimer::timeout, this, [this] () { + if (!m_configKey.isEmpty() && m_containment) { + m_layoutManager->parseLayout(m_containment->config().readEntry(m_configKey, "")); + + if (width() > 0 && height() > 0) { + m_layoutManager->resetLayoutFromConfig(); + m_savedSize = size(); + } + } + }); + m_pressAndHoldTimer = new QTimer(this); + m_pressAndHoldTimer->setSingleShot(true); + connect(m_pressAndHoldTimer, &QTimer::timeout, this, [this]() { + setEditMode(true); + }); + + m_sizeSyncTimer = new QTimer(this); + m_sizeSyncTimer->setSingleShot(true); + m_sizeSyncTimer->setInterval(150); + connect(m_sizeSyncTimer, &QTimer::timeout, this, [this]() { + const QRect newGeom(x(), y(), width(), height()); + // The size has been restored from the last one it has been saved: restore that exact same layout + if (newGeom.size() == m_savedSize) { + m_layoutManager->resetLayoutFromConfig(); + + // If the resize is consequence of a screen resolution change, queue a relayout maintaining the distance between screen edges + } else if (!m_geometryBeforeResolutionChange.isEmpty()) { + m_layoutManager->layoutGeometryChanged(newGeom, m_geometryBeforeResolutionChange); + m_geometryBeforeResolutionChange = QRectF(); + + // Heuristically relayout items only when the plasma startup is fully completed + } else { + polish(); + } + }); + + connect(this, &QQuickItem::focusChanged, this, [this]() { + if (!hasFocus()) { + setEditMode(false); + } + }); +} + +AppletsLayout::~AppletsLayout() +{ +} + +PlasmaQuick::AppletQuickItem *AppletsLayout::containment() const +{ + return m_containmentItem; +} + +void AppletsLayout::setContainment(PlasmaQuick::AppletQuickItem *containmentItem) +{ + // Forbid changing containmentItem at runtime + if (m_containmentItem + || containmentItem == m_containmentItem + || !containmentItem->applet() + || !containmentItem->applet()->isContainment()) { + qWarning() << "Error: cannot change the containment to AppletsLayout"; + return; + } + + // Can't assign containments that aren't parents + QQuickItem *candidate = parentItem(); + while (candidate) { + if (candidate == m_containmentItem) { + break; + } + candidate = candidate->parentItem(); + } + if (candidate != m_containmentItem) { + return; + } + + m_containmentItem = containmentItem; + m_containment = static_cast(m_containmentItem->applet()); + + connect(m_containmentItem, SIGNAL(appletAdded(QObject *, int, int)), + this, SLOT(appletAdded(QObject *, int, int))); + + connect(m_containmentItem, SIGNAL(appletRemoved(QObject *)), + this, SLOT(appletRemoved(QObject *))); + + emit containmentChanged(); +} + +QString AppletsLayout::configKey() const +{ + return m_configKey; +} + +void AppletsLayout::setConfigKey(const QString &key) +{ + if (m_configKey == key) { + return; + } + + m_configKey = key; + + // Reloading everything from the new config is expansive, event compress it + m_configKeyChangeTimer->start(); + + emit configKeyChanged(); +} + +QJSValue AppletsLayout::acceptsAppletCallback() const +{ + return m_acceptsAppletCallback; +} + +qreal AppletsLayout::minimumItemWidth() const +{ + return m_minimumItemSize.width(); +} + +void AppletsLayout::setMinimumItemWidth(qreal width) +{ + if (qFuzzyCompare(width, m_minimumItemSize.width())) { + return; + } + + m_minimumItemSize.setWidth(width); + + emit minimumItemWidthChanged(); +} + +qreal AppletsLayout::minimumItemHeight() const +{ + return m_minimumItemSize.height(); +} + +void AppletsLayout::setMinimumItemHeight(qreal height) +{ + if (qFuzzyCompare(height, m_minimumItemSize.height())) { + return; + } + + m_minimumItemSize.setHeight(height); + + emit minimumItemHeightChanged(); +} + +qreal AppletsLayout::defaultItemWidth() const +{ + return m_defaultItemSize.width(); +} + +void AppletsLayout::setDefaultItemWidth(qreal width) +{ + if (qFuzzyCompare(width, m_defaultItemSize.width())) { + return; + } + + m_defaultItemSize.setWidth(width); + + emit defaultItemWidthChanged(); +} + +qreal AppletsLayout::defaultItemHeight() const +{ + return m_defaultItemSize.height(); +} + +void AppletsLayout::setDefaultItemHeight(qreal height) +{ + if (qFuzzyCompare(height, m_defaultItemSize.height())) { + return; + } + + m_defaultItemSize.setHeight(height); + + emit defaultItemHeightChanged(); +} + +qreal AppletsLayout::cellWidth() const +{ + return m_layoutManager->cellSize().width(); +} + +void AppletsLayout::setCellWidth(qreal width) +{ + if (qFuzzyCompare(width, m_layoutManager->cellSize().width())) { + return; + } + + m_layoutManager->setCellSize(QSizeF(width, m_layoutManager->cellSize().height())); + + emit cellWidthChanged(); +} + +qreal AppletsLayout::cellHeight() const +{ + return m_layoutManager->cellSize().height(); +} + +void AppletsLayout::setCellHeight(qreal height) +{ + if (qFuzzyCompare(height, m_layoutManager->cellSize().height())) { + return; + } + + m_layoutManager->setCellSize(QSizeF(m_layoutManager->cellSize().width(), height)); + + emit cellHeightChanged(); +} + +void AppletsLayout::setAcceptsAppletCallback(const QJSValue& callback) +{ + if (m_acceptsAppletCallback.strictlyEquals(callback)) { + return; + } + + if (!callback.isNull() && !callback.isCallable()) { + return; + } + + m_acceptsAppletCallback = callback; + + Q_EMIT acceptsAppletCallbackChanged(); +} + +QQmlComponent *AppletsLayout::appletContainerComponent() const +{ + return m_appletContainerComponent; +} + +void AppletsLayout::setAppletContainerComponent(QQmlComponent *component) +{ + if (m_appletContainerComponent == component) { + return; + } + + m_appletContainerComponent = component; + + emit appletContainerComponentChanged(); +} + +AppletsLayout::EditModeCondition AppletsLayout::editModeCondition() const +{ + return m_editModeCondition; +} + +void AppletsLayout::setEditModeCondition(AppletsLayout::EditModeCondition condition) +{ + if (m_editModeCondition == condition) { + return; + } + + if (m_editModeCondition == Locked) { + setEditMode(false); + } + + m_editModeCondition = condition; + + emit editModeConditionChanged(); +} + +bool AppletsLayout::editMode() const +{ + return m_editMode; +} + +void AppletsLayout::setEditMode(bool editMode) +{ + if (m_editMode == editMode) { + return; + } + + m_editMode = editMode; + + emit editModeChanged(); +} + +ItemContainer *AppletsLayout::placeHolder() const +{ + return m_placeHolder; +} + +void AppletsLayout::setPlaceHolder(ItemContainer *placeHolder) +{ + if (m_placeHolder == placeHolder) { + return; + } + + m_placeHolder = placeHolder; + m_placeHolder->setParentItem(this); + m_placeHolder->setZ(9999); + m_placeHolder->setOpacity(false); + + emit placeHolderChanged(); +} + +void AppletsLayout::save() +{ + m_saveLayoutTimer->start(); +} + +void AppletsLayout::showPlaceHolderAt(const QRectF &geom) +{ + if (!m_placeHolder) { + return; + } + + m_placeHolder->setPosition(geom.topLeft()); + m_placeHolder->setSize(geom.size()); + + m_layoutManager->positionItem(m_placeHolder); + + m_placeHolder->setProperty("opacity", 1); +} + +void AppletsLayout::showPlaceHolderForItem(QQuickItem *item) +{ + if (!m_placeHolder) { + return; + } + + m_placeHolder->setPosition(item->position()); + m_placeHolder->setSize(item->size()); + + m_layoutManager->positionItem(m_placeHolder); + + m_placeHolder->setProperty("opacity", 1); +} + +void AppletsLayout::hidePlaceHolder() +{ + if (!m_placeHolder) { + return; + } + + m_placeHolder->setProperty("opacity", 0); +} + +bool AppletsLayout::isRectAvailable(qreal x, qreal y, qreal width, qreal height) +{ + return m_layoutManager->isRectAvailable(QRectF(x, y, width, height)); +} + +bool AppletsLayout::itemIsManaged(ItemContainer *item) +{ + return m_layoutManager->itemIsManaged(item); +} + +void AppletsLayout::positionItem(ItemContainer *item) +{ + item->setParent(this); + m_layoutManager->positionItemAndAssign(item); +} + +void AppletsLayout::releaseSpace(ItemContainer *item) +{ + m_layoutManager->releaseSpace(item); +} + +void AppletsLayout::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + // Ignore completely moves without resize + if (newGeometry.size() == oldGeometry.size()) { + QQuickItem::geometryChanged(newGeometry, oldGeometry); + return; + } + + // Don't care for anythin happening before startup completion + if (!m_containment || !m_containment->corona() || !m_containment->corona()->isStartupCompleted()) { + return; + } + + // Only do a layouting procedure if we received a valid size + if (!newGeometry.isEmpty()) { + m_sizeSyncTimer->start(); + } + + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} + +void AppletsLayout::updatePolish() +{ + m_layoutManager->resetLayout(); + m_savedSize = size(); +} + +void AppletsLayout::componentComplete() +{ + if (!m_containment || !m_containmentItem) { + return; + } + + if (!m_configKey.isEmpty()) { + m_layoutManager->parseLayout(m_containment->config().readEntry(m_configKey, "")); + } + + QList appletObjects = m_containmentItem->property("applets").value >(); + + for (auto *obj : appletObjects) { + PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(obj); + + if (!obj) { + continue; + } + + AppletContainer *container = createContainerForApplet(appletItem); + if (width() > 0 && height() > 0) { + m_layoutManager->positionItemAndAssign(container); + } + } + + //layout all extra non applet items + if (width() > 0 && height() > 0) { + for (auto *child : childItems()) { + ItemContainer *item = qobject_cast(child); + if (item && item != m_placeHolder && !m_layoutManager->itemIsManaged(item)) { + m_layoutManager->positionItemAndAssign(item); + } + } + } + + if (m_containment && m_containment->corona()) { + connect(m_containment->corona(), &Plasma::Corona::startupCompleted, this, [this](){ + // m_savedSize = size(); + }); + // When the screen geometry changes, we need to know the geometry just before it did, so we can apply out heuristic of keeping the distance with borders constant + connect(m_containment->corona(), &Plasma::Corona::screenGeometryChanged, this, [this](int id){ + if (m_containment->screen() == id) { + m_geometryBeforeResolutionChange = QRectF(x(), y(), width(), height()); + } + }); + } + QQuickItem::componentComplete(); +} + + + +void AppletsLayout::mousePressEvent(QMouseEvent *event) +{ + forceActiveFocus(Qt::MouseFocusReason); + + if (!m_editMode && m_editModeCondition == AppletsLayout::Manual) { + return; + } + + if (!m_editMode && m_editModeCondition == AppletsLayout::AfterPressAndHold) { + m_pressAndHoldTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + } + + m_mouseDownWasEditMode = m_editMode; + m_mouseDownPosition = event->windowPos(); + + //event->setAccepted(false); +} + +void AppletsLayout::mouseMoveEvent(QMouseEvent *event) +{ + if (!m_editMode && m_editModeCondition == AppletsLayout::Manual) { + return; + } + + if (!m_editMode + && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { + m_pressAndHoldTimer->stop(); + } +} + +void AppletsLayout::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_editMode + && m_mouseDownWasEditMode + && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() < QGuiApplication::styleHints()->startDragDistance()) { + setEditMode(false); + } + + m_pressAndHoldTimer->stop(); + + if (!m_editMode) { + for (auto *child : childItems()) { + ItemContainer *item = qobject_cast(child); + if (item && item != m_placeHolder) { + item->setEditMode(false); + } + } + } +} + +void AppletsLayout::appletAdded(QObject *applet, int x, int y) +{ + PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); + + //maybe even an assert? + if (!appletItem) { + return; + } + + if (m_acceptsAppletCallback.isCallable()) { + QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine(); + Q_ASSERT(engine); + QJSValueList args; + args << engine->newQObject(applet) << QJSValue(x) << QJSValue(y); + + if (!m_acceptsAppletCallback.call(args).toBool()) { + emit appletRefused(applet, x, y); + return; + } + } + + AppletContainer *container = createContainerForApplet(appletItem); + container->setPosition(QPointF(x, y)); + container->setVisible(true); + + m_layoutManager->positionItemAndAssign(container); +} + +void AppletsLayout::appletRemoved(QObject *applet) +{ + PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); + + //maybe even an assert? + if (!appletItem) { + return; + } + + AppletContainer *container = m_containerForApplet.value(appletItem); + if (!container) { + return; + } + + m_layoutManager->releaseSpace(container); + m_containerForApplet.remove(appletItem); + appletItem->setParentItem(this); + container->deleteLater(); +} + +AppletContainer *AppletsLayout::createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem) +{ + AppletContainer *container = m_containerForApplet.value(appletItem); + + if (container) { + return container; + } + + bool createdFromQml = true; + + if (m_appletContainerComponent) { + QQmlContext *context = QQmlEngine::contextForObject(this); + Q_ASSERT(context); + QObject *instance = m_appletContainerComponent->beginCreate(context); + container = qobject_cast(instance); + if (container) { + container->setParentItem(this); + } else { + qWarning() << "Error: provided component not an AppletContainer instance"; + if (instance) { + instance->deleteLater(); + } + createdFromQml = false; + } + } + + if (!container) { + container = new AppletContainer(this); + } + + container->setVisible(false); + + const QSizeF appletSize = appletItem->size(); + container->setContentItem(appletItem); + + m_containerForApplet[appletItem] = container; + container->setLayout(this); + container->setKey(QLatin1String("Applet-") + QString::number(appletItem->applet()->id())); + + const bool geometryWasSaved = m_layoutManager->restoreItem(container); + + if (!geometryWasSaved) { + container->setPosition(QPointF(appletItem->x() - container->leftPadding(), + appletItem->y() - container->topPadding())); + + if (!appletSize.isEmpty()) { + container->setSize(QSizeF(qMax(m_minimumItemSize.width(), appletSize.width() + container->leftPadding() + container->rightPadding()), + qMax(m_minimumItemSize.height(), appletSize.height() + container->topPadding() + container->bottomPadding()))); + } + } + + if (m_appletContainerComponent && createdFromQml) { + m_appletContainerComponent->completeCreate(); + } + + //NOTE: This has to be done here as we need the component completed to have all the bindings evaluated + if (!geometryWasSaved && appletSize.isEmpty()) { + if (container->initialSize().width() > m_minimumItemSize.width() && + container->initialSize().height() > m_minimumItemSize.height()) { + const QSizeF size = m_layoutManager->cellAlignedContainingSize( container->initialSize()); + container->setSize(size); + } else { + container->setSize(QSizeF(qMax(m_minimumItemSize.width(), m_defaultItemSize.width()), + qMax(m_minimumItemSize.height(), m_defaultItemSize.height()))); + } + } + + container->setVisible(true); + appletItem->setVisible(true); + + + return container; +} + +#include "moc_appletslayout.cpp" diff --git a/components/containmentlayoutmanager/appletslayout.h b/components/containmentlayoutmanager/appletslayout.h new file mode 100644 index 000000000..43a6d16fb --- /dev/null +++ b/components/containmentlayoutmanager/appletslayout.h @@ -0,0 +1,210 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include +#include +#include + +class QTimer; + +namespace Plasma { + class Containment; +} + +namespace PlasmaQuick { + class AppletQuickItem; +} + +class AbstractLayoutManager; +class AppletContainer; +class ItemContainer; + +class AppletsLayout: public QQuickItem +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QString configKey READ configKey WRITE setConfigKey NOTIFY configKeyChanged) + + Q_PROPERTY(PlasmaQuick::AppletQuickItem *containment READ containment WRITE setContainment NOTIFY containmentChanged) + + Q_PROPERTY(QJSValue acceptsAppletCallback READ acceptsAppletCallback WRITE setAcceptsAppletCallback NOTIFY acceptsAppletCallbackChanged) + + Q_PROPERTY(qreal minimumItemWidth READ minimumItemWidth WRITE setMinimumItemWidth NOTIFY minimumItemWidthChanged) + + Q_PROPERTY(qreal minimumItemHeight READ minimumItemHeight WRITE setMinimumItemHeight NOTIFY minimumItemHeightChanged) + + Q_PROPERTY(qreal defaultItemWidth READ defaultItemWidth WRITE setDefaultItemWidth NOTIFY defaultItemWidthChanged) + + Q_PROPERTY(qreal defaultItemHeight READ defaultItemHeight WRITE setDefaultItemHeight NOTIFY defaultItemHeightChanged) + + Q_PROPERTY(qreal cellWidth READ cellWidth WRITE setCellWidth NOTIFY cellWidthChanged) + + Q_PROPERTY(qreal cellHeight READ cellHeight WRITE setCellHeight NOTIFY cellHeightChanged) + + Q_PROPERTY(QQmlComponent *appletContainerComponent READ appletContainerComponent WRITE setAppletContainerComponent NOTIFY appletContainerComponentChanged) + + Q_PROPERTY(ItemContainer *placeHolder READ placeHolder WRITE setPlaceHolder NOTIFY placeHolderChanged); + + Q_PROPERTY(AppletsLayout::EditModeCondition editModeCondition READ editModeCondition WRITE setEditModeCondition NOTIFY editModeConditionChanged) + Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged) + +public: + enum PreferredLayoutDirection { + Closest = 0, + LeftToRight, + RightToLeft, + TopToBottom, + BottomToTop + }; + Q_ENUM(PreferredLayoutDirection) + + enum EditModeCondition { + Locked = 0, + Manual, + AfterPressAndHold, + }; + Q_ENUM(EditModeCondition) + + AppletsLayout(QQuickItem *parent = nullptr); + ~AppletsLayout(); + + // QML setters and getters + QString configKey() const; + void setConfigKey(const QString &key); + + PlasmaQuick::AppletQuickItem *containment() const; + void setContainment(PlasmaQuick::AppletQuickItem *containment); + + QJSValue acceptsAppletCallback() const; + void setAcceptsAppletCallback(const QJSValue& callback); + + qreal minimumItemWidth() const; + void setMinimumItemWidth(qreal width); + + qreal minimumItemHeight() const; + void setMinimumItemHeight(qreal height); + + qreal defaultItemWidth() const; + void setDefaultItemWidth(qreal width); + + qreal defaultItemHeight() const; + void setDefaultItemHeight(qreal height); + + qreal cellWidth() const; + void setCellWidth(qreal width); + + qreal cellHeight() const; + void setCellHeight(qreal height); + + QQmlComponent *appletContainerComponent() const; + void setAppletContainerComponent(QQmlComponent *component); + + ItemContainer *placeHolder() const; + void setPlaceHolder(ItemContainer *placeHolder); + + EditModeCondition editModeCondition() const; + void setEditModeCondition(EditModeCondition condition); + + bool editMode() const; + void setEditMode(bool edit); + + Q_INVOKABLE void save(); + Q_INVOKABLE void showPlaceHolderAt(const QRectF &geom); + Q_INVOKABLE void showPlaceHolderForItem(QQuickItem *item); + Q_INVOKABLE void hidePlaceHolder(); + + Q_INVOKABLE bool isRectAvailable(qreal x, qreal y, qreal width, qreal height); + Q_INVOKABLE bool itemIsManaged(ItemContainer *item); + Q_INVOKABLE void positionItem(ItemContainer *item); + Q_INVOKABLE void releaseSpace(ItemContainer *item); + +Q_SIGNALS: + /** + * An applet has been refused by the layout: acceptsAppletCallback + * returned false and will need to be managed in a different way + */ + void appletRefused(QObject *applet, int x, int y); + + void configKeyChanged(); + void containmentChanged(); + void minimumItemWidthChanged(); + void minimumItemHeightChanged(); + void defaultItemWidthChanged(); + void defaultItemHeightChanged(); + void cellWidthChanged(); + void cellHeightChanged(); + void acceptsAppletCallbackChanged(); + void appletContainerComponentChanged(); + void placeHolderChanged(); + void editModeConditionChanged(); + void editModeChanged(); + +protected: + void updatePolish() override; + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + + //void classBegin() override; + void componentComplete() override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + +private Q_SLOTS: + void appletAdded(QObject *applet, int x, int y); + void appletRemoved(QObject *applet); + +private: + AppletContainer *createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem); + + + QString m_configKey; + QTimer *m_saveLayoutTimer; + QTimer *m_configKeyChangeTimer; + + PlasmaQuick::AppletQuickItem *m_containmentItem = nullptr; + Plasma::Containment *m_containment = nullptr; + QQmlComponent *m_appletContainerComponent = nullptr; + + AbstractLayoutManager *m_layoutManager = nullptr; + + QPointer m_placeHolder; + + QTimer *m_pressAndHoldTimer; + QTimer *m_sizeSyncTimer; + + QJSValue m_acceptsAppletCallback; + + AppletsLayout::EditModeCondition m_editModeCondition = AppletsLayout::Manual; + + QHash m_containerForApplet; + + QSizeF m_minimumItemSize; + QSizeF m_defaultItemSize; + QSizeF m_savedSize; + QRectF m_geometryBeforeResolutionChange; + + QPointF m_mouseDownPosition = QPoint(-1, -1); + bool m_mouseDownWasEditMode = false; + bool m_editMode = false; +}; + diff --git a/components/containmentlayoutmanager/configoverlay.cpp b/components/containmentlayoutmanager/configoverlay.cpp new file mode 100644 index 000000000..78ce7f3d5 --- /dev/null +++ b/components/containmentlayoutmanager/configoverlay.cpp @@ -0,0 +1,133 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "configoverlay.h" + +#include + +ConfigOverlay::ConfigOverlay(QQuickItem *parent) + : QQuickItem(parent) +{ + m_hideTimer = new QTimer(this); + m_hideTimer->setSingleShot(true); + m_hideTimer->setInterval(600); + connect(m_hideTimer, &QTimer::timeout, this, [this] () { + setVisible(false); + }); +} + +ConfigOverlay::~ConfigOverlay() +{ +} + +bool ConfigOverlay::open() const +{ + return m_open; +} + +void ConfigOverlay::setOpen(bool open) +{ + if (open == m_open) { + return; + } + + m_open = open; + + if (open) { + m_hideTimer->stop(); + setVisible(true); + } else { + m_hideTimer->start(); + } + + emit openChanged(); +} + +bool ConfigOverlay::touchInteraction() const +{ + return m_touchInteraction; +} +void ConfigOverlay::setTouchInteraction(bool touch) +{ + if (touch == m_touchInteraction) { + return; + } + + m_touchInteraction = touch; + emit touchInteractionChanged(); +} + +ItemContainer *ConfigOverlay::itemContainer() const +{ + return m_itemContainer; +} + +void ConfigOverlay::setItemContainer(ItemContainer *container) +{ + if (container == m_itemContainer) { + return; + } + + if (m_itemContainer) { + disconnect(m_itemContainer, nullptr, this, nullptr); + } + + m_itemContainer = container; + + if (!m_itemContainer || !m_itemContainer->layout()) { + return; + } + + m_leftAvailableSpace = qMax(0.0, m_itemContainer->x()); + m_rightAvailableSpace = qMax(0.0, m_itemContainer->layout()->width() - (m_itemContainer->x() + m_itemContainer->width())); + m_topAvailableSpace = qMax(0.0, m_itemContainer->y()); + m_bottomAvailableSpace = qMax(0.0, m_itemContainer->layout()->height() - (m_itemContainer->y() + m_itemContainer->height())); + emit leftAvailableSpaceChanged(); + emit rightAvailableSpaceChanged(); + emit topAvailableSpaceChanged(); + emit bottomAvailableSpaceChanged(); + + connect(m_itemContainer.data(), &ItemContainer::xChanged, this, [this] () { + m_leftAvailableSpace = qMax(0.0, m_itemContainer->x()); + m_rightAvailableSpace = qMax(0.0, m_itemContainer->layout()->width() - (m_itemContainer->x() + m_itemContainer->width())); + emit leftAvailableSpaceChanged(); + emit rightAvailableSpaceChanged(); + }); + + connect(m_itemContainer.data(), &ItemContainer::yChanged, this, [this] () { + m_topAvailableSpace = qMax(0.0, m_itemContainer->y()); + m_bottomAvailableSpace = qMax(0.0, m_itemContainer->layout()->height() - (m_itemContainer->y() + m_itemContainer->height())); + emit topAvailableSpaceChanged(); + emit bottomAvailableSpaceChanged(); + }); + + connect(m_itemContainer.data(), &ItemContainer::widthChanged, this, [this] () { + m_rightAvailableSpace = qMax(0.0, m_itemContainer->layout()->width() - (m_itemContainer->x() + m_itemContainer->width())); + emit rightAvailableSpaceChanged(); + }); + + connect(m_itemContainer.data(), &ItemContainer::heightChanged, this, [this] () { + m_bottomAvailableSpace = qMax(0.0, m_itemContainer->layout()->height() - (m_itemContainer->y() + m_itemContainer->height())); + emit bottomAvailableSpaceChanged(); + }); + emit itemContainerChanged(); +} + +#include "moc_configoverlay.cpp" diff --git a/components/containmentlayoutmanager/configoverlay.h b/components/containmentlayoutmanager/configoverlay.h new file mode 100644 index 000000000..e86cf8d90 --- /dev/null +++ b/components/containmentlayoutmanager/configoverlay.h @@ -0,0 +1,82 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include +#include + +#include "itemcontainer.h" + +class ConfigOverlay: public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(bool open READ open WRITE setOpen NOTIFY openChanged) + Q_PROPERTY(ItemContainer *itemContainer READ itemContainer NOTIFY itemContainerChanged) + Q_PROPERTY(qreal leftAvailableSpace READ leftAvailableSpace NOTIFY leftAvailableSpaceChanged); + Q_PROPERTY(qreal topAvailableSpace READ topAvailableSpace NOTIFY topAvailableSpaceChanged); + Q_PROPERTY(qreal rightAvailableSpace READ rightAvailableSpace NOTIFY rightAvailableSpaceChanged); + Q_PROPERTY(qreal bottomAvailableSpace READ bottomAvailableSpace NOTIFY bottomAvailableSpaceChanged); + Q_PROPERTY(bool touchInteraction READ touchInteraction NOTIFY touchInteractionChanged) + +public: + ConfigOverlay(QQuickItem *parent = nullptr); + ~ConfigOverlay(); + + ItemContainer *itemContainer() const; + // NOTE: setter not accessible from QML by purpose + void setItemContainer(ItemContainer *container); + + bool open() const; + void setOpen(bool open); + + qreal leftAvailableSpace() {return m_leftAvailableSpace;} + qreal topAvailableSpace() {return m_topAvailableSpace;} + qreal rightAvailableSpace() {return m_rightAvailableSpace;} + qreal bottomAvailableSpace() {return m_bottomAvailableSpace;} + + bool touchInteraction() const; + // This only usable from C++ + void setTouchInteraction(bool touch); + +Q_SIGNALS: + void openChanged(); + void itemContainerChanged(); + void leftAvailableSpaceChanged(); + void topAvailableSpaceChanged(); + void rightAvailableSpaceChanged(); + void bottomAvailableSpaceChanged(); + void touchInteractionChanged(); + +private: + QPointer m_itemContainer; + qreal m_leftAvailableSpace = 0; + qreal m_topAvailableSpace = 0; + qreal m_rightAvailableSpace = 0; + qreal m_bottomAvailableSpace = 0; + + QTimer *m_hideTimer = nullptr; + + QList m_oldTouchPoints; + + bool m_open = false; + bool m_touchInteraction = false; +}; + diff --git a/components/containmentlayoutmanager/containmentlayoutmanagerplugin.cpp b/components/containmentlayoutmanager/containmentlayoutmanagerplugin.cpp new file mode 100644 index 000000000..a8745c138 --- /dev/null +++ b/components/containmentlayoutmanager/containmentlayoutmanagerplugin.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2019 by Marco Martin + + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "containmentlayoutmanagerplugin.h" + +#include +#include +#include + +#include "appletslayout.h" +#include "appletcontainer.h" +#include "configoverlay.h" +#include "itemcontainer.h" +#include "resizehandle.h" + +void ContainmentLayoutManagerPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.private.containmentlayoutmanager")); + + qmlRegisterType(uri, 1, 0, "AppletsLayout"); + qmlRegisterType(uri, 1, 0, "AppletContainer"); + qmlRegisterType(uri, 1, 0, "ConfigOverlay"); + qmlRegisterType(uri, 1, 0, "ItemContainer"); + qmlRegisterType(uri, 1, 0, "ResizeHandle"); + + // qmlProtectModule(uri, 1); +} + +#include "moc_containmentlayoutmanagerplugin.cpp" + diff --git a/components/containmentlayoutmanager/containmentlayoutmanagerplugin.h b/components/containmentlayoutmanager/containmentlayoutmanagerplugin.h new file mode 100644 index 000000000..4dda53b4c --- /dev/null +++ b/components/containmentlayoutmanager/containmentlayoutmanagerplugin.h @@ -0,0 +1,35 @@ +/* + * Copyright 2019 by Marco Martin + + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include +#include + +class ContainmentLayoutManagerPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; + diff --git a/components/containmentlayoutmanager/gridlayoutmanager.cpp b/components/containmentlayoutmanager/gridlayoutmanager.cpp new file mode 100644 index 000000000..7574882c6 --- /dev/null +++ b/components/containmentlayoutmanager/gridlayoutmanager.cpp @@ -0,0 +1,551 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "gridlayoutmanager.h" +#include "appletslayout.h" +#include + +GridLayoutManager::GridLayoutManager(AppletsLayout *layout) + : AbstractLayoutManager(layout) +{ +} + +GridLayoutManager::~GridLayoutManager() +{ +} + +QString GridLayoutManager::serializeLayout() const +{ + QString result; + + for (auto *item : layout()->childItems()) { + ItemContainer *itemCont = qobject_cast(item); + if (itemCont && itemCont != layout()->placeHolder()) { + result += itemCont->key() + QLatin1Char(':') + + QString::number(itemCont->x()) + QLatin1Char(',') + + QString::number(itemCont->y()) + QLatin1Char(',') + + QString::number(itemCont->width()) + QLatin1Char(',') + + QString::number(itemCont->height()) + QLatin1Char(',') + + QString::number(itemCont->rotation()) + QLatin1Char(';'); + } + } + + return result; +} + +void GridLayoutManager::parseLayout(const QString &savedLayout) +{ + m_parsedConfig.clear(); + QStringList itemsConfigs = savedLayout.split(QLatin1Char(';')); + + for (const auto &itemString : itemsConfigs) { + QStringList itemConfig = itemString.split(QLatin1Char(':')); + if (itemConfig.count() != 2) { + continue; + } + + QString id = itemConfig[0]; + QStringList itemGeom = itemConfig[1].split(QLatin1Char(',')); + if (itemGeom.count() != 5) { + continue; + } + + m_parsedConfig[id] = {itemGeom[0].toInt(), itemGeom[1].toInt(), itemGeom[2].toInt(), itemGeom[3].toInt(), itemGeom[4].toInt()}; + } +} + +bool GridLayoutManager::itemIsManaged(ItemContainer *item) +{ + return m_pointsForItem.contains(item); +} + +inline void maintainItemEdgeAlignment(QQuickItem *item, const QRectF &newRect, const QRectF &oldRect) +{ + const qreal leftDist = item->x() - oldRect.x(); + const qreal hCenterDist = item->x() + item->width()/2 - oldRect.center().x(); + const qreal rightDist = oldRect.right() - item->x() - item->width(); + + qreal hMin = qMin(qMin(qAbs(leftDist), qAbs(hCenterDist)), qAbs(rightDist)); + if (qFuzzyCompare(hMin, qAbs(leftDist))) { + // Right alignment, do nothing + } else if (qFuzzyCompare(hMin, qAbs(hCenterDist))) { + item->setX(newRect.center().x() - item->width()/2 + hCenterDist); + } else if (qFuzzyCompare(hMin, qAbs(rightDist))) { + item->setX(newRect.right() - item->width() - rightDist ); + } + + const qreal topDist = item->y() - oldRect.y(); + const qreal vCenterDist = item->y() + item->height()/2 - oldRect.center().y(); + const qreal bottomDist = oldRect.bottom() - item->y() - item->height(); + + qreal vMin = qMin(qMin(qAbs(topDist), qAbs(vCenterDist)), qAbs(bottomDist)); + + if (qFuzzyCompare(vMin, qAbs(topDist))) { + // Top alignment, do nothing + } else if (qFuzzyCompare(vMin, qAbs(vCenterDist))) { + item->setY(newRect.center().y() - item->height()/2 + vCenterDist); + } else if (qFuzzyCompare(vMin, qAbs(bottomDist))) { + item->setY(newRect.bottom() - item->height() - bottomDist ); + } +} + +void GridLayoutManager::layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + for (auto *item : layout()->childItems()) { + // Stash the old config + //m_parsedConfig[item->key()] = {item->x(), item->y(), item->width(), item->height(), item->rotation()}; + // Move the item to maintain the distance with the anchors point + maintainItemEdgeAlignment(item, newGeometry, oldGeometry); + } +} + +void GridLayoutManager::resetLayout() +{ + m_grid.clear(); + m_pointsForItem.clear(); + for (auto *item : layout()->childItems()) { + ItemContainer *itemCont = qobject_cast(item); + if (itemCont && itemCont != layout()->placeHolder()) { + // NOTE: do not use positionItemAndAssign here, because we do not want to emit layoutNeedsSaving, to not save after resize + positionItem(itemCont); + assignSpaceImpl(itemCont); + } + } +} + +void GridLayoutManager::resetLayoutFromConfig() +{ + m_grid.clear(); + m_pointsForItem.clear(); + QList missingItems; + + for (auto *item : layout()->childItems()) { + ItemContainer *itemCont = qobject_cast(item); + if (itemCont && itemCont != layout()->placeHolder()) { + if (!restoreItem(itemCont)) { + missingItems << itemCont; + } + } + } + + for (auto *item : missingItems) { + // NOTE: do not use positionItemAndAssign here, because we do not want to emit layoutNeedsSaving, to not save after resize + positionItem(item); + assignSpaceImpl(item); + } +} + +bool GridLayoutManager::restoreItem(ItemContainer *item) +{ + auto it = m_parsedConfig.find(item->key()); + + if (it != m_parsedConfig.end()) { + // Actual restore + item->setPosition(QPointF(it.value().x, it.value().y)); + item->setSize(QSizeF(it.value().width, it.value().height)); + item->setRotation(it.value().rotation); + + // NOTE: do not use positionItemAndAssign here, because we do not want to emit layoutNeedsSaving, to not save after resize + // If size is empty the layout is not in a valid state and probably startup is not completed yet + if (!layout()->size().isEmpty()) { + releaseSpaceImpl(item); + positionItem(item); + assignSpaceImpl(item); + } + + return true; + } + + return false; +} + +bool GridLayoutManager::isRectAvailable(const QRectF &rect) +{ + //TODO: define directions in which it can grow + if (rect.x() < 0 || rect.y() < 0 || rect.x() + rect.width() > layout()->width() || rect.y() + rect.height() > layout()->height()) { + return false; + } + + const QRect cellItemGeom = cellBasedGeometry(rect); + + for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { + for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { + if (!isCellAvailable(QPair(row, column))) { + return false; + } + } + } + return true; +} + +bool GridLayoutManager::assignSpaceImpl(ItemContainer *item) +{ + // Don't emit extra layoutneedssaving signals + releaseSpaceImpl(item); + if (!isRectAvailable(itemGeometry(item))) { + qWarning()<<"Trying to take space not available" << item; + return false; + } + + const QRect cellItemGeom = cellBasedGeometry(itemGeometry(item)); + + for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { + for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { + QPair cell(row, column); + m_grid.insert(cell, item); + m_pointsForItem[item].insert(cell); + } + } + + // Reorder items tab order + for (auto *i2 : layout()->childItems()) { + ItemContainer *item2 = qobject_cast(i2); + if (item2 && item != item2 && item2 != layout()->placeHolder() + && item->y() < item2->y() + item2->height() + && item->x() <= item2->x()) { + item->stackBefore(item2); + break; + } + } + + if (item->layoutAttached()) { + connect(item, &ItemContainer::sizeHintsChanged, this, [this, item]() { + adjustToItemSizeHints(item); + }); + } + + return true; +} + +void GridLayoutManager::releaseSpaceImpl(ItemContainer *item) +{ + auto it = m_pointsForItem.find(item); + + if (it == m_pointsForItem.end()) { + return; + } + + for (const auto &point : it.value()) { + m_grid.remove(point); + } + + m_pointsForItem.erase(it); + + disconnect(item, &ItemContainer::sizeHintsChanged, this, nullptr); +} + +int GridLayoutManager::rows() const +{ + return layout()->height() / cellSize().height(); +} + +int GridLayoutManager::columns() const +{ + return layout()->width() / cellSize().width(); +} + +void GridLayoutManager::adjustToItemSizeHints(ItemContainer *item) +{ + if (!item->layoutAttached() || item->editMode()) { + return; + } + + bool changed = false; + + // Minimum + const qreal newMinimumHeight = item->layoutAttached()->property("minimumHeight").toReal(); + const qreal newMinimumWidth = item->layoutAttached()->property("minimumWidth").toReal(); + + if (newMinimumHeight > item->height()) { + item->setHeight(newMinimumHeight); + changed = true; + } + if (newMinimumWidth > item->width()) { + item->setWidth(newMinimumWidth); + changed = true; + } + + // Preferred + const qreal newPreferredHeight = item->layoutAttached()->property("preferredHeight").toReal(); + const qreal newPreferredWidth = item->layoutAttached()->property("preferredWidth").toReal(); + + if (newPreferredHeight > item->height()) { + item->setHeight(layout()->cellHeight() * ceil(newPreferredHeight / layout()->cellHeight())); + changed = true; + } + if (newPreferredWidth > item->width()) { + item->setWidth(layout()->cellWidth() * ceil(newPreferredWidth / layout()->cellWidth())); + changed = true; + } + + /*// Maximum : IGNORE? + const qreal newMaximumHeight = item->layoutAttached()->property("preferredHeight").toReal(); + const qreal newMaximumWidth = item->layoutAttached()->property("preferredWidth").toReal(); + + if (newMaximumHeight > 0 && newMaximumHeight < height()) { + item->setHeight(newMaximumHeight); + changed = true; + } + if (newMaximumHeight > 0 && newMaximumWidth < width()) { + item->setWidth(newMaximumWidth); + changed = true; + }*/ + + // Relayout if anything changed + if (changed && itemIsManaged(item)) { + releaseSpace(item); + positionItem(item); + } +} + +QRect GridLayoutManager::cellBasedGeometry(const QRectF &geom) const +{ + return QRect( + round(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), + round(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), + round((qreal)geom.width() / cellSize().width()), + round((qreal)geom.height() / cellSize().height()) + ); +} + +QRect GridLayoutManager::cellBasedBoundingGeometry(const QRectF &geom) const +{ + return QRect( + floor(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), + floor(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), + ceil((qreal)geom.width() / cellSize().width()), + ceil((qreal)geom.height() / cellSize().height()) + ); +} + +bool GridLayoutManager::isOutOfBounds(const QPair &cell) const +{ + return cell.first < 0 + || cell.second < 0 + || cell.first >= rows() + || cell.second >= columns(); +} + +bool GridLayoutManager::isCellAvailable(const QPair &cell) const +{ + return !isOutOfBounds(cell) && !m_grid.contains(cell); +} + +QRectF GridLayoutManager::itemGeometry(QQuickItem *item) const +{ + return QRectF(item->x(), item->y(), item->width(), item->height()); +} + +QPair GridLayoutManager::nextCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const +{ + QPair nCell = cell; + + switch (direction) { + case AppletsLayout::AppletsLayout::BottomToTop: + --nCell.first; + break; + case AppletsLayout::AppletsLayout::TopToBottom: + ++nCell.first; + break; + case AppletsLayout::AppletsLayout::RightToLeft: + --nCell.second; + break; + case AppletsLayout::AppletsLayout::LeftToRight: + default: + ++nCell.second; + break; + } + + return nCell; +} + +QPair GridLayoutManager::nextAvailableCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const +{ + QPair nCell = cell; + while (!isOutOfBounds(nCell)) { + nCell = nextCell(nCell, direction); + + if (isOutOfBounds(nCell)) { + switch (direction) { + case AppletsLayout::AppletsLayout::BottomToTop: + nCell.first = rows() - 1; + --nCell.second; + break; + case AppletsLayout::AppletsLayout::TopToBottom: + nCell.first = 0; + ++nCell.second; + break; + case AppletsLayout::AppletsLayout::RightToLeft: + --nCell.first; + nCell.second = columns() - 1; + break; + case AppletsLayout::AppletsLayout::LeftToRight: + default: + ++nCell.first; + nCell.second = 0; + break; + } + } + + if (isCellAvailable(nCell)) { + return nCell; + } + } + + return QPair(-1, -1); +} + +int GridLayoutManager::freeSpaceInDirection(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const +{ + QPair nCell = cell; + + int avail = 0; + + while (isCellAvailable(nCell)) { + ++avail; + nCell = nextCell(nCell, direction); + } + + return avail; +} + +QRectF GridLayoutManager::nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const +{ + // The mionimum size in grid units + const QSize minimumGridSize( + ceil((qreal)minimumSize.width() / cellSize().width()), + ceil((qreal)minimumSize.height() / cellSize().height()) + ); + + QRect itemCellGeom = cellBasedGeometry(itemGeometry(item)); + itemCellGeom.setWidth(qMax(itemCellGeom.width(), minimumGridSize.width())); + itemCellGeom.setHeight(qMax(itemCellGeom.height(), minimumGridSize.height())); + + QSize partialSize; + + QPair cell(itemCellGeom.y(), itemCellGeom.x()); + if (direction == AppletsLayout::AppletsLayout::RightToLeft) { + cell.second += itemCellGeom.width(); + } else if (direction == AppletsLayout::AppletsLayout::BottomToTop) { + cell.first += itemCellGeom.height(); + } + + while (!isOutOfBounds(cell)) { + + if (!isCellAvailable(cell)) { + cell = nextAvailableCell(cell, direction); + } + + if (direction == AppletsLayout::LeftToRight || direction == AppletsLayout::RightToLeft) { + partialSize = QSize(INT_MAX, 0); + + int currentRow = cell.first; + for (; currentRow < cell.first + itemCellGeom.height(); ++currentRow) { + + const int freeRow = freeSpaceInDirection(QPair(currentRow, cell.second), direction); + + partialSize.setWidth(qMin(partialSize.width(), freeRow)); + + if (freeRow > 0) { + partialSize.setHeight(partialSize.height() + 1); + } else if (partialSize.height() < minimumGridSize.height()) { + break; + } + + if (partialSize.width() >= itemCellGeom.width() + && partialSize.height() >= itemCellGeom.height()) { + break; + } else if (partialSize.width() < minimumGridSize.width()) { + break; + } + } + + if (partialSize.width() >= minimumGridSize.width() + && partialSize.height() >= minimumGridSize.height()) { + + const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); + const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); + + if (direction == AppletsLayout::RightToLeft) { + return QRectF((cell.second + 1) * cellSize().width() - width, + cell.first * cellSize().height(), + width, height); + // AppletsLayout::LeftToRight + } else { + return QRectF(cell.second * cellSize().width(), + cell.first * cellSize().height(), + width, height); + } + } else { + cell.first = currentRow + 1; + } + + } else if (direction == AppletsLayout::TopToBottom || direction == AppletsLayout::BottomToTop) { + partialSize = QSize(0, INT_MAX); + + int currentColumn = cell.second; + for (; currentColumn < cell.second + itemCellGeom.width(); ++currentColumn) { + + const int freeColumn = freeSpaceInDirection(QPair(cell.first, currentColumn), direction); + + partialSize.setHeight(qMin(partialSize.height(), freeColumn)); + + if (freeColumn > 0) { + partialSize.setWidth(partialSize.width() + 1); + } else if (partialSize.width() < minimumGridSize.width()) { + break; + } + + if (partialSize.width() >= itemCellGeom.width() + && partialSize.height() >= itemCellGeom.height()) { + break; + } else if (partialSize.height() < minimumGridSize.height()) { + break; + } + } + + if (partialSize.width() >= minimumGridSize.width() + && partialSize.height() >= minimumGridSize.height()) { + + const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); + const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); + + if (direction == AppletsLayout::BottomToTop) { + return QRectF(cell.second * cellSize().width(), + (cell.first + 1) * cellSize().height() - height, + width, height); + // AppletsLayout::TopToBottom: + } else { + return QRectF(cell.second * cellSize().width(), + cell.first * cellSize().height(), + width, height); + } + } else { + cell.second = currentColumn + 1; + } + } + } + + //We didn't manage to find layout space, return invalid geometry + return QRectF(); +} + + +#include "moc_gridlayoutmanager.cpp" diff --git a/components/containmentlayoutmanager/gridlayoutmanager.h b/components/containmentlayoutmanager/gridlayoutmanager.h new file mode 100644 index 000000000..a5f1b0cd4 --- /dev/null +++ b/components/containmentlayoutmanager/gridlayoutmanager.h @@ -0,0 +1,111 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include "abstractlayoutmanager.h" +#include "appletcontainer.h" + +class AppletsLayout; +class ItemContainer; + +struct Geom { + int x; + int y; + int width; + int height; + int rotation; +}; + +class GridLayoutManager : public AbstractLayoutManager +{ + Q_OBJECT + +public: + GridLayoutManager(AppletsLayout *layout); + ~GridLayoutManager(); + + void layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + + QString serializeLayout() const override; + void parseLayout(const QString &savedLayout) override; + + bool itemIsManaged(ItemContainer *item) override; + + void resetLayout() override; + void resetLayoutFromConfig() override; + + bool restoreItem(ItemContainer *item) override; + + bool isRectAvailable(const QRectF &rect) override; + + + +protected: + // The rectangle as near as possible to the current item geometry which can fit it + QRectF nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const override; + + bool assignSpaceImpl(ItemContainer *item) override; + void releaseSpaceImpl(ItemContainer *item) override; + +private: + // Total cell rows + inline int rows() const; + + // Total cell columns + inline int columns() const; + + // Converts the item pixel-based geometry to a cellsize-based geometry + inline QRect cellBasedGeometry(const QRectF &geom) const; + + // Converts the item pixel-based geometry to a cellsize-based geometry + // This is the bounding geometry, usually larger than cellBasedGeometry + inline QRect cellBasedBoundingGeometry(const QRectF &geom) const; + + // true if the cell is out of the bounds of the containment + inline bool isOutOfBounds(const QPair &cell) const; + + // True if the space for the given cell is available + inline bool isCellAvailable(const QPair &cell) const; + + // Returns the qrect geometry for an item + inline QRectF itemGeometry(QQuickItem *item) const; + + // The next cell given the direction + QPair nextCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + + // The next cell that is available given the direction + QPair nextAvailableCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + + // How many cells are available in the row starting from the given cell and direction + int freeSpaceInDirection(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + + /** + * This reacts to changes in size hints by an item + */ + void adjustToItemSizeHints(ItemContainer *item); + + // What is the item that occupies the point. The point is expressed in cells rather than pixels. a qpair rather a QPointF as QHash doesn't support indicization by QPointF + QHash , ItemContainer *> m_grid; + QHash > > m_pointsForItem; + + QHash m_parsedConfig; +}; + diff --git a/components/containmentlayoutmanager/itemcontainer.cpp b/components/containmentlayoutmanager/itemcontainer.cpp new file mode 100644 index 000000000..5a4dfbdf0 --- /dev/null +++ b/components/containmentlayoutmanager/itemcontainer.cpp @@ -0,0 +1,737 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "itemcontainer.h" +#include "configoverlay.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +ItemContainer::ItemContainer(QQuickItem *parent) + : QQuickItem(parent) +{ + setFiltersChildMouseEvents(true); + setFlags(QQuickItem::ItemIsFocusScope); + setActiveFocusOnTab(true); + setAcceptedMouseButtons(Qt::LeftButton); + + setLayout(qobject_cast(parent)); + + m_editModeTimer = new QTimer(this); + m_editModeTimer->setSingleShot(true); + + connect(this, &QQuickItem::parentChanged, this, [this]() { + setLayout(qobject_cast(parentItem())); + }); + + connect(m_editModeTimer, &QTimer::timeout, this, [this]() { + setEditMode(true); + }); + + + m_sizeHintAdjustTimer = new QTimer(this); + m_sizeHintAdjustTimer->setSingleShot(true); + m_sizeHintAdjustTimer->setInterval(0); + + connect(m_sizeHintAdjustTimer, &QTimer::timeout, this, &ItemContainer::sizeHintsChanged); + + // Lose edit mode when going out of focus + connect(this, &QQuickItem::focusChanged, this, [this]() { + if (!hasFocus()) { + setEditMode(false); + } + }); +} + +ItemContainer::~ItemContainer() +{ + if (m_contentItem) { + m_contentItem->setEnabled(true); + } +} + +QString ItemContainer::key() const +{ + return m_key; +} + +void ItemContainer::setKey(const QString &key) +{ + if (m_key == key) { + return; + } + + m_key = key; + + emit keyChanged(); +} + +bool ItemContainer::editMode() const +{ + return m_editMode; +} + +void ItemContainer::setEditMode(bool editMode) +{ + if (m_editMode == editMode) { + return; + } + + if (editMode && editModeCondition() == Locked) { + return; + } + + m_editMode = editMode; + + // Leave this decision to QML? + if (m_editModeCondition != AfterMouseOver || m_layout->editMode()) { + m_contentItem->setEnabled(!editMode); + } + + if (editMode) { + setZ(1); + } else { + setZ(0); + } + + if (m_mouseDown) { + sendUngrabRecursive(m_contentItem); + grabMouse(); + } + + setConfigOverlayVisible(editMode); + + emit editModeChanged(editMode); +} + +ItemContainer::EditModeCondition ItemContainer::editModeCondition() const +{ + if (m_layout->editModeCondition() == AppletsLayout::Locked) { + return Locked; + } + + return m_editModeCondition; +} + +void ItemContainer::setEditModeCondition(EditModeCondition condition) +{ + if (condition == m_editModeCondition) { + return; + } + + if (condition == Locked) { + setEditMode(false); + } + + m_editModeCondition = condition; + + setAcceptHoverEvents(condition == AfterMouseOver); + + emit editModeConditionChanged(); +} + +AppletsLayout::PreferredLayoutDirection ItemContainer::preferredLayoutDirection() const +{ + return m_preferredLayoutDirection; +} + +void ItemContainer::setPreferredLayoutDirection(AppletsLayout::PreferredLayoutDirection direction) +{ + if (direction == m_preferredLayoutDirection) { + return; + } + + m_preferredLayoutDirection = direction; + + emit preferredLayoutDirectionChanged(); +} + +void ItemContainer::setLayout(AppletsLayout *layout) +{ + if (m_layout == layout) { + return; + } + + m_layout = layout; + if (parentItem() != layout) { + setParentItem(layout); + } + + connect(m_layout, &AppletsLayout::editModeConditionChanged, this, [this]() { + if (m_layout->editModeCondition() == AppletsLayout::Locked) { + setEditMode(false); + } + if ((m_layout->editModeCondition() == AppletsLayout::Locked) != + (m_editModeCondition == ItemContainer::Locked)) { + emit editModeConditionChanged(); + } + }); + emit layoutChanged(); +} + +AppletsLayout *ItemContainer::layout() const +{ + return m_layout; +} + +void ItemContainer::syncChildItemsGeometry(const QSizeF &size) +{ + if (m_contentItem) { + m_contentItem->setPosition(QPointF(m_leftPadding, m_topPadding)); + + m_contentItem->setSize(QSizeF(size.width() - m_leftPadding - m_rightPadding, + size.height() - m_topPadding - m_bottomPadding)); + } + + if (m_backgroundItem) { + m_backgroundItem->setPosition(QPointF(0, 0)); + m_backgroundItem->setSize(size); + } + + if (m_configOverlay) { + m_configOverlay->setPosition(QPointF(0, 0)); + m_configOverlay->setSize(size); + } +} + +QQmlComponent *ItemContainer::configOverlayComponent() const +{ + return m_configOverlayComponent; +} + +void ItemContainer::setConfigOverlayComponent(QQmlComponent *component) +{ + if (component == m_configOverlayComponent) { + return; + } + + m_configOverlayComponent = component; + if (m_configOverlay) { + m_configOverlay->deleteLater(); + m_configOverlay = nullptr; + } + + emit configOverlayComponentChanged(); +} + +ConfigOverlay *ItemContainer::configOverlayItem() const +{ + return m_configOverlay; +} + +QSizeF ItemContainer::initialSize() const +{ + return m_initialSize; +} + +void ItemContainer::setInitialSize(const QSizeF &size) +{ + if (m_initialSize == size) { + return; + } + + m_initialSize = size; + + emit initialSizeChanged(); +} + +bool ItemContainer::configOverlayVisible() const +{ + return m_configOverlay && m_configOverlay->open(); +} + +void ItemContainer::setConfigOverlayVisible(bool visible) +{ + if (!m_configOverlayComponent) { + return; + } + + if (visible == configOverlayVisible()) { + return; + } + + if (visible && !m_configOverlay) { + QQmlContext *context = QQmlEngine::contextForObject(this); + Q_ASSERT(context); + QObject *instance = m_configOverlayComponent->beginCreate(context); + m_configOverlay = qobject_cast(instance); + + if (!m_configOverlay) { + qWarning() << "Error: Applet configOverlay not of ConfigOverlay type"; + if (instance) { + instance->deleteLater(); + } + return; + } + + m_configOverlay->setVisible(false); + m_configOverlay->setItemContainer(this); + m_configOverlay->setParentItem(this); + m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); + m_configOverlay->setZ(999); + m_configOverlay->setPosition(QPointF(0, 0)); + m_configOverlay->setSize(size()); + + m_configOverlayComponent->completeCreate(); + + connect(m_configOverlay, &ConfigOverlay::openChanged, this, [this]() { + emit configOverlayVisibleChanged(m_configOverlay->open()); + }); + + emit configOverlayItemChanged(); + } + + if (m_configOverlay) { + m_configOverlay->setOpen(visible); + } +} + +void ItemContainer::contentData_append(QQmlListProperty *prop, QObject *object) +{ + ItemContainer *container = static_cast(prop->object); + if (!container) { + return; + } + +// QQuickItem *item = qobject_cast(object); + container->m_contentData.append(object); +} + +int ItemContainer::contentData_count(QQmlListProperty *prop) +{ + ItemContainer *container = static_cast(prop->object); + if (!container) { + return 0; + } + + return container->m_contentData.count(); +} + +QObject *ItemContainer::contentData_at(QQmlListProperty *prop, int index) +{ + ItemContainer *container = static_cast(prop->object); + if (!container) { + return nullptr; + } + + if (index < 0 || index >= container->m_contentData.count()) { + return nullptr; + } + return container->m_contentData.value(index); +} + +void ItemContainer::contentData_clear(QQmlListProperty *prop) +{ + ItemContainer *container = static_cast(prop->object); + if (!container) { + return; + } + + return container->m_contentData.clear(); +} + +QQmlListProperty ItemContainer::contentData() +{ + return QQmlListProperty(this, nullptr, + contentData_append, + contentData_count, + contentData_at, + contentData_clear); +} + +void ItemContainer::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + syncChildItemsGeometry(newGeometry.size()); + QQuickItem::geometryChanged(newGeometry, oldGeometry); + emit contentWidthChanged(); + emit contentHeightChanged(); +} + +void ItemContainer::componentComplete() +{ + if (!m_contentItem) { + //qWarning()<<"Creting default contentItem"; + m_contentItem = new QQuickItem(this); + syncChildItemsGeometry(size()); + } + + for (auto *o : m_contentData) { + QQuickItem *item = qobject_cast(o); + if (item) { + item->setParentItem(m_contentItem); + } + } + + // Search for the Layout attached property + // Qt6: this should become public api + // https://bugreports.qt.io/browse/QTBUG-77103 + for (auto *o : children()) { + if (o->inherits("QQuickLayoutAttached")) { + m_layoutAttached = o; + } + } + + if (m_layoutAttached) { + //NOTE: new syntax cannot be used because we don't have access to the QQuickLayoutAttached class + connect(m_layoutAttached, SIGNAL(minimumHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); + connect(m_layoutAttached, SIGNAL(minimumWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); + + connect(m_layoutAttached, SIGNAL(preferredHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); + connect(m_layoutAttached, SIGNAL(preferredWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); + + connect(m_layoutAttached, SIGNAL(maximumHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); + connect(m_layoutAttached, SIGNAL(maximumWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); + } + QQuickItem::componentComplete(); +} + +void ItemContainer::sendUngrabRecursive(QQuickItem *item) +{ + if (!item || !item->window()) { + return; + } + + for (auto *child : item->childItems()) { + sendUngrabRecursive(child); + } + + QEvent ev(QEvent::UngrabMouse); + + item->window()->sendEvent(item, &ev); +} + +bool ItemContainer::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + // Don't filter the configoverlay + if (item == m_configOverlay + || (m_configOverlay && m_configOverlay->isAncestorOf(item)) + || (!m_editMode && m_editModeCondition == Manual)) { + return QQuickItem::childMouseEventFilter(item, event); + } + + //give more time before closing + if (m_closeEditModeTimer && m_closeEditModeTimer->isActive()) { + m_closeEditModeTimer->start(); + } + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent *me = static_cast(event); + if (me->button() != Qt::LeftButton && !(me->buttons() & Qt::LeftButton)) { + return QQuickItem::childMouseEventFilter(item, event); + } + forceActiveFocus(Qt::MouseFocusReason); + m_mouseDown = true; + m_mouseSynthetizedFromTouch = me->source() == Qt::MouseEventSynthesizedBySystem || me->source() == Qt::MouseEventSynthesizedByQt; + if (m_configOverlay) { + m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); + } + + const bool wasEditMode = m_editMode; + if (m_layout->editMode()) { + setEditMode(true); + } else if (m_editModeCondition == AfterPressAndHold) { + m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + } + m_lastMousePosition = me->windowPos(); + m_mouseDownPosition = me->windowPos(); + + if (m_editMode && !wasEditMode) { + return true; + } + + } else if (event->type() == QEvent::MouseMove) { + QMouseEvent *me = static_cast(event); + + if (!m_editMode + && QPointF(me->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { + m_editModeTimer->stop(); + } + + } else if (event->type() == QEvent::MouseButtonRelease) { + m_editModeTimer->stop(); + m_mouseDown = false; + m_mouseSynthetizedFromTouch = false; + ungrabMouse(); + } + + return QQuickItem::childMouseEventFilter(item, event); +} + +void ItemContainer::mousePressEvent(QMouseEvent *event) +{ + forceActiveFocus(Qt::MouseFocusReason); + + if (!m_editMode && m_editModeCondition == Manual) { + return; + } + + m_mouseDown = true; + m_mouseSynthetizedFromTouch = event->source() == Qt::MouseEventSynthesizedBySystem || event->source() == Qt::MouseEventSynthesizedByQt; + if (m_configOverlay) { + m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); + } + + if (m_layout->editMode()) { + setEditMode(true); + } + + if (m_editMode) { + grabMouse(); + } else if (m_editModeCondition == AfterPressAndHold) { + m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + } + + m_lastMousePosition = event->windowPos(); + m_mouseDownPosition = event->windowPos(); + event->accept(); +} + +void ItemContainer::mouseReleaseEvent(QMouseEvent *event) +{ + Q_UNUSED(event); + + if (!m_layout + || (!m_editMode && m_editModeCondition == Manual)) { + return; + } + + m_mouseDown = false; + m_mouseSynthetizedFromTouch = false; + m_editModeTimer->stop(); + ungrabMouse(); + + if (m_editMode && !m_layout->itemIsManaged(this)) { + m_layout->hidePlaceHolder(); + m_layout->positionItem(this); + } +} + +void ItemContainer::mouseMoveEvent(QMouseEvent *event) +{ + if ((event->button() == Qt::NoButton && event->buttons() == Qt::NoButton) + || !m_layout + || (!m_editMode && m_editModeCondition == Manual)) { + return; + } + + if (!m_editMode + && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { + if (m_editModeCondition == AfterPress) { + setEditMode(true); + } else { + m_editModeTimer->stop(); + } + } + + if (!m_editMode) { + return; + } + + if (m_layout->itemIsManaged(this)) { + m_layout->releaseSpace(this); + grabMouse(); + + } else { + setPosition(QPointF(x() + event->windowPos().x() - m_lastMousePosition.x(), + y() + event->windowPos().y() - m_lastMousePosition.y())); + + m_layout->showPlaceHolderForItem(this); + + emit userDrag(QPointF(x(), y()), event->pos()); + } + m_lastMousePosition = event->windowPos(); +} + +void ItemContainer::mouseUngrabEvent() +{ + m_mouseDown = false; + m_mouseSynthetizedFromTouch = false; + m_editModeTimer->stop(); + ungrabMouse(); + + if (m_editMode && !m_layout->itemIsManaged(this)) { + m_layout->hidePlaceHolder(); + m_layout->positionItem(this); + } +} + +void ItemContainer::hoverEnterEvent(QHoverEvent *event) +{ + Q_UNUSED(event); + + if (m_editModeCondition != AfterMouseOver) { + return; + } + + if (m_closeEditModeTimer) { + m_closeEditModeTimer->stop(); + } + + m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); +} + +void ItemContainer::hoverLeaveEvent(QHoverEvent *event) +{ + Q_UNUSED(event); + + if (m_editModeCondition != AfterMouseOver) { + return; + } + + m_editModeTimer->stop(); + if (!m_closeEditModeTimer) { + m_closeEditModeTimer = new QTimer(this); + m_closeEditModeTimer->setSingleShot(true); + m_closeEditModeTimer->setInterval(500); + connect(m_closeEditModeTimer, &QTimer::timeout, this, [this] () { + setEditMode(false); + }); + } + m_closeEditModeTimer->start(); +} + +QQuickItem *ItemContainer::contentItem() const +{ + return m_contentItem; +} + +void ItemContainer::setContentItem(QQuickItem *item) +{ + if (m_contentItem == item) { + return; + } + + m_contentItem = item; + item->setParentItem(this); + m_contentItem->setPosition(QPointF(m_leftPadding, m_topPadding)); + + m_contentItem->setSize(QSizeF(width() - m_leftPadding - m_rightPadding, + height() - m_topPadding - m_bottomPadding)); + + emit contentItemChanged(); +} + +QQuickItem *ItemContainer::background() const +{ + return m_backgroundItem; +} + +void ItemContainer::setBackground(QQuickItem *item) +{ + if (m_backgroundItem == item) { + return; + } + + m_backgroundItem = item; + m_backgroundItem->setParentItem(this); + m_backgroundItem->setPosition(QPointF(0, 0)); + m_backgroundItem->setSize(size()); + + emit backgroundChanged(); +} + +int ItemContainer::leftPadding() const +{ + return m_leftPadding; +} + +void ItemContainer::setLeftPadding(int padding) +{ + if (m_leftPadding == padding) { + return; + } + + m_leftPadding = padding; + syncChildItemsGeometry(size()); + emit leftPaddingChanged(); + emit contentWidthChanged(); +} + + +int ItemContainer::topPadding() const +{ + return m_topPadding; +} + +void ItemContainer::setTopPadding(int padding) +{ + if (m_topPadding == padding) { + return; + } + + m_topPadding = padding; + syncChildItemsGeometry(size()); + emit topPaddingChanged(); + emit contentHeightChanged(); +} + + +int ItemContainer::rightPadding() const +{ + return m_rightPadding; +} + +void ItemContainer::setRightPadding(int padding) +{ + if (m_rightPadding == padding) { + return; + } + + m_rightPadding = padding; + syncChildItemsGeometry(size()); + emit rightPaddingChanged(); + emit contentWidthChanged(); +} + + +int ItemContainer::bottomPadding() const +{ + return m_bottomPadding; +} + +void ItemContainer::setBottomPadding(int padding) +{ + if (m_bottomPadding == padding) { + return; + } + + m_bottomPadding = padding; + syncChildItemsGeometry(size()); + emit bottomPaddingChanged(); + emit contentHeightChanged(); +} + +int ItemContainer::contentWidth() const +{ + return width() - m_leftPadding - m_rightPadding; +} + +int ItemContainer::contentHeight() const +{ + return height() - m_topPadding - m_bottomPadding; +} + +#include "moc_itemcontainer.cpp" diff --git a/components/containmentlayoutmanager/itemcontainer.h b/components/containmentlayoutmanager/itemcontainer.h new file mode 100644 index 000000000..52c93e2e8 --- /dev/null +++ b/components/containmentlayoutmanager/itemcontainer.h @@ -0,0 +1,239 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include +#include +#include + +#include "appletslayout.h" + +class QTimer; + +class ConfigOverlay; + +class ItemContainer: public QQuickItem +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(AppletsLayout *layout READ layout NOTIFY layoutChanged) + //TODO: make it unchangeable? probably not + Q_PROPERTY(QString key READ key WRITE setKey NOTIFY keyChanged) + Q_PROPERTY(ItemContainer::EditModeCondition editModeCondition READ editModeCondition WRITE setEditModeCondition NOTIFY editModeConditionChanged) + Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged) + Q_PROPERTY(AppletsLayout::PreferredLayoutDirection preferredLayoutDirection READ preferredLayoutDirection WRITE setPreferredLayoutDirection NOTIFY preferredLayoutDirectionChanged) + + Q_PROPERTY(QQmlComponent *configOverlayComponent READ configOverlayComponent WRITE setConfigOverlayComponent NOTIFY configOverlayComponentChanged) + Q_PROPERTY(bool configOverlayVisible READ configOverlayVisible WRITE setConfigOverlayVisible NOTIFY configOverlayVisibleChanged) + Q_PROPERTY(QQuickItem *configOverlayItem READ configOverlayItem NOTIFY configOverlayItemChanged) + + /** + * Initial size this container asks to have upon creation. only positive values are considered + */ + Q_PROPERTY(QSizeF initialSize READ initialSize WRITE setInitialSize NOTIFY initialSizeChanged) + // From there mostly a clone of QQC2 Control + Q_PROPERTY(QQuickItem *contentItem READ contentItem WRITE setContentItem NOTIFY contentItemChanged) + Q_PROPERTY(QQuickItem *background READ background WRITE setBackground NOTIFY backgroundChanged) + + /** + * Padding adds a space between each edge of the content item and the background item, effectively controlling the size of the content item. + */ + Q_PROPERTY(int leftPadding READ leftPadding WRITE setLeftPadding NOTIFY leftPaddingChanged) + Q_PROPERTY(int rightPadding READ rightPadding WRITE setRightPadding NOTIFY rightPaddingChanged) + Q_PROPERTY(int topPadding READ topPadding WRITE setTopPadding NOTIFY topPaddingChanged) + Q_PROPERTY(int bottomPadding READ bottomPadding WRITE setBottomPadding NOTIFY bottomPaddingChanged) + + /** + * The size of the contents: the size of this item minus the padding + */ + Q_PROPERTY(int contentWidth READ contentWidth NOTIFY contentWidthChanged) + Q_PROPERTY(int contentHeight READ contentHeight NOTIFY contentHeightChanged) + + Q_PROPERTY(QQmlListProperty contentData READ contentData FINAL) + // Q_CLASSINFO("DeferredPropertyNames", "background,contentItem") + Q_CLASSINFO("DefaultProperty", "contentData") + +public: + enum EditModeCondition { + Locked = AppletsLayout::EditModeCondition::Locked, + Manual = AppletsLayout::EditModeCondition::Manual, + AfterPressAndHold = AppletsLayout::EditModeCondition::AfterPressAndHold, + AfterPress, + AfterMouseOver + }; + Q_ENUMS(EditModeCondition) + + ItemContainer(QQuickItem *parent = nullptr); + ~ItemContainer(); + + QQmlListProperty contentData(); + + QString key() const; + void setKey(const QString &id); + + bool editMode() const; + void setEditMode(bool edit); + + EditModeCondition editModeCondition() const; + void setEditModeCondition(EditModeCondition condition); + + AppletsLayout::PreferredLayoutDirection preferredLayoutDirection() const; + void setPreferredLayoutDirection(AppletsLayout::PreferredLayoutDirection direction); + + QQmlComponent *configOverlayComponent() const; + void setConfigOverlayComponent(QQmlComponent *component); + + bool configOverlayVisible() const; + void setConfigOverlayVisible(bool visible); + + //TODO: keep this accessible? + ConfigOverlay *configOverlayItem() const; + + QSizeF initialSize() const; + void setInitialSize(const QSizeF &size); + + // Control-like api + QQuickItem *contentItem() const; + void setContentItem(QQuickItem *item); + + QQuickItem *background() const; + void setBackground(QQuickItem *item); + + // Setters and getters for the padding + int leftPadding() const; + void setLeftPadding(int padding); + + int topPadding() const; + void setTopPadding(int padding); + + int rightPadding() const; + void setRightPadding(int padding); + + int bottomPadding() const; + void setBottomPadding(int padding); + + int contentWidth() const; + int contentHeight() const; + + AppletsLayout *layout() const; + + // Not for QML + void setLayout(AppletsLayout *layout); + + QObject *layoutAttached() const {return m_layoutAttached;} + +protected: + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + + //void classBegin() override; + void componentComplete() override; + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + void hoverEnterEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *event) override; + +Q_SIGNALS: + + /** + * The user manually dragged the ItemContainer around + * @param newPosition new position of the ItemContainer in parent coordinates + * @param dragCenter position in ItemContainer coordinates of the drag hotspot, i.e. where the user pressed the mouse or the + * finger over the ItemContainer + */ + void userDrag(const QPointF &newPosition, const QPointF &dragCenter); + + /** + * The attached layout object changed some of its size hints + */ + void sizeHintsChanged(); + + //QML property notifiers + void layoutChanged(); + void keyChanged(); + void editModeConditionChanged(); + void editModeChanged(bool editMode); + void preferredLayoutDirectionChanged(); + void configOverlayComponentChanged(); + void configOverlayItemChanged(); + void initialSizeChanged(); + void configOverlayVisibleChanged(bool configOverlayVisile); + + + void backgroundChanged(); + void contentItemChanged(); + void leftPaddingChanged(); + void rightPaddingChanged(); + void topPaddingChanged(); + void bottomPaddingChanged(); + void contentWidthChanged(); + void contentHeightChanged(); + +private: + void syncChildItemsGeometry(const QSizeF &size); + void sendUngrabRecursive(QQuickItem *item); + + //internal accessorts for the contentData QProperty + static void contentData_append(QQmlListProperty *prop, QObject *object); + static int contentData_count(QQmlListProperty *prop); + static QObject *contentData_at(QQmlListProperty *prop, int index); + static void contentData_clear(QQmlListProperty *prop); + + + QPointer m_contentItem; + QPointer m_backgroundItem; + + //Internal implementation detail: this is used to reparent all items to contentItem + QList m_contentData; + + /** + * Padding adds a space between each edge of the content item and the background item, effectively controlling the size of the content item. + */ + int m_leftPadding = 0; + int m_rightPadding = 0; + int m_topPadding = 0; + int m_bottomPadding = 0; + + + QString m_key; + + QPointer m_layout; + QTimer *m_editModeTimer = nullptr; + QTimer *m_closeEditModeTimer = nullptr; + QTimer *m_sizeHintAdjustTimer = nullptr; + QObject *m_layoutAttached = nullptr; + EditModeCondition m_editModeCondition = Manual; + QSizeF m_initialSize; + + QPointer m_configOverlayComponent; + ConfigOverlay *m_configOverlay = nullptr; + + QPointF m_lastMousePosition = QPoint(-1, -1); + QPointF m_mouseDownPosition = QPoint(-1, -1); + AppletsLayout::PreferredLayoutDirection m_preferredLayoutDirection = AppletsLayout::Closest; + bool m_editMode = false; + bool m_mouseDown = false; + bool m_mouseSynthetizedFromTouch = false; +}; + diff --git a/components/containmentlayoutmanager/qml/BasicAppletContainer.qml b/components/containmentlayoutmanager/qml/BasicAppletContainer.qml new file mode 100644 index 000000000..8b95fbb42 --- /dev/null +++ b/components/containmentlayoutmanager/qml/BasicAppletContainer.qml @@ -0,0 +1,84 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager + +ContainmentLayoutManager.AppletContainer { + id: appletContainer + editModeCondition: plasmoid.immutable + ? ContainmentLayoutManager.ItemContainer.Manual + : ContainmentLayoutManager.ItemContainer.AfterPressAndHold + + Layout.minimumWidth: { + if (!applet) { + return leftPadding + rightPadding; + } + + if (applet.preferredRepresentation != applet.fullRepresentation + && applet.compactRepresentationItem + ) { + return applet.compactRepresentationItem.Layout.minimumWidth + leftPadding + rightPadding; + } else { + return applet.Layout.minimumWidth + leftPadding + rightPadding; + } + } + Layout.minimumHeight: { + if (!applet) { + return topPadding + bottomPadding; + } + + if (applet.preferredRepresentation != applet.fullRepresentation + && applet.compactRepresentationItem + ) { + return applet.compactRepresentationItem.Layout.minimumHeight + topPadding + bottomPadding; + } else { + return applet.Layout.minimumHeight + topPadding + bottomPadding; + } + } + + Layout.preferredWidth: Math.max(applet.Layout.minimumWidth, applet.Layout.preferredWidth) + Layout.preferredHeight: Math.max(applet.Layout.minimumHeight, applet.Layout.preferredHeight) + + Layout.maximumWidth: applet.Layout.maximumWidth + Layout.maximumHeight: applet.Layout.maximumHeight + + leftPadding: background.margins.left + topPadding: background.margins.top + rightPadding: background.margins.right + bottomPadding: background.margins.bottom + + initialSize.width: applet.switchWidth + leftPadding + rightPadding + initialSize.height: applet.switchHeight + topPadding + bottomPadding + + background: PlasmaCore.FrameSvgItem { + imagePath: contentItem && contentItem.backgroundHints == PlasmaCore.Types.StandardBackground ? "widgets/background" : "" + } + + busyIndicatorComponent: PlasmaComponents.BusyIndicator { + anchors.centerIn: parent + visible: applet.busy + running: visible + } +} diff --git a/components/containmentlayoutmanager/qml/ConfigOverlayWithHandles.qml b/components/containmentlayoutmanager/qml/ConfigOverlayWithHandles.qml new file mode 100644 index 000000000..5f3c2ddb7 --- /dev/null +++ b/components/containmentlayoutmanager/qml/ConfigOverlayWithHandles.qml @@ -0,0 +1,165 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents + +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager + +import "private" + +ContainmentLayoutManager.ConfigOverlay { + id: overlay + + opacity: open + Behavior on opacity { + OpacityAnimator { + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } + + MultiPointTouchArea { + anchors.fill: parent + property real previousMinX + property real previousMinY + property real previousMaxX + property real previousMaxY + property bool pinching: false + mouseEnabled: false + maximumTouchPoints: 2 + touchPoints: [ + TouchPoint { id: point1 }, + TouchPoint { id: point2 } + ] + + onPressed: { + overlay.itemContainer.layout.releaseSpace(overlay.itemContainer); + previousMinX = point1.sceneX; + previousMinY = point1.sceneY; + } + + onUpdated: { + var minX; + var minY; + var maxX; + var maxY; + + if (point1.pressed && point2.pressed) { + minX = Math.min(point1.sceneX, point2.sceneX); + minY = Math.min(point1.sceneY, point2.sceneY); + + maxX = Math.max(point1.sceneX, point2.sceneX); + maxY = Math.max(point1.sceneY, point2.sceneY); + } else { + minX = point1.pressed ? point1.sceneX : point2.sceneX; + minY = point1.pressed ? point1.sceneY : point2.sceneY; + maxX = -1; + maxY = -1; + } + + if (pinching == (point1.pressed && point2.pressed)) { + overlay.itemContainer.x += minX - previousMinX; + overlay.itemContainer.y += minY - previousMinY; + + if (pinching) { + overlay.itemContainer.width += maxX - previousMaxX + previousMinX - minX; + overlay.itemContainer.height += maxY - previousMaxY + previousMinY - minY; + } + overlay.itemContainer.layout.showPlaceHolderForItem(overlay.itemContainer); + } + + pinching = point1.pressed && point2.pressed + previousMinX = minX; + previousMinY = minY; + previousMaxX = maxX; + previousMaxY = maxY; + } + onReleased: { + if (point1.pressed || point2.pressed) { + return; + } + overlay.itemContainer.layout.positionItem(overlay.itemContainer); + overlay.itemContainer.layout.hidePlaceHolder(); + pinching = false + } + onCanceled: released() + } + + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.TopLeft + anchors { + horizontalCenter: parent.left + verticalCenter: parent.top + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.Left + anchors { + horizontalCenter: parent.left + verticalCenter: parent.verticalCenter + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.BottomLeft + anchors { + horizontalCenter: parent.left + verticalCenter: parent.bottom + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.Bottom + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.bottom + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.BottomRight + anchors { + horizontalCenter: parent.right + verticalCenter: parent.bottom + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.Right + anchors { + horizontalCenter: parent.right + verticalCenter: parent.verticalCenter + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.TopRight + anchors { + horizontalCenter: parent.right + verticalCenter: parent.top + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.Top + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.top + } + } +} + diff --git a/components/containmentlayoutmanager/qml/PlaceHolder.qml b/components/containmentlayoutmanager/qml/PlaceHolder.qml new file mode 100644 index 000000000..376f171cf --- /dev/null +++ b/components/containmentlayoutmanager/qml/PlaceHolder.qml @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.12 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager + +ContainmentLayoutManager.ItemContainer { + enabled: false + PlasmaCore.FrameSvgItem { + anchors.fill:parent + imagePath: "widgets/viewitem" + prefix: "hover" + opacity: 0.5 + } + Behavior on opacity { + NumberAnimation { + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } +} diff --git a/components/containmentlayoutmanager/qml/private/BasicResizeHandle.qml b/components/containmentlayoutmanager/qml/private/BasicResizeHandle.qml new file mode 100644 index 000000000..c980064fa --- /dev/null +++ b/components/containmentlayoutmanager/qml/private/BasicResizeHandle.qml @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.12 + +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager + + +ContainmentLayoutManager.ResizeHandle { + width: overlay.touchInteraction ? units.gridUnit * 2 : units.gridUnit + height: width + Rectangle { + color: resizeBlocked ? theme.negativeTextColor : theme.backgroundColor + anchors.fill: parent + radius: width + opacity: 0.6 + } + scale: overlay.open ? 1 : 0 + Behavior on scale { + NumberAnimation { + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } +} + diff --git a/components/containmentlayoutmanager/qml/qmldir b/components/containmentlayoutmanager/qml/qmldir new file mode 100644 index 000000000..d7299a528 --- /dev/null +++ b/components/containmentlayoutmanager/qml/qmldir @@ -0,0 +1,7 @@ +module org.kde.plasma.private.containmentlayoutmanager + +plugin containmentlayoutmanagerplugin +BasicAppletContainer 1.0 BasicAppletContainer.qml +ConfigOverlayWithHandles 1.0 ConfigOverlayWithHandles.qml +PlaceHolder 1.0 PlaceHolder.qml + diff --git a/components/containmentlayoutmanager/resizehandle.cpp b/components/containmentlayoutmanager/resizehandle.cpp new file mode 100644 index 000000000..0b3fb5c5c --- /dev/null +++ b/components/containmentlayoutmanager/resizehandle.cpp @@ -0,0 +1,243 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "resizehandle.h" + +#include +#include + +ResizeHandle::ResizeHandle(QQuickItem *parent) + : QQuickItem(parent) +{ + setAcceptedMouseButtons(Qt::LeftButton); + + QQuickItem *candidate = parent; + while (candidate) { + ConfigOverlay *overlay = qobject_cast(candidate); + if (overlay) { + setConfigOverlay(overlay); + break; + } + + candidate = candidate->parentItem(); + } + + connect(this, &QQuickItem::parentChanged, this, [this]() { + QQuickItem *candidate = parentItem(); + while (candidate) { + ConfigOverlay *overlay = qobject_cast(candidate); + if (overlay) { + setConfigOverlay(overlay); + break; + } + + candidate = candidate->parentItem(); + } + }); + + auto syncCursor = [this] () { + switch (m_resizeCorner) { + case Left: + case Right: + setCursor(QCursor(Qt::SizeHorCursor)); + break; + case Top: + case Bottom: + setCursor(QCursor(Qt::SizeVerCursor)); + break; + case TopLeft: + case BottomRight: + setCursor(QCursor(Qt::SizeFDiagCursor)); + break; + case TopRight: + case BottomLeft: + default: + setCursor(Qt::SizeBDiagCursor); + } + }; + + syncCursor(); + connect(this, &ResizeHandle::resizeCornerChanged, this, syncCursor); +} + +ResizeHandle::~ResizeHandle() +{ +} + +bool ResizeHandle::resizeBlocked() const +{ + return m_resizeWidthBlocked || m_resizeHeightBlocked; +} + +bool ResizeHandle::resizeLeft() const +{ + return m_resizeCorner == Left || m_resizeCorner == TopLeft || m_resizeCorner == BottomLeft; +} + +bool ResizeHandle::resizeTop() const +{ + return m_resizeCorner == Top || m_resizeCorner == TopLeft || m_resizeCorner == TopRight; +} + +bool ResizeHandle::resizeRight() const +{ + return m_resizeCorner == Right || m_resizeCorner == TopRight ||m_resizeCorner == BottomRight; +} + +bool ResizeHandle::resizeBottom() const +{ + return m_resizeCorner == Bottom || m_resizeCorner == BottomLeft || m_resizeCorner == BottomRight; +} + +void ResizeHandle::setResizeBlocked(bool width, bool height) +{ + if (m_resizeWidthBlocked == width && m_resizeHeightBlocked == height) { + return; + } + + m_resizeWidthBlocked = width; + m_resizeHeightBlocked = height; + + emit resizeBlockedChanged(); +} + + +void ResizeHandle::mousePressEvent(QMouseEvent *event) +{ + ItemContainer *itemContainer = m_configOverlay->itemContainer(); + if (!itemContainer) { + return; + } + m_mouseDownPosition = event->windowPos(); + m_mouseDownGeometry = QRectF(itemContainer->x(), itemContainer->y(), itemContainer->width(), itemContainer->height()); + setResizeBlocked(false, false); + event->accept(); +} + +void ResizeHandle::mouseMoveEvent(QMouseEvent *event) +{ + if (!m_configOverlay || !m_configOverlay->itemContainer()) { + return; + } + + ItemContainer *itemContainer = m_configOverlay->itemContainer(); + AppletsLayout *layout = itemContainer->layout(); + + if (!layout) { + return; + } + + layout->releaseSpace(itemContainer); + const QPointF difference = m_mouseDownPosition - event->windowPos(); + + QSizeF minimumSize = QSize(layout->minimumItemWidth(), layout->minimumItemHeight()); + if (itemContainer->layoutAttached()) { + minimumSize.setWidth(qMax(minimumSize.width(), itemContainer->layoutAttached()->property("minimumWidth").toReal())); + minimumSize.setHeight(qMax(minimumSize.height(), itemContainer->layoutAttached()->property("minimumHeight").toReal())); + } + + //Now make minimumSize an integer number of cells + minimumSize.setWidth(ceil(minimumSize.width() / layout->cellWidth()) * layout->cellWidth()); + minimumSize.setHeight(ceil(minimumSize.height() / layout->cellWidth()) * layout->cellHeight()); + + // Horizontal resize + if (resizeLeft()) { + const qreal width = qMax(minimumSize.width(), m_mouseDownGeometry.width() + difference.x()); + const qreal x = m_mouseDownGeometry.x() + (m_mouseDownGeometry.width() - width); + + // -1 to have a bit of margins around + if (layout->isRectAvailable(x - 1, m_mouseDownGeometry.y(), width, m_mouseDownGeometry.height())) { + itemContainer->setX(x); + itemContainer->setWidth(width); + setResizeBlocked(m_mouseDownGeometry.width() + difference.x() < minimumSize.width(), m_resizeHeightBlocked); + } else { + setResizeBlocked(true, m_resizeHeightBlocked); + } + } else if (resizeRight()) { + const qreal width = qMax(minimumSize.width(), m_mouseDownGeometry.width() - difference.x()); + + if (layout->isRectAvailable(m_mouseDownGeometry.x(), m_mouseDownGeometry.y(), width, m_mouseDownGeometry.height())) { + itemContainer->setWidth(width); + setResizeBlocked(m_mouseDownGeometry.width() - difference.x() < minimumSize.width(), m_resizeHeightBlocked); + } else { + setResizeBlocked(true, m_resizeHeightBlocked); + } + } + + // Vertical Resize + if (resizeTop()) { + const qreal height = qMax(minimumSize.height(), m_mouseDownGeometry.height() + difference.y()); + const qreal y = m_mouseDownGeometry.y() + (m_mouseDownGeometry.height() - height); + + // -1 to have a bit of margins around + if (layout->isRectAvailable(m_mouseDownGeometry.x(), y - 1, m_mouseDownGeometry.width(), m_mouseDownGeometry.height())) { + itemContainer->setY(y); + itemContainer->setHeight(height); + setResizeBlocked(m_resizeWidthBlocked, + m_mouseDownGeometry.height() + difference.y() < minimumSize.height()); + } else { + setResizeBlocked(m_resizeWidthBlocked, true); + } + } else if (resizeBottom()) { + const qreal height = qMax(minimumSize.height(), m_mouseDownGeometry.height() - difference.y()); + + if (layout->isRectAvailable(m_mouseDownGeometry.x(), m_mouseDownGeometry.y(), m_mouseDownGeometry.width(), height)) { + itemContainer->setHeight(qMax(height, minimumSize.height())); + setResizeBlocked(m_resizeWidthBlocked, + m_mouseDownGeometry.height() - difference.y() < minimumSize.height()); + } else { + setResizeBlocked(m_resizeWidthBlocked, true); + } + } + + event->accept(); +} + +void ResizeHandle::mouseReleaseEvent(QMouseEvent *event) +{ + if (!m_configOverlay || !m_configOverlay->itemContainer()) { + return; + } + + ItemContainer *itemContainer = m_configOverlay->itemContainer(); + AppletsLayout *layout = itemContainer->layout(); + + if (!layout) { + return; + } + + layout->positionItem(itemContainer); + + event->accept(); + + setResizeBlocked(false, false); + emit resizeBlockedChanged(); +} + +void ResizeHandle::setConfigOverlay(ConfigOverlay *handle) +{ + if (handle == m_configOverlay) { + return; + } + + m_configOverlay = handle; +} + +#include "moc_resizehandle.cpp" diff --git a/components/containmentlayoutmanager/resizehandle.h b/components/containmentlayoutmanager/resizehandle.h new file mode 100644 index 000000000..f176d64a0 --- /dev/null +++ b/components/containmentlayoutmanager/resizehandle.h @@ -0,0 +1,77 @@ +/* + * Copyright 2019 by Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include + +#include "configoverlay.h" + +class ResizeHandle: public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(Corner resizeCorner MEMBER m_resizeCorner NOTIFY resizeCornerChanged) + Q_PROPERTY(bool resizeBlocked READ resizeBlocked NOTIFY resizeBlockedChanged) + +public: + enum Corner { + Left = 0, + TopLeft, + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft + }; + Q_ENUMS(Corner) + + ResizeHandle(QQuickItem *parent = nullptr); + ~ResizeHandle(); + + bool resizeBlocked() const; + +protected: + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + +Q_SIGNALS: + void resizeCornerChanged(); + void resizeBlockedChanged(); + +private: + void setConfigOverlay(ConfigOverlay *configOverlay); + + inline bool resizeLeft() const; + inline bool resizeTop() const; + inline bool resizeRight() const; + inline bool resizeBottom() const; + void setResizeBlocked(bool width, bool height); + + QPointF m_mouseDownPosition; + QRectF m_mouseDownGeometry; + + QPointer m_configOverlay; + Corner m_resizeCorner = Left; + bool m_resizeWidthBlocked = false; + bool m_resizeHeightBlocked = false; +}; +