From 580afdfc9c0f0aac2f516e0babc4acbcc0ee543d Mon Sep 17 00:00:00 2001 From: Devin Lin Date: Sat, 21 Oct 2023 20:59:27 -0700 Subject: [PATCH] folio: Rewrite fix landscape favourites bar drag and drop, and cleanup folder fix drawer scrolling add settings --- .../mobileshell/components/swipearea.cpp | 54 +- components/mobileshell/components/swipearea.h | 18 +- components/mobileshell/mobileshellplugin.cpp | 1 + .../qml/widgets/krunner/KRunnerScreen.qml | 221 ++++ .../qml/widgets/krunner/KRunnerWidget.qml | 2 +- components/mobileshell/resources.qrc | 1 + containments/homescreens/folio/CMakeLists.txt | 18 +- containments/homescreens/folio/README.md | 26 + .../folio/applicationlistmodel.cpp | 121 ++ .../homescreens/folio/applicationlistmodel.h | 45 + .../homescreens/folio/delegatetoucharea.cpp | 302 +++++ .../homescreens/folio/delegatetoucharea.h | 81 ++ containments/homescreens/folio/dragstate.cpp | 883 ++++++++++++++ containments/homescreens/folio/dragstate.h | 156 +++ .../homescreens/folio/favouritesmodel.cpp | 403 +++++++ .../homescreens/folio/favouritesmodel.h | 80 ++ .../homescreens/folio/folioapplication.cpp | 139 +++ .../homescreens/folio/folioapplication.h | 62 + .../folio/folioapplicationfolder.cpp | 468 ++++++++ .../folio/folioapplicationfolder.h | 138 +++ .../homescreens/folio/foliodelegate.cpp | 88 ++ .../homescreens/folio/foliodelegate.h | 44 + .../homescreens/folio/foliosettings.cpp | 141 +++ .../homescreens/folio/foliosettings.h | 69 ++ containments/homescreens/folio/homescreen.cpp | 54 +- .../homescreens/folio/homescreenstate.cpp | 1027 +++++++++++++++++ .../homescreens/folio/homescreenstate.h | 400 +++++++ .../folio/package/contents/config/main.xml | 21 - .../folio/package/contents/ui/AppDrawer.qml | 85 ++ .../package/contents/ui/AppDrawerGrid.qml | 109 ++ .../package/contents/ui/AppDrawerHeader.qml | 32 + .../package/contents/ui/ContextMenuLoader.qml | 44 + .../package/contents/ui/DelegateDragItem.qml | 166 +++ .../package/contents/ui/FavoriteStrip.qml | 40 - .../package/contents/ui/FavouritesBar.qml | 223 ++++ .../package/contents/ui/FlickContainer.qml | 77 -- .../folio/package/contents/ui/FolderView.qml | 297 +++++ .../package/contents/ui/FolderViewTitle.qml | 104 ++ .../package/contents/ui/HomeDelegate.qml | 233 ---- .../folio/package/contents/ui/HomeScreen.qml | 493 ++++++-- .../contents/ui/HomeScreenContents.qml | 239 ---- .../package/contents/ui/HomeScreenPage.qml | 242 ++++ .../package/contents/ui/HomeScreenPages.qml | 177 +-- .../package/contents/ui/HomeScreenState.qml | 434 ------- .../package/contents/ui/LauncherContainer.qml | 85 -- .../contents/ui/LauncherDragManager.qml | 298 ----- .../package/contents/ui/LauncherRepeater.qml | 128 -- .../contents/ui/MobileAppletContainer.qml | 126 -- .../contents/ui/PlaceholderDelegate.qml | 45 + .../ui/appdrawer/AbstractAppDrawer.qml | 175 --- .../contents/ui/appdrawer/AppDrawerHeader.qml | 55 - .../contents/ui/appdrawer/AppDrawerLoader.qml | 80 -- .../ui/appdrawer/DrawerGridDelegate.qml | 166 --- .../ui/appdrawer/DrawerListDelegate.qml | 108 -- .../ui/appdrawer/GridViewAppDrawer.qml | 102 -- .../ui/appdrawer/ListViewAppDrawer.qml | 87 -- .../contents/ui/delegate/AbstractDelegate.qml | 128 ++ .../contents/ui/delegate/AppDelegate.qml | 111 ++ .../ui/delegate/AppFolderDelegate.qml | 26 + .../contents/ui/delegate/DelegateAppIcon.qml | 18 + .../ui/delegate/DelegateFolderIcon.qml | 60 + .../ui/delegate/DelegateIconLoader.qml | 53 + .../contents/ui/delegate/DelegateLabel.qml | 22 + .../contents/ui/delegate/DelegateShadow.qml | 12 + .../folio/package/contents/ui/main.qml | 129 +-- .../contents/ui/private/ActionButton.qml | 138 --- .../contents/ui/private/ConfigOverlay.qml | 132 --- .../ui/private/DelegateRemoveButton.qml | 85 -- .../contents/ui/private/GradientBar.qml | 36 - .../contents/ui/private/OpenDrawerButton.qml | 70 -- .../contents/ui/private/ScrollIndicator.qml | 42 - .../ui/settings/SettingsComponent.qml | 95 ++ .../contents/ui/settings/SettingsWindow.qml | 208 ++++ .../homescreens/folio/pagelistmodel.cpp | 148 +++ .../homescreens/folio/pagelistmodel.h | 48 + containments/homescreens/folio/pagemodel.cpp | 291 +++++ containments/homescreens/folio/pagemodel.h | 89 ++ .../homescreens/folio/plugin/CMakeLists.txt | 13 +- .../folio/plugin/applicationlistmodel.cpp | 236 ---- .../folio/plugin/applicationlistmodel.h | 78 -- .../homescreens/folio/plugin/desktopmodel.cpp | 309 ----- .../homescreens/folio/plugin/desktopmodel.h | 60 - .../homescreens/folio/plugin/folioplugin.cpp | 17 - .../homescreens/folio/plugin/folioplugin.h | 16 - .../homescreens/folio/windowlistener.cpp | 62 + .../homescreens/folio/windowlistener.h | 34 + .../package/contents/ui/HomeScreen.qml | 2 +- .../contents/lockscreen/FlickContainer.qml | 2 +- .../org.kde.phone.homescreen.js | 9 - .../configuration/WallpaperSelector.qml | 5 +- shell/contents/defaults | 2 +- 91 files changed, 8247 insertions(+), 4052 deletions(-) create mode 100644 components/mobileshell/qml/widgets/krunner/KRunnerScreen.qml create mode 100644 containments/homescreens/folio/README.md create mode 100644 containments/homescreens/folio/applicationlistmodel.cpp create mode 100644 containments/homescreens/folio/applicationlistmodel.h create mode 100644 containments/homescreens/folio/delegatetoucharea.cpp create mode 100644 containments/homescreens/folio/delegatetoucharea.h create mode 100644 containments/homescreens/folio/dragstate.cpp create mode 100644 containments/homescreens/folio/dragstate.h create mode 100644 containments/homescreens/folio/favouritesmodel.cpp create mode 100644 containments/homescreens/folio/favouritesmodel.h create mode 100644 containments/homescreens/folio/folioapplication.cpp create mode 100644 containments/homescreens/folio/folioapplication.h create mode 100644 containments/homescreens/folio/folioapplicationfolder.cpp create mode 100644 containments/homescreens/folio/folioapplicationfolder.h create mode 100644 containments/homescreens/folio/foliodelegate.cpp create mode 100644 containments/homescreens/folio/foliodelegate.h create mode 100644 containments/homescreens/folio/foliosettings.cpp create mode 100644 containments/homescreens/folio/foliosettings.h create mode 100644 containments/homescreens/folio/homescreenstate.cpp create mode 100644 containments/homescreens/folio/homescreenstate.h delete mode 100644 containments/homescreens/folio/package/contents/config/main.xml create mode 100644 containments/homescreens/folio/package/contents/ui/AppDrawer.qml create mode 100644 containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml create mode 100644 containments/homescreens/folio/package/contents/ui/AppDrawerHeader.qml create mode 100644 containments/homescreens/folio/package/contents/ui/ContextMenuLoader.qml create mode 100644 containments/homescreens/folio/package/contents/ui/DelegateDragItem.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/FavoriteStrip.qml create mode 100644 containments/homescreens/folio/package/contents/ui/FavouritesBar.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/FlickContainer.qml create mode 100644 containments/homescreens/folio/package/contents/ui/FolderView.qml create mode 100644 containments/homescreens/folio/package/contents/ui/FolderViewTitle.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/HomeDelegate.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/HomeScreenContents.qml create mode 100644 containments/homescreens/folio/package/contents/ui/HomeScreenPage.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/HomeScreenState.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/LauncherContainer.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/LauncherDragManager.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/LauncherRepeater.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/MobileAppletContainer.qml create mode 100644 containments/homescreens/folio/package/contents/ui/PlaceholderDelegate.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/appdrawer/AbstractAppDrawer.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/appdrawer/AppDrawerHeader.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/appdrawer/AppDrawerLoader.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/appdrawer/DrawerGridDelegate.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/appdrawer/DrawerListDelegate.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/appdrawer/GridViewAppDrawer.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/appdrawer/ListViewAppDrawer.qml create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/AppDelegate.qml create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/AppFolderDelegate.qml create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/DelegateAppIcon.qml create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/DelegateFolderIcon.qml create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/DelegateIconLoader.qml create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/DelegateLabel.qml create mode 100644 containments/homescreens/folio/package/contents/ui/delegate/DelegateShadow.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/private/ActionButton.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/private/ConfigOverlay.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/private/DelegateRemoveButton.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/private/GradientBar.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/private/OpenDrawerButton.qml delete mode 100644 containments/homescreens/folio/package/contents/ui/private/ScrollIndicator.qml create mode 100644 containments/homescreens/folio/package/contents/ui/settings/SettingsComponent.qml create mode 100644 containments/homescreens/folio/package/contents/ui/settings/SettingsWindow.qml create mode 100644 containments/homescreens/folio/pagelistmodel.cpp create mode 100644 containments/homescreens/folio/pagelistmodel.h create mode 100644 containments/homescreens/folio/pagemodel.cpp create mode 100644 containments/homescreens/folio/pagemodel.h delete mode 100644 containments/homescreens/folio/plugin/applicationlistmodel.cpp delete mode 100644 containments/homescreens/folio/plugin/applicationlistmodel.h delete mode 100644 containments/homescreens/folio/plugin/desktopmodel.cpp delete mode 100644 containments/homescreens/folio/plugin/desktopmodel.h delete mode 100644 containments/homescreens/folio/plugin/folioplugin.cpp delete mode 100644 containments/homescreens/folio/plugin/folioplugin.h create mode 100644 containments/homescreens/folio/windowlistener.cpp create mode 100644 containments/homescreens/folio/windowlistener.h delete mode 100644 lookandfeel/contents/plasmoidsetupscripts/org.kde.phone.homescreen.js diff --git a/components/mobileshell/components/swipearea.cpp b/components/mobileshell/components/swipearea.cpp index 38ad16df..3a3f13e9 100644 --- a/components/mobileshell/components/swipearea.cpp +++ b/components/mobileshell/components/swipearea.cpp @@ -22,7 +22,7 @@ SwipeArea::SwipeArea(QQuickItem *parent) setFiltersChildMouseEvents(true); } -SwipeArea::Mode SwipeArea::mode() +SwipeArea::Mode SwipeArea::mode() const { return m_mode; } @@ -33,21 +33,32 @@ void SwipeArea::setMode(Mode mode) Q_EMIT modeChanged(); } -bool SwipeArea::interactive() +bool SwipeArea::interactive() const { return m_interactive; } -bool SwipeArea::moving() +void SwipeArea::setInteractive(bool interactive) +{ + m_interactive = interactive; + Q_EMIT interactiveChanged(); +} + +bool SwipeArea::moving() const { return m_moving; } -bool SwipeArea::pressed() +bool SwipeArea::pressed() const { return m_pressed; } +void SwipeArea::setSkipSwipeThreshold(bool value) +{ + m_skipSwipeThreshold = value; +} + bool SwipeArea::childMouseEventFilter(QQuickItem *item, QEvent *event) { if (!isVisible() || !isEnabled() || !m_interactive) { @@ -205,12 +216,6 @@ void SwipeArea::touchUngrabEvent() QQuickItem::touchUngrabEvent(); } -void SwipeArea::setInteractive(bool interactive) -{ - m_interactive = interactive; - Q_EMIT interactiveChanged(); -} - void SwipeArea::setMoving(bool moving) { m_moving = moving; @@ -225,6 +230,7 @@ void SwipeArea::setPressed(bool pressed) void SwipeArea::resetSwipe() { + m_skipSwipeThreshold = false; m_stealMouse = false; if (m_pressed) { setPressed(false); @@ -253,34 +259,38 @@ void SwipeArea::handleReleaseEvent(QPointerEvent *event, QPointF point) if (m_moving) { Q_EMIT swipeEnded(); } + resetSwipe(); } void SwipeArea::handleMoveEvent(QPointerEvent *event, QPointF point) { - const QVector2D totalDelta = QVector2D(point - m_startPos); - const QVector2D delta = QVector2D(point - m_lastPos); - - m_lastPos = point; - if (!m_stealMouse) { - // if we haven't reached the swipe registering threshold yet, don't start the swipe - if (m_mode == Mode::VerticalOnly && qAbs(point.y() - m_pressPos.y()) < SWIPE_REGISTER_THRESHOLD) { - return; - } else if (m_mode == Mode::HorizontalOnly && qAbs(point.x() - m_pressPos.x()) < SWIPE_REGISTER_THRESHOLD) { - return; - } else if (m_mode == Mode::BothAxis && qAbs(point.manhattanLength() - m_pressPos.manhattanLength()) < SWIPE_REGISTER_THRESHOLD) { - return; + if (!m_skipSwipeThreshold) { + // if we haven't reached the swipe registering threshold yet, don't start the swipe + if (m_mode == Mode::VerticalOnly && qAbs(point.y() - m_pressPos.y()) < SWIPE_REGISTER_THRESHOLD) { + return; + } else if (m_mode == Mode::HorizontalOnly && qAbs(point.x() - m_pressPos.x()) < SWIPE_REGISTER_THRESHOLD) { + return; + } else if (m_mode == Mode::BothAxis && qAbs(point.manhattanLength() - m_pressPos.manhattanLength()) < SWIPE_REGISTER_THRESHOLD) { + return; + } } + m_skipSwipeThreshold = false; // we now start the swipe, stealing it from children m_startPos = point; + m_lastPos = point; m_stealMouse = true; setMoving(true); Q_EMIT swipeStarted(m_startPos); } + const QVector2D totalDelta = QVector2D(point - m_startPos); + const QVector2D delta = QVector2D(point - m_lastPos); + m_lastPos = point; + // ensure it's called AFTER swipeStarted() Q_EMIT swipeMove(totalDelta.x(), totalDelta.y(), delta.x(), delta.y()); } diff --git a/components/mobileshell/components/swipearea.h b/components/mobileshell/components/swipearea.h index 1e89b7be..89097cf4 100644 --- a/components/mobileshell/components/swipearea.h +++ b/components/mobileshell/components/swipearea.h @@ -23,7 +23,7 @@ class SwipeArea : public QQuickItem { Q_OBJECT Q_PROPERTY(SwipeArea::Mode mode READ mode WRITE setMode NOTIFY modeChanged) - Q_PROPERTY(bool interactive READ interactive NOTIFY interactiveChanged) + Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) Q_PROPERTY(bool moving READ moving NOTIFY movingChanged) Q_PROPERTY(bool pressed READ pressed NOTIFY pressedChanged) @@ -35,12 +35,16 @@ public: enum Mode { BothAxis = 0, VerticalOnly, HorizontalOnly }; Q_ENUM(Mode) - Mode mode(); + Mode mode() const; void setMode(Mode mode); - bool interactive(); - bool moving(); - bool pressed(); + bool interactive() const; + void setInteractive(bool interactive); + + bool moving() const; + bool pressed() const; + + Q_INVOKABLE void setSkipSwipeThreshold(bool value); Q_SIGNALS: void modeChanged(); @@ -65,7 +69,6 @@ protected: void touchUngrabEvent() override; private: - void setInteractive(bool interactive); void setMoving(bool moving); void setPressed(bool pressed); @@ -95,6 +98,9 @@ private: // the previous point where interaction was at QPointF m_lastPos; + + // whether to skip trying to measure the swipe threshold + bool m_skipSwipeThreshold; }; QML_DECLARE_TYPE(SwipeArea) diff --git a/components/mobileshell/mobileshellplugin.cpp b/components/mobileshell/mobileshellplugin.cpp index c5bc8e92..c83d3673 100644 --- a/components/mobileshell/mobileshellplugin.cpp +++ b/components/mobileshell/mobileshellplugin.cpp @@ -81,6 +81,7 @@ void MobileShellPlugin::registerTypes(const char *uri) qmlRegisterSingletonType(resolvePath("volumeosd/VolumeOSDProviderLoader.qml"), uri, 1, 0, "VolumeOSDProviderLoader"); // /widgets + qmlRegisterType(resolvePath("widgets/krunner/KRunnerScreen.qml"), uri, 1, 0, "KRunnerScreen"); qmlRegisterType(resolvePath("widgets/krunner/KRunnerWidget.qml"), uri, 1, 0, "KRunnerWidget"); qmlRegisterType(resolvePath("widgets/mediacontrols/MediaControlsWidget.qml"), uri, 1, 0, "MediaControlsWidget"); qmlRegisterType(resolvePath("widgets/notifications/NotificationsWidget.qml"), uri, 1, 0, "NotificationsWidget"); diff --git a/components/mobileshell/qml/widgets/krunner/KRunnerScreen.qml b/components/mobileshell/qml/widgets/krunner/KRunnerScreen.qml new file mode 100644 index 00000000..39ba5bcf --- /dev/null +++ b/components/mobileshell/qml/widgets/krunner/KRunnerScreen.qml @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: 2014 Aaron Seigo + * SPDX-FileCopyrightText: 2015 Marco Martin + * SPDX-FileCopyrightText: 2021-2023 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Effects +import QtQuick.Controls as Controls +import QtQuick.Layouts + +import org.kde.plasma.core as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.milou as Milou +import org.kde.kirigami 2.19 as Kirigami + +import "../../components" as Components + +Item { + id: root + + function requestFocus() { + queryField.forceActiveFocus(); + } + + signal requestedClose() + + ColumnLayout { + id: column + anchors.fill: parent + + Controls.Control { + Layout.fillWidth: true + Layout.maximumWidth: Kirigami.Units.gridUnit * 30 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Kirigami.Units.gridUnit + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + + leftPadding: Kirigami.Units.smallSpacing + rightPadding: Kirigami.Units.smallSpacing + topPadding: Kirigami.Units.smallSpacing + bottomPadding: Kirigami.Units.smallSpacing + + background: Item { + + // shadow for search window + MultiEffect { + anchors.fill: parent + source: rectBackground + blurMax: 16 + shadowEnabled: true + shadowVerticalOffset: 1 + shadowOpacity: 0.15 + } + + Rectangle { + id: rectBackground + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.smallSpacing + } + } + + contentItem: RowLayout { + Item { + implicitHeight: queryField.height + implicitWidth: height + Kirigami.Icon { + anchors.fill: parent + anchors.margins: Math.round(Kirigami.Units.smallSpacing) + source: "start-here-symbolic" + } + } + PlasmaComponents.TextField { + id: queryField + Layout.fillWidth: true + placeholderText: i18n("Search…") + inputMethodHints: Qt.ImhNoPredictiveText // don't need to press "enter" to update text + } + } + } + + Controls.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: listView.contentHeight > availableHeight + + Milou.ResultsListView { + id: listView + queryString: queryField.text + clip: true + Kirigami.Theme.colorSet: Kirigami.Theme.Window + + highlight: activeFocus ? highlightComponent : null + Component { + id: highlightComponent + + PlasmaExtras.Highlight {} + } + + onActivated: { + root.requestedClose(); + } + onUpdateQueryString: { + queryField.text = text + queryField.cursorPosition = cursorPosition + } + + delegate: MouseArea { + id: delegate + height: rowLayout.height + width: listView.width + + onClicked: { + listView.currentIndex = model.index; + listView.runCurrentIndex(); + + root.requestedClose(); + } + hoverEnabled: true + + function activateNextAction() { + queryField.forceActiveFocus(); + queryField.selectAll(); + listView.currentIndex = -1; + } + + Rectangle { + anchors.fill: parent + color: delegate.pressed ? Qt.rgba(255, 255, 255, 0.2) : (delegate.containsMouse ? Qt.rgba(255, 255, 255, 0.05) : "transparent") + Behavior on color { + ColorAnimation { duration: Kirigami.Units.shortDuration } + } + } + + RowLayout { + id: rowLayout + height: Kirigami.Units.gridUnit * 3 + anchors { + top: parent.top + left: parent.left + right: parent.right + leftMargin: Kirigami.Units.gridUnit + rightMargin: Kirigami.Units.gridUnit + } + + Kirigami.Icon { + Layout.alignment: Qt.AlignVCenter + source: model.decoration + implicitWidth: Kirigami.Units.iconSizes.medium + implicitHeight: Kirigami.Units.iconSizes.medium + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Kirigami.Units.smallSpacing + + PlasmaComponents.Label { + id: title + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.smallSpacing * 2 + Layout.rightMargin: Kirigami.Units.gridUnit + + maximumLineCount: 1 + elide: Text.ElideRight + text: typeof modelData !== "undefined" ? modelData : model.display + color: "white" + + font.pointSize: Kirigami.Theme.defaultFont.pointSize + } + PlasmaComponents.Label { + id: subtitle + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.smallSpacing * 2 + Layout.rightMargin: Kirigami.Units.gridUnit + + maximumLineCount: 1 + elide: Text.ElideRight + text: model.subtext || "" + color: "white" + opacity: 0.8 + + font.pointSize: Math.round(Kirigami.Theme.defaultFont.pointSize * 0.8) + } + } + + Repeater { + id: actionsRepeater + model: typeof actions !== "undefined" ? actions : [] + + Controls.ToolButton { + icon: modelData.icon || "" + visible: modelData.visible || true + enabled: modelData.enabled || true + + Accessible.role: Accessible.Button + Accessible.name: modelData.text + checkable: checked + checked: delegate.activeAction === index + focus: delegate.activeAction === index + onClicked: delegate.ListView.view.runAction(index) + } + } + } + } + } + } + + MouseArea { + Layout.fillWidth: true + Layout.fillHeight: true + + onClicked: root.requestedClose() + } + } +} diff --git a/components/mobileshell/qml/widgets/krunner/KRunnerWidget.qml b/components/mobileshell/qml/widgets/krunner/KRunnerWidget.qml index 794a76ce..43cafcd7 100644 --- a/components/mobileshell/qml/widgets/krunner/KRunnerWidget.qml +++ b/components/mobileshell/qml/widgets/krunner/KRunnerWidget.qml @@ -202,7 +202,7 @@ Item { Kirigami.Theme.colorSet: Kirigami.Theme.Window highlight: activeFocus ? highlightComponent : null - Component{ + Component { id: highlightComponent PlasmaExtras.Highlight {} diff --git a/components/mobileshell/resources.qrc b/components/mobileshell/resources.qrc index 369a58a2..40913611 100644 --- a/components/mobileshell/resources.qrc +++ b/components/mobileshell/resources.qrc @@ -67,6 +67,7 @@ qml/statusbar/StatusBar.qml qml/statusbar/TaskWidget.qml + qml/widgets/krunner/KRunnerScreen.qml qml/widgets/krunner/KRunnerWidget.qml qml/widgets/mediacontrols/BlurredBackground.qml diff --git a/containments/homescreens/folio/CMakeLists.txt b/containments/homescreens/folio/CMakeLists.txt index 2abda804..8bc66bc2 100644 --- a/containments/homescreens/folio/CMakeLists.txt +++ b/containments/homescreens/folio/CMakeLists.txt @@ -1,8 +1,20 @@ -# SPDX-FileCopyrightText: 2015-2021 Marco Martin +# SPDX-FileCopyrightText: 2023 Devin Lin # SPDX-License-Identifier: GPL-2.0-or-later set(homescreen_SRCS homescreen.cpp + applicationlistmodel.cpp + homescreenstate.cpp + windowlistener.cpp + favouritesmodel.cpp + folioapplication.cpp + folioapplicationfolder.cpp + foliodelegate.cpp + foliosettings.cpp + pagemodel.cpp + pagelistmodel.cpp + delegatetoucharea.cpp + dragstate.cpp ) add_library(org.kde.plasma.mobile.homescreen.folio MODULE ${homescreen_SRCS}) @@ -19,11 +31,9 @@ target_link_libraries(org.kde.plasma.mobile.homescreen.folio KF6::Notifications KF6::WaylandClient KF6::WindowSystem + KF6::JobWidgets ) - install(TARGETS org.kde.plasma.mobile.homescreen.folio DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/applets) plasma_install_package(package org.kde.plasma.mobile.homescreen.folio) - -add_subdirectory(plugin) diff --git a/containments/homescreens/folio/README.md b/containments/homescreens/folio/README.md new file mode 100644 index 00000000..fc9684fd --- /dev/null +++ b/containments/homescreens/folio/README.md @@ -0,0 +1,26 @@ + + +# Folio Homescreen + +This is the paged homescreen for Plasma Mobile. + +### How it works + +Most of the homescreen is in C++ in order to keep logic together, with QML only responsible for the display and user input. + +As such, all of the positioning and placement of delegates on the screen are top down from the model, as well as drag and drop behaviour. + +#### TODO +- Add folio/halcyon switcher in initial-start +- If an app gets uninstalled, the homescreen UI needs to ensure that delegates are updated +- BUG: the position of where things think the dragged icon is during drag-and-drop is slightly off because of the label +- BUG: landscape favourites bar duplication when dragging icon from it sometimes +- BUG: can't insert delegates in-between very well in landscape favourites bar +- can make the touch area only the icon? +- FEATURE: add import/export +- FEATURE: keyboard navigation +- FEATURE: touchpad navigation +- BUG: it's possible to get stuck in an unswipeable state after swiping down from the app drawer diff --git a/containments/homescreens/folio/applicationlistmodel.cpp b/containments/homescreens/folio/applicationlistmodel.cpp new file mode 100644 index 00000000..ab91b8e2 --- /dev/null +++ b/containments/homescreens/folio/applicationlistmodel.cpp @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "applicationlistmodel.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +ApplicationListModel::ApplicationListModel(QObject *parent) + : QAbstractListModel(parent) +{ + connect(KSycoca::self(), &KSycoca::databaseChanged, this, &ApplicationListModel::sycocaDbChanged); + + // initialize wayland window checking + KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(this); + if (!connection) { + return; + } + + load(); +} + +ApplicationListModel::~ApplicationListModel() = default; + +ApplicationListModel *ApplicationListModel::self() +{ + static ApplicationListModel *inst = new ApplicationListModel(nullptr); + return inst; +} + +QHash ApplicationListModel::roleNames() const +{ + return {{DelegateRole, QByteArrayLiteral("delegate")}}; +} + +void ApplicationListModel::sycocaDbChanged() +{ + load(); +} + +void ApplicationListModel::load() +{ + auto cfg = KSharedConfig::openConfig(QStringLiteral("applications-blacklistrc")); + auto blgroup = KConfigGroup(cfg, QStringLiteral("Applications")); + + const QStringList blacklist = blgroup.readEntry("blacklist", QStringList()); + + beginResetModel(); + + m_delegates.clear(); + + QList unorderedList; + + auto filter = [blacklist](const KService::Ptr &service) -> bool { + if (service->noDisplay()) { + return false; + } + + if (!service->showOnCurrentPlatform()) { + return false; + } + + if (blacklist.contains(service->desktopEntryName())) { + return false; + } + + return true; + }; + + const KService::List apps = KApplicationTrader::query(filter); + + for (const KService::Ptr &service : apps) { + FolioApplication *app = new FolioApplication{this, service}; + FolioDelegate *delegate = new FolioDelegate{app, this}; + unorderedList << delegate; + } + + std::sort(unorderedList.begin(), unorderedList.end(), [](FolioDelegate *a1, FolioDelegate *a2) { + return a1->application()->name().compare(a2->application()->name(), Qt::CaseInsensitive) < 0; + }); + + m_delegates << unorderedList; + + endResetModel(); +} + +QVariant ApplicationListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: + case DelegateRole: + return QVariant::fromValue(m_delegates.at(index.row())); + default: + return QVariant(); + } +} + +int ApplicationListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_delegates.count(); +} diff --git a/containments/homescreens/folio/applicationlistmodel.h b/containments/homescreens/folio/applicationlistmodel.h new file mode 100644 index 00000000..609c3bd6 --- /dev/null +++ b/containments/homescreens/folio/applicationlistmodel.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "foliodelegate.h" + +/** + * @short The base application list, used directly by the app drawer. + */ +class ApplicationListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + DelegateRole = Qt::UserRole + 1, + }; + + ApplicationListModel(QObject *parent = nullptr); + ~ApplicationListModel() override; + static ApplicationListModel *self(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QHash roleNames() const Q_DECL_OVERRIDE; + + Q_INVOKABLE void load(); + +public Q_SLOTS: + void sycocaDbChanged(); + +Q_SIGNALS: + void launchError(const QString &msg); + +protected: + QList m_delegates; +}; diff --git a/containments/homescreens/folio/delegatetoucharea.cpp b/containments/homescreens/folio/delegatetoucharea.cpp new file mode 100644 index 00000000..02399830 --- /dev/null +++ b/containments/homescreens/folio/delegatetoucharea.cpp @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: 2016 The Qt Company Ltd. +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "delegatetoucharea.h" + +#include + +// Some code taken from MouseArea + +DelegateTouchArea::DelegateTouchArea(QQuickItem *parent) + : QQuickItem{parent} + , m_pressAndHoldTimer{new QTimer{this}} +{ + // TODO: currently hardcoded 2s press and hold interval + m_pressAndHoldTimer->setInterval(600); + m_pressAndHoldTimer->setSingleShot(true); + connect(m_pressAndHoldTimer, &QTimer::timeout, this, &DelegateTouchArea::startPressAndHold); + + // Explcitly call setCursor on QQuickItem since + // it internally keeps a boolean hasCursor that doesn't + // get set to true unless you call setCursor + setCursor(Qt::ArrowCursor); + + setAcceptHoverEvents(true); + setAcceptTouchEvents(true); + // setFiltersChildMouseEvents(true); + setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton); +} + +bool DelegateTouchArea::pressed() +{ + return m_pressed; +} + +void DelegateTouchArea::setPressed(bool pressed) +{ + if (pressed != m_pressed) { + m_pressed = pressed; + Q_EMIT pressedChanged(pressed); + } +} + +bool DelegateTouchArea::hovered() +{ + return m_hovered; +} + +void DelegateTouchArea::setHovered(bool hovered) +{ + if (hovered != m_hovered) { + m_hovered = hovered; + Q_EMIT hoveredChanged(hovered); + } +} + +bool DelegateTouchArea::dragging() +{ + return m_dragging; +} + +void DelegateTouchArea::setDragging(bool dragging) +{ + if (dragging != m_dragging) { + m_dragging = dragging; + Q_EMIT draggingChanged(dragging); + } +} + +Qt::CursorShape DelegateTouchArea::cursorShape() +{ + return cursor().shape(); +} + +void DelegateTouchArea::setCursorShape(Qt::CursorShape cursorShape) +{ + if (cursor().shape() == cursorShape) { + return; + } + + setCursor(cursorShape); + Q_EMIT cursorShapeChanged(); +} + +void DelegateTouchArea::unsetCursor() +{ + setCursorShape(Qt::ArrowCursor); +} + +void DelegateTouchArea::mousePressEvent(QMouseEvent *event) +{ + if (event->button() & Qt::RightButton) { + Q_EMIT rightMousePress(); + } else if (event->button() & Qt::LeftButton) { + handlePressEvent(event, event->points().first().position()); + event->accept(); + } else { + QQuickItem::mousePressEvent(event); + } +} + +void DelegateTouchArea::mouseMoveEvent(QMouseEvent *event) +{ + handleMoveEvent(event, event->points().first().position()); + event->accept(); +} + +void DelegateTouchArea::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() & Qt::LeftButton) { + handleReleaseEvent(event, true); + event->accept(); + } else { + QQuickItem::mouseReleaseEvent(event); + } +} + +void DelegateTouchArea::mouseUngrabEvent() +{ + if (m_pressed) { + handleReleaseEvent(nullptr, false); + } + QQuickItem::mouseUngrabEvent(); +} + +void DelegateTouchArea::touchEvent(QTouchEvent *event) +{ + bool unhandled = true; + const auto &firstPoint = event->points().first(); + + switch (firstPoint.state()) { + case QEventPoint::State::Pressed: + handlePressEvent(event, firstPoint.position()); + event->accept(); + unhandled = false; + break; + case QEventPoint::State::Updated: + handleMoveEvent(event, firstPoint.position()); + event->accept(); + unhandled = false; + break; + case QEventPoint::State::Released: + handleReleaseEvent(event, true); + event->accept(); + unhandled = false; + break; + case QEventPoint::State::Stationary: + case QEventPoint::State::Unknown: + break; + } + + if (unhandled) { + QQuickItem::touchEvent(event); + } +} + +void DelegateTouchArea::touchUngrabEvent() +{ + if (m_pressed) { + handleReleaseEvent(nullptr, false); + } + QQuickItem::touchUngrabEvent(); +} + +void DelegateTouchArea::hoverEnterEvent(QHoverEvent *event) +{ + setHovered(true); + + // don't block hover events + event->ignore(); +} + +void DelegateTouchArea::hoverLeaveEvent(QHoverEvent *event) +{ + setHovered(false); + + // don't block hover events + event->ignore(); +} + +// bool DelegateTouchArea::childMouseEventFilter(QQuickItem *item, QEvent *event) +// { +// if (!isVisible() || !isEnabled()) { +// handleReleaseEvent(nullptr, false); +// return QQuickItem::childMouseEventFilter(item, event); +// } +// +// if (event->isPointerEvent() && event->type() != QEvent::UngrabMouse) { +// return filterPointerEvent(item, static_cast(event)); +// } +// +// return QQuickItem::childMouseEventFilter(item, event); +// } +// +// // take exclusive grab from children +// bool DelegateTouchArea::filterPointerEvent(QQuickItem *receiver, QPointerEvent *event) +// { +// // only filter mouse, touch or tablet events +// if (!dynamic_cast(event) && !dynamic_cast(event) && !dynamic_cast(event)) { +// return false; +// } +// +// const auto &firstPoint = event->points().first(); +// +// if (event->pointCount() == 1 && event->exclusiveGrabber(firstPoint) == this) { +// // We have an exclusive grab (since we're e.g dragging), but at the same time, we have +// // a child with a passive grab (which is why this filter is being called). And because +// // of that, we end up getting the same pointer events twice; First in our own event +// // handlers (because of the grab), then once more in here, since we filter the child. +// // To avoid processing the event twice (e.g avoid calling handleReleaseEvent once more +// // from below), we mark the event as filtered, and simply return. +// event->setAccepted(true); +// return true; +// } +// +// QPointF localPos = mapFromScene(firstPoint.scenePosition()); +// bool receiverDisabled = receiver && !receiver->isEnabled(); +// bool receiverKeepsGrab = receiver && (receiver->keepMouseGrab() || receiver->keepTouchGrab()); +// +// if ((m_pressAndHeld || contains(localPos)) && (!receiver || !receiverKeepsGrab || receiverDisabled)) { +// // clone the event, and set the first point's local position +// // HACK: we can't change QPointerEvent's points since it's const, so we have to pass localPos into the handlers +// QPointerEvent *localizedEvent = event->clone(); +// localizedEvent->setAccepted(false); +// +// switch (firstPoint.state()) { +// case QEventPoint::State::Updated: +// handleMoveEvent(localizedEvent, localPos); +// break; +// case QEventPoint::State::Pressed: +// handlePressEvent(localizedEvent, localPos); +// break; +// case QEventPoint::State::Released: +// handleReleaseEvent(localizedEvent, true); +// break; +// case QEventPoint::State::Stationary: +// case QEventPoint::State::Unknown: +// break; +// } +// +// if ((receiver && m_pressAndHeld && !receiverKeepsGrab && receiver != this) || receiverDisabled) { +// event->setExclusiveGrabber(firstPoint, this); +// } +// +// bool filtered = m_pressAndHeld || receiverDisabled; +// if (filtered) { +// event->setAccepted(true); +// } +// +// return filtered; +// } +// +// if (firstPoint.state() == QEventPoint::State::Released || (receiverKeepsGrab && !receiverDisabled)) { +// // mouse released, or another item has claimed the grab +// handleReleaseEvent(nullptr, false); +// } +// +// return false; +// } + +void DelegateTouchArea::handlePressEvent(QPointerEvent *event, QPointF point) +{ + // ignore multiple press events + if (m_pressed) { + return; + } + + setPressed(true); + m_pressAndHoldTimer->start(); +} + +void DelegateTouchArea::handleReleaseEvent(QPointerEvent *event, bool click) +{ + // NOTE: event can be nullptr! + + setPressed(false); + setDragging(false); + + if (!m_pressAndHeld && click) { + Q_EMIT clicked(); + } + + if (m_pressAndHeld) { + Q_EMIT pressAndHoldReleased(); + } + + m_pressAndHoldTimer->stop(); + m_pressAndHeld = false; +} + +void DelegateTouchArea::handleMoveEvent(QPointerEvent *event, QPointF point) +{ + if (m_pressAndHeld) { + // TODO + } +} + +void DelegateTouchArea::startPressAndHold() +{ + m_pressAndHeld = true; + Q_EMIT pressAndHold(); +} diff --git a/containments/homescreens/folio/delegatetoucharea.h b/containments/homescreens/folio/delegatetoucharea.h new file mode 100644 index 00000000..c609b7a8 --- /dev/null +++ b/containments/homescreens/folio/delegatetoucharea.h @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +/** + * @short A component that is similar to MouseArea but allows for a + * simpler tracking of dragging movements after pressing and holding. + * + * @author Devin Lin + */ +class DelegateTouchArea : public QQuickItem +{ + Q_OBJECT + + Q_PROPERTY(bool pressed READ pressed NOTIFY pressedChanged FINAL) + Q_PROPERTY(bool hovered READ hovered NOTIFY hoveredChanged FINAL) + Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged FINAL) + Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape RESET unsetCursor NOTIFY cursorShapeChanged FINAL) + + QML_NAMED_ELEMENT(DelegateTouchArea) + +public: + DelegateTouchArea(QQuickItem *parent = nullptr); + + bool pressed(); + bool hovered(); + bool dragging(); + Qt::CursorShape cursorShape(); + void setCursorShape(Qt::CursorShape cursorShape); + void unsetCursor(); + +Q_SIGNALS: + void clicked(); + void rightMousePress(); + void pressAndHold(); + void pressAndHoldReleased(); + void drag(qreal x, qreal y); + void pressedChanged(bool pressed); + void hoveredChanged(bool hovered); + void draggingChanged(bool dragging); + void cursorShapeChanged(); + +protected: + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + void touchEvent(QTouchEvent *event) override; + void touchUngrabEvent() override; + void hoverEnterEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *event) override; + // bool childMouseEventFilter(QQuickItem *i, QEvent *e) override; + +private Q_SLOTS: + void startPressAndHold(); + +private: + // bool filterPointerEvent(QQuickItem *receiver, QPointerEvent *event); + void setPressed(bool pressed); + void setHovered(bool hovered); + void setDragging(bool dragging); + + void handlePressEvent(QPointerEvent *event, QPointF point); + void handleReleaseEvent(QPointerEvent *event, bool click); + void handleMoveEvent(QPointerEvent *event, QPointF point); + + bool m_pressed{false}; + bool m_hovered{false}; + bool m_dragging{false}; + bool m_pressAndHeld{false}; + Qt::CursorShape m_cursorShape{Qt::ArrowCursor}; + + QTimer *m_pressAndHoldTimer{nullptr}; +}; + +QML_DECLARE_TYPE(DelegateTouchArea) diff --git a/containments/homescreens/folio/dragstate.cpp b/containments/homescreens/folio/dragstate.cpp new file mode 100644 index 00000000..8e8cfaac --- /dev/null +++ b/containments/homescreens/folio/dragstate.cpp @@ -0,0 +1,883 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "dragstate.h" +#include "favouritesmodel.h" +#include "pagelistmodel.h" + +#include +#include + +// TODO don't hardcode, use page widths +const int PAGE_CHANGE_THRESHOLD = 30; + +const QString DEFAULT_FOLDER_NAME = i18n("Folder"); + +DelegateDragPosition::DelegateDragPosition(QObject *parent) + : QObject{parent} +{ +} + +DelegateDragPosition::~DelegateDragPosition() = default; + +void DelegateDragPosition::copyFrom(DelegateDragPosition *position) +{ + setPage(position->page()); + setPageRow(position->pageRow()); + setPageColumn(position->pageColumn()); + setFavouritesPosition(position->favouritesPosition()); + setFolderPosition(position->folderPosition()); + setFolder(position->folder()); + setLocation(position->location()); +} + +DelegateDragPosition::Location DelegateDragPosition::location() const +{ + return m_location; +} + +void DelegateDragPosition::setLocation(Location location) +{ + if (m_location != location) { + m_location = location; + Q_EMIT locationChanged(); + } +} + +int DelegateDragPosition::page() const +{ + return m_page; +} + +void DelegateDragPosition::setPage(int page) +{ + if (m_page != page) { + m_page = page; + Q_EMIT pageChanged(); + } +} + +int DelegateDragPosition::pageRow() const +{ + return m_pageRow; +} + +void DelegateDragPosition::setPageRow(int pageRow) +{ + if (m_pageRow != pageRow) { + m_pageRow = pageRow; + Q_EMIT pageRowChanged(); + } +} + +int DelegateDragPosition::pageColumn() const +{ + return m_pageColumn; +} + +void DelegateDragPosition::setPageColumn(int pageColumn) +{ + if (m_pageColumn != pageColumn) { + m_pageColumn = pageColumn; + Q_EMIT pageColumnChanged(); + } +} + +int DelegateDragPosition::favouritesPosition() const +{ + return m_favouritesPosition; +} + +void DelegateDragPosition::setFavouritesPosition(int favouritesPosition) +{ + if (m_favouritesPosition != favouritesPosition) { + m_favouritesPosition = favouritesPosition; + Q_EMIT favouritesPositionChanged(); + } +} + +int DelegateDragPosition::folderPosition() const +{ + return m_folderPosition; +} + +void DelegateDragPosition::setFolderPosition(int folderPosition) +{ + if (m_folderPosition != folderPosition) { + m_folderPosition = folderPosition; + Q_EMIT folderPositionChanged(); + } +} + +FolioApplicationFolder *DelegateDragPosition::folder() const +{ + return m_folder; +} + +void DelegateDragPosition::setFolder(FolioApplicationFolder *folder) +{ + if (m_folder != folder) { + m_folder = folder; + Q_EMIT folderChanged(); + } +} + +DragState::DragState(HomeScreenState *state, QObject *parent) + : QObject{parent} + , m_changePageTimer{new QTimer{this}} + , m_openFolderTimer{new QTimer{this}} + , m_leaveFolderTimer{new QTimer{this}} + , m_changeFolderPageTimer{new QTimer{this}} + , m_folderInsertBetweenTimer{new QTimer{this}} + , m_favouritesInsertBetweenTimer{new QTimer{this}} + , m_candidateDropPosition{new DelegateDragPosition{this}} + , m_startPosition{new DelegateDragPosition{this}} + , m_state{state} +{ + if (!state) { + return; + } + + // 500 ms hold before page timer changes + m_changePageTimer->setInterval(500); + m_changePageTimer->setSingleShot(true); + + m_openFolderTimer->setInterval(1000); + m_openFolderTimer->setSingleShot(true); + + m_leaveFolderTimer->setInterval(500); + m_leaveFolderTimer->setSingleShot(true); + + m_changeFolderPageTimer->setInterval(500); + m_changeFolderPageTimer->setSingleShot(true); + + m_folderInsertBetweenTimer->setInterval(250); + m_folderInsertBetweenTimer->setSingleShot(true); + + m_favouritesInsertBetweenTimer->setInterval(250); + m_favouritesInsertBetweenTimer->setSingleShot(true); + + connect(m_changePageTimer, &QTimer::timeout, this, &DragState::onChangePageTimerFinished); + connect(m_openFolderTimer, &QTimer::timeout, this, &DragState::onOpenFolderTimerFinished); + connect(m_leaveFolderTimer, &QTimer::timeout, this, &DragState::onLeaveFolderTimerFinished); + connect(m_changeFolderPageTimer, &QTimer::timeout, this, &DragState::onChangeFolderPageTimerFinished); + connect(m_folderInsertBetweenTimer, &QTimer::timeout, this, &DragState::onFolderInsertBetweenTimerFinished); + connect(m_favouritesInsertBetweenTimer, &QTimer::timeout, this, &DragState::onFavouritesInsertBetweenTimerFinished); + + connect(m_state, &HomeScreenState::delegateDragFromPageStarted, this, &DragState::onDelegateDragFromPageStarted); + connect(m_state, &HomeScreenState::delegateDragFromAppDrawerStarted, this, &DragState::onDelegateDragFromAppDrawerStarted); + connect(m_state, &HomeScreenState::delegateDragFromFavouritesStarted, this, &DragState::onDelegateDragFromFavouritesStarted); + connect(m_state, &HomeScreenState::delegateDragFromFolderStarted, this, &DragState::onDelegateDragFromFolderStarted); + connect(m_state, &HomeScreenState::swipeStateChanged, this, [this]() { + if (HomeScreenState::self()->swipeState() == HomeScreenState::DraggingDelegate) { + onDelegateDraggingStarted(); + } + }); + connect(m_state, &HomeScreenState::delegateDragEnded, this, &DragState::onDelegateDropped); + + connect(m_state, &HomeScreenState::pageNumChanged, this, [this]() { + m_candidateDropPosition->setPageRow(m_state->currentPage()); + }); + + connect(m_state, &HomeScreenState::delegateDragXChanged, this, &DragState::onDelegateDragPositionChanged); + connect(m_state, &HomeScreenState::delegateDragYChanged, this, &DragState::onDelegateDragPositionChanged); + + connect(m_state, &HomeScreenState::leftCurrentFolder, this, &DragState::onLeaveCurrentFolder); +} + +DelegateDragPosition *DragState::candidateDropPosition() const +{ + return m_candidateDropPosition; +} + +DelegateDragPosition *DragState::startPosition() const +{ + return m_startPosition; +} + +FolioDelegate *DragState::dropDelegate() const +{ + return m_dropDelegate; +} + +void DragState::setDropDelegate(FolioDelegate *dropDelegate) +{ + m_dropDelegate = dropDelegate; + Q_EMIT dropDelegateChanged(); +} + +void DragState::onDelegateDragPositionChanged() +{ + if (!m_state) { + return; + } + + // we want to update the candidate drop position variable in this function! + + qreal x = getDraggedDelegateX(); + qreal y = getDraggedDelegateY(); + + bool inFolder = m_state->viewState() == HomeScreenState::FolderView; + bool inFavouritesArea = !inFolder; + + // the favourites bar can be in different locations, so account for each + switch (m_state->favouritesBarLocation()) { + case HomeScreenState::Bottom: + inFavouritesArea = inFavouritesArea && y > m_state->pageHeight(); + break; + case HomeScreenState::Left: + inFavouritesArea = inFavouritesArea && x < m_state->viewWidth() - m_state->pageHeight(); + break; + case HomeScreenState::Right: + inFavouritesArea = inFavouritesArea && x > m_state->pageWidth(); + break; + } + + // stop the favourites insertion timer if the delegate has moved out + if (!inFavouritesArea) { + m_favouritesInsertBetweenTimer->stop(); + } + + if (inFavouritesArea || inFolder) { + m_openFolderTimer->stop(); + } + + if (m_state->viewState() == HomeScreenState::FolderView) { + // if we are in a folder + onDelegateDragPositionOverFolderViewChanged(); + + } else if (inFavouritesArea) { + // we are in the favourites bar area + onDelegateDragPositionOverFavouritesChanged(); + } else { + // we are in the homescreen pages area + onDelegateDragPositionOverPageViewChanged(); + } +} + +void DragState::onDelegateDragPositionOverFolderViewChanged() +{ + // if the drag position changes while in the folder view + qreal x = getDraggedDelegateX(); + qreal y = getDraggedDelegateY(); + + auto *folder = m_state->currentFolder(); + if (!folder) { + return; + } + + // if the drop position is not in the folder, but outside (going to page view) + if (folder->isDropPositionOutside(x, y)) { + if (!m_leaveFolderTimer->isActive()) { + m_leaveFolderTimer->start(); + } + return; + } else if (m_leaveFolderTimer->isActive()) { + // cancel timer if we are back in the folder + m_leaveFolderTimer->stop(); + } + + // the potential folder index that can be dropped at + int dropIndex = folder->dropInsertPosition(m_state->currentFolderPage(), x, y); + + // if the delegate has moved to another position, cancel the insert timer + if (dropIndex != m_folderInsertBetweenIndex) { + m_folderInsertBetweenTimer->stop(); + } + + // start the insertion timer (so that the user has time to move the delegate away) + if (!m_folderInsertBetweenTimer->isActive()) { + m_folderInsertBetweenTimer->start(); + m_folderInsertBetweenIndex = dropIndex; + } + + const qreal leftPagePosition = folder->applications()->leftMarginFromScreenEdge(); + const qreal rightPagePosition = m_state->viewWidth() - leftPagePosition; + + // determine if the delegate is near the edge of a page (to switch pages). + // -> start the change page timer if we at the page edge. + if (x <= leftPagePosition + PAGE_CHANGE_THRESHOLD || x >= rightPagePosition - PAGE_CHANGE_THRESHOLD) { + if (!m_changeFolderPageTimer->isActive()) { + m_changeFolderPageTimer->start(); + } + } else { + if (m_changeFolderPageTimer->isActive()) { + m_changeFolderPageTimer->stop(); + } + } +} + +void DragState::onDelegateDragPositionOverFavouritesChanged() +{ + // the drag position changed while over the favourites strip + + qreal x = getDraggedDelegateX(); + qreal y = getDraggedDelegateY(); + int dropIndex = FavouritesModel::self()->dropInsertPosition(x, y); + + // if the drop position changed, cancel the open folder timer + if (m_candidateDropPosition->location() != DelegateDragPosition::Favourites || m_candidateDropPosition->favouritesPosition() != dropIndex) { + if (m_openFolderTimer->isActive()) { + m_openFolderTimer->stop(); + } + } + + // if the delegate has moved to another position, cancel the insert timer + if (dropIndex != m_favouritesInsertBetweenIndex) { + m_favouritesInsertBetweenTimer->stop(); + } + + if (FavouritesModel::self()->dropPositionIsEdge(x, y)) { + // if we need to make space for the delegate + + // start the insertion timer (so that the user has time to move the delegate away) + if (!m_favouritesInsertBetweenTimer->isActive()) { + m_favouritesInsertBetweenTimer->start(); + m_favouritesInsertBetweenIndex = dropIndex; + } + } else { + // if we are hovering over the center of a folder or app + + // delete ghost entry if there is one + int ghostEntryPosition = FavouritesModel::self()->getGhostEntryPosition(); + if (ghostEntryPosition != -1 && ghostEntryPosition != dropIndex) { + if (dropIndex > ghostEntryPosition) { + // correct index if deleting the ghost will change the index + dropIndex--; + } + FavouritesModel::self()->deleteGhostEntry(); + } + + // update the current drop position + m_candidateDropPosition->setFavouritesPosition(dropIndex); + m_candidateDropPosition->setLocation(DelegateDragPosition::Favourites); + + // start folder open timer if hovering over a folder + // get delegate being hovered over + FolioDelegate *delegate = FavouritesModel::self()->getEntryAt(dropIndex); + + // check delegate is a folder and the drop delegate is an app + if (delegate && delegate->type() == FolioDelegate::Folder && m_dropDelegate && m_dropDelegate->type() == FolioDelegate::Application) { + if (!m_openFolderTimer->isActive()) { + m_openFolderTimer->start(); + } + } + } +} + +void DragState::onDelegateDragPositionOverPageViewChanged() +{ + // the drag position changed while over the homescreen pages strip + + qreal x = getDraggedDelegateX(); + qreal y = getDraggedDelegateY(); + int page = m_state->currentPage(); + + // calculate the row and column the delegate is over + qreal pageHorizontalMargin = (m_state->pageWidth() - m_state->pageContentWidth()) / 2; + qreal pageVerticalMargin = (m_state->pageHeight() - m_state->pageContentHeight()) / 2; + + int row = (y - pageVerticalMargin) / m_state->pageCellHeight(); + int column = (x - pageHorizontalMargin) / m_state->pageCellWidth(); + + // ensure it's in bounds + row = std::max(0, std::min(m_state->pageRows() - 1, row)); + column = std::max(0, std::min(m_state->pageColumns() - 1, column)); + + // if the drop position changed, cancel the open folder timer + if (m_candidateDropPosition->location() != DelegateDragPosition::Pages || m_candidateDropPosition->pageRow() != row + || m_candidateDropPosition->pageColumn() != column) { + if (m_openFolderTimer->isActive()) { + m_openFolderTimer->stop(); + } + } + + // update the current drop position + m_candidateDropPosition->setPage(page); + m_candidateDropPosition->setPageRow(row); + m_candidateDropPosition->setPageColumn(column); + m_candidateDropPosition->setLocation(DelegateDragPosition::Pages); + + // start folder open timer if hovering over a folder + PageModel *pageModel = PageListModel::self()->getPage(page); + if (pageModel) { + // get delegate being hovered over + FolioDelegate *delegate = pageModel->getDelegate(row, column); + + // check delegate is a folder and the drop delegate is an app + if (delegate && delegate->type() == FolioDelegate::Folder && m_dropDelegate && m_dropDelegate->type() == FolioDelegate::Application) { + if (!m_openFolderTimer->isActive()) { + m_openFolderTimer->start(); + } + } + } + + const int leftPagePosition = 0; + const int rightPagePosition = m_state->pageWidth(); + + // determine if the delegate is near the edge of a page (to switch pages). + // -> start the change page timer if we at the page edge. + if (qAbs(leftPagePosition - x) <= PAGE_CHANGE_THRESHOLD || qAbs(rightPagePosition - x) <= PAGE_CHANGE_THRESHOLD) { + if (!m_changePageTimer->isActive()) { + m_changePageTimer->start(); + } + } else { + if (m_changePageTimer->isActive()) { + m_changePageTimer->stop(); + } + } +} + +void DragState::onDelegateDraggingStarted() +{ + // remove the delegate from the model + // NOTE: we only delete here (and not from the event trigger, ex. onDelegateDragFromPageStarted) + // because the actual dragging only started when this is called + deleteStartPositionDelegate(); +} + +void DragState::onDelegateDragFromPageStarted(int page, int row, int column) +{ + // fetch delegate at start position + PageModel *pageModel = PageListModel::self()->getPage(page); + if (pageModel) { + setDropDelegate(pageModel->getDelegate(row, column)); + } else { + setDropDelegate(nullptr); + } + + // set start location + m_startPosition->setPage(page); + m_startPosition->setPageRow(row); + m_startPosition->setPageColumn(column); + m_startPosition->setLocation(DelegateDragPosition::Pages); +} + +void DragState::onDelegateDragFromFavouritesStarted(int position) +{ + // fetch delegate at start position + setDropDelegate(FavouritesModel::self()->getEntryAt(position)); + + // set start location + m_startPosition->setFavouritesPosition(position); + m_startPosition->setLocation(DelegateDragPosition::Favourites); +} + +void DragState::onDelegateDragFromAppDrawerStarted(QString storageId) +{ + // fetch delegate at start position + if (KService::Ptr service = KService::serviceByStorageId(storageId)) { + FolioApplication *app = new FolioApplication{this, service}; + setDropDelegate(new FolioDelegate{app, this}); + } else { + setDropDelegate(nullptr); + } + + // set start location + m_startPosition->setLocation(DelegateDragPosition::AppDrawer); +} + +void DragState::onDelegateDragFromFolderStarted(FolioApplicationFolder *folder, int position) +{ + // fetch delegate at start position + setDropDelegate(folder->applications()->getDelegate(position)); + + // set start location + m_startPosition->setFolder(folder); + m_startPosition->setFolderPosition(position); + m_startPosition->setLocation(DelegateDragPosition::Folder); +} + +void DragState::onDelegateDropped() +{ + if (!m_dropDelegate) { + return; + } + + // add dropped delegate + createDropPositionDelegate(); + + // delete empty pages at the end if they exist + // (it can be created if user drags app to new page, but doesn't place it there) + while (PageListModel::self()->isLastPageEmpty() && PageListModel::self()->rowCount() > 1) { + PageListModel::self()->removePage(PageListModel::self()->rowCount() - 1); + } + + // clear ghost position if there is one + FavouritesModel::self()->deleteGhostEntry(); + + // reset timers + m_folderInsertBetweenTimer->stop(); + m_changeFolderPageTimer->stop(); + m_leaveFolderTimer->stop(); + m_changePageTimer->stop(); + m_favouritesInsertBetweenTimer->stop(); + + // emit signal + Q_EMIT delegateDroppedAndPlaced(); +} + +void DragState::onLeaveCurrentFolder() +{ + if (!m_state) { + return; + } + + // reset timers + m_folderInsertBetweenTimer->stop(); + m_changeFolderPageTimer->stop(); + m_leaveFolderTimer->stop(); + + if (m_candidateDropPosition->location() == DelegateDragPosition::Folder && m_candidateDropPosition->folder()) { + // clear ghost entry + m_candidateDropPosition->folder()->applications()->deleteGhostEntry(); + } +} + +void DragState::onChangePageTimerFinished() +{ + if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate)) { + return; + } + + const int leftPagePosition = 0; + const int rightPagePosition = m_state->pageWidth(); + + qreal x = getDraggedDelegateX(); + if (qAbs(leftPagePosition - x) <= PAGE_CHANGE_THRESHOLD) { + // if we are at the left edge, go left + int page = m_state->currentPage() - 1; + if (page >= 0) { + m_state->goToPage(page); + } + + } else if (qAbs(rightPagePosition - x) <= PAGE_CHANGE_THRESHOLD) { + // if we are at the right edge, go right + int page = m_state->currentPage() + 1; + + // if we are at the right-most page, try to create a new one if the current page isn't empty + if (page == PageListModel::self()->rowCount() && !PageListModel::self()->isLastPageEmpty()) { + PageListModel::self()->addPageAtEnd(); + } + + // go to page if it exists + if (page < PageListModel::self()->rowCount()) { + m_state->goToPage(page); + } + } +} + +void DragState::onOpenFolderTimerFinished() +{ + if (!m_state || m_state->swipeState() != HomeScreenState::DraggingDelegate || m_state->viewState() != HomeScreenState::PageView + || (m_candidateDropPosition->location() != DelegateDragPosition::Pages && m_candidateDropPosition->location() != DelegateDragPosition::Favourites)) { + return; + } + + FolioApplicationFolder *folder = nullptr; + QPointF screenPosition; + + switch (m_candidateDropPosition->location()) { + case DelegateDragPosition::Pages: { + // get current page + PageModel *page = PageListModel::self()->getPage(m_candidateDropPosition->page()); + if (!page) { + return; + } + + // get delegate being hovered over + FolioDelegate *delegate = page->getDelegate(m_candidateDropPosition->pageRow(), m_candidateDropPosition->pageColumn()); + if (!delegate || delegate->type() != FolioDelegate::Folder) { + return; + } + + folder = delegate->folder(); + screenPosition = HomeScreenState::self()->getPageDelegateScreenPosition(m_candidateDropPosition->page(), + m_candidateDropPosition->pageRow(), + m_candidateDropPosition->pageColumn()); + break; + } + case DelegateDragPosition::Favourites: { + // get delegate being hovered over in favourites bar + FolioDelegate *delegate = FavouritesModel::self()->getEntryAt(m_candidateDropPosition->favouritesPosition()); + if (!delegate || delegate->type() != FolioDelegate::Folder) { + return; + } + + folder = delegate->folder(); + screenPosition = HomeScreenState::self()->getFavouritesDelegateScreenPosition(m_candidateDropPosition->favouritesPosition()); + break; + } + default: + break; + } + + // open the folder + m_state->openFolder(screenPosition.x(), screenPosition.y(), folder); +} + +void DragState::onLeaveFolderTimerFinished() +{ + if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) { + return; + } + + // check if the drag position is outside of the folder + if (m_state->currentFolder()->isDropPositionOutside(getDraggedDelegateX(), getDraggedDelegateY())) { + m_state->closeFolder(); + } +} + +void DragState::onChangeFolderPageTimerFinished() +{ + if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) { + return; + } + + auto *folder = m_state->currentFolder(); + + // check if the drag position is outside of the folder + if (folder->isDropPositionOutside(getDraggedDelegateX(), getDraggedDelegateY())) { + return; + } + + const qreal leftPagePosition = folder->applications()->leftMarginFromScreenEdge(); + const qreal rightPagePosition = m_state->viewWidth() - leftPagePosition; + + qreal x = getDraggedDelegateX(); + if (x <= leftPagePosition + PAGE_CHANGE_THRESHOLD) { + // if we are at the left edge, go left + int page = m_state->currentFolderPage() - 1; + if (page >= 0) { + m_state->goToFolderPage(page); + } + + } else if (x >= rightPagePosition - PAGE_CHANGE_THRESHOLD) { + // if we are at the right edge, go right + int page = m_state->currentFolderPage() + 1; + + // TODO!!!! + // if we are at the right-most page, try to create a new one if the current page isn't empty + // if (page == folder->applications()->rowCount() && !PageListModel::self()->isLastPageEmpty()) { + // PageListModel::self()->addPageAtEnd(); + // } + + // go to page if it exists + if (page < folder->applications()->numTotalPages()) { + m_state->goToFolderPage(page); + } + } +} + +void DragState::onFolderInsertBetweenTimerFinished() +{ + if (!m_state || (m_state->swipeState() != HomeScreenState::DraggingDelegate) || !m_state->currentFolder()) { + return; + } + + auto *folder = m_state->currentFolder(); + + // update the candidate drop position + m_candidateDropPosition->setFolder(folder); + m_candidateDropPosition->setFolderPosition(m_folderInsertBetweenIndex); + m_candidateDropPosition->setLocation(DelegateDragPosition::Folder); + + // insert it at this position, shifting existing apps to the side + // TODO the ghost entry may shift the m_folderInsertBetweenIndex, we should update?? + folder->applications()->setGhostEntry(m_folderInsertBetweenIndex); +} + +void DragState::onFavouritesInsertBetweenTimerFinished() +{ + // update the candidate drop position + m_candidateDropPosition->setFavouritesPosition(m_favouritesInsertBetweenIndex); + m_candidateDropPosition->setLocation(DelegateDragPosition::Favourites); + + // insert it at this position, shifting existing apps to the side + FavouritesModel::self()->setGhostEntry(m_favouritesInsertBetweenIndex); +} + +void DragState::deleteStartPositionDelegate() +{ + // delete the delegate at the start position + switch (m_startPosition->location()) { + case DelegateDragPosition::Pages: { + PageModel *page = PageListModel::self()->getPage(m_startPosition->page()); + if (page) { + page->removeDelegate(m_startPosition->pageRow(), m_startPosition->pageColumn()); + } + break; + } + case DelegateDragPosition::Favourites: + FavouritesModel::self()->removeEntry(m_startPosition->favouritesPosition()); + break; + case DelegateDragPosition::Folder: + m_startPosition->folder()->removeDelegate(m_startPosition->folderPosition()); + break; + case DelegateDragPosition::AppDrawer: + default: + break; + } +} + +void DragState::createDropPositionDelegate() +{ + if (!m_dropDelegate) { + return; + } + + // creates the delegate at the drop position + switch (m_candidateDropPosition->location()) { + case DelegateDragPosition::Pages: { + // locate the page we are dropping on + PageModel *page = PageListModel::self()->getPage(m_candidateDropPosition->page()); + if (!page) { + break; + } + + int row = m_candidateDropPosition->pageRow(); + int column = m_candidateDropPosition->pageColumn(); + + // delegate to add + FolioPageDelegate *delegate = new FolioPageDelegate{row, column, m_dropDelegate, page}; + + // delegate that exists at the drop position + FolioPageDelegate *existingDelegate = page->getDelegate(row, column); + + // if a delegate already exists at the spot, check if we can insert/create a folder + if (existingDelegate) { + if (delegate->type() == FolioDelegate::Application) { + if (existingDelegate->type() == FolioDelegate::Folder) { + // add the app to the existing folder + + auto existingFolder = existingDelegate->folder(); + existingFolder->addDelegate(delegate, existingFolder->applications()->rowCount()); + + break; + } else if (existingDelegate->type() == FolioDelegate::Application && !isStartPositionEqualDropPosition()) { + // create a folder from the two apps + + FolioApplicationFolder *folder = new FolioApplicationFolder(this, DEFAULT_FOLDER_NAME); + folder->addDelegate(delegate, 0); + folder->addDelegate(existingDelegate, 0); + FolioPageDelegate *folderDelegate = new FolioPageDelegate{row, column, folder, this}; + + page->removeDelegate(row, column); + page->addDelegate(folderDelegate); + + break; + } + } + } + + // default behavior for folders or dropping an app at an empty spot + + bool added = page->addDelegate(delegate); + + // if we couldn't add the delegate, try again but at the start position (return to start) + if (!added && !isStartPositionEqualDropPosition()) { + m_candidateDropPosition->copyFrom(m_startPosition); + createDropPositionDelegate(); + } + break; + } + case DelegateDragPosition::Favourites: { + // delegate that exists at the drop position + FolioDelegate *existingDelegate = FavouritesModel::self()->getEntryAt(m_candidateDropPosition->favouritesPosition()); + + // if a delegate already exists at the spot, check if we can insert/create a folder + if (existingDelegate) { + if (m_dropDelegate->type() == FolioDelegate::Application) { + if (existingDelegate->type() == FolioDelegate::Folder) { + // add the app to the existing folder + + auto existingFolder = existingDelegate->folder(); + existingFolder->addDelegate(m_dropDelegate, existingFolder->applications()->rowCount()); + + break; + } else if (existingDelegate->type() == FolioDelegate::Application && !isStartPositionEqualDropPosition()) { + // create a folder from the two apps + + FolioApplicationFolder *folder = new FolioApplicationFolder(this, DEFAULT_FOLDER_NAME); + folder->addDelegate(m_dropDelegate, 0); + folder->addDelegate(existingDelegate, 0); + FolioDelegate *folderDelegate = new FolioDelegate{folder, this}; + + FavouritesModel::self()->removeEntry(m_candidateDropPosition->favouritesPosition()); + FavouritesModel::self()->addEntry(m_candidateDropPosition->favouritesPosition(), folderDelegate); + + break; + } + } + } + + // otherwise, just add the delegate at this position + + bool added = FavouritesModel::self()->addEntry(m_candidateDropPosition->favouritesPosition(), m_dropDelegate); + + // if we couldn't add the delegate, try again but at the start position + if (!added && !isStartPositionEqualDropPosition()) { + m_candidateDropPosition->copyFrom(m_startPosition); + createDropPositionDelegate(); + } + + // correct position when we delete from an entry earlier in the favourites + if (added) { + if (m_startPosition->location() == DelegateDragPosition::Favourites + && m_startPosition->favouritesPosition() > m_candidateDropPosition->favouritesPosition()) { + m_startPosition->setFavouritesPosition(m_startPosition->favouritesPosition() - 1); + } + } + break; + } + case DelegateDragPosition::Folder: { + auto *folder = m_candidateDropPosition->folder(); + if (!folder) { + return; + } + + // only support dropping apps into folders + if (m_dropDelegate->type() != FolioDelegate::Application) { + return; + } + + bool added = folder->addDelegate(m_dropDelegate, m_candidateDropPosition->folderPosition()); + + // if we couldn't add the delegate, try again but at the start position + if (!added && !isStartPositionEqualDropPosition()) { + m_candidateDropPosition->copyFrom(m_startPosition); + createDropPositionDelegate(); + } + + if (added) { + folder->applications()->deleteGhostEntry(); + + // TODO correct m_startPosition? + } + break; + } + case DelegateDragPosition::AppDrawer: + default: + break; + } +} + +bool DragState::isStartPositionEqualDropPosition() +{ + return m_startPosition->location() == m_candidateDropPosition->location() && m_startPosition->page() == m_candidateDropPosition->page() + && m_startPosition->pageRow() == m_candidateDropPosition->pageRow() && m_startPosition->pageColumn() == m_candidateDropPosition->pageColumn() + && m_startPosition->favouritesPosition() == m_candidateDropPosition->favouritesPosition() + && m_startPosition->folder() == m_candidateDropPosition->folder() && m_startPosition->folderPosition() == m_candidateDropPosition->folderPosition(); +} + +qreal DragState::getDraggedDelegateX() +{ + // adjust to get the position of the center of the delegate + return m_state->delegateDragX() + m_state->pageCellWidth() / 2; +} + +qreal DragState::getDraggedDelegateY() +{ + // adjust to get the position of the center of the delegate + return m_state->delegateDragY() + m_state->pageCellHeight() / 2; +} diff --git a/containments/homescreens/folio/dragstate.h b/containments/homescreens/folio/dragstate.h new file mode 100644 index 00000000..b1c9381a --- /dev/null +++ b/containments/homescreens/folio/dragstate.h @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "folioapplicationfolder.h" +#include "foliodelegate.h" +#include "homescreenstate.h" + +class HomeScreenState; + +class DelegateDragPosition : public QObject +{ + Q_OBJECT + Q_PROPERTY(DelegateDragPosition::Location location READ location NOTIFY locationChanged) + Q_PROPERTY(int page READ page NOTIFY pageChanged) + Q_PROPERTY(int pageRow READ pageRow NOTIFY pageRowChanged) + Q_PROPERTY(int pageColumn READ pageColumn NOTIFY pageColumnChanged) + Q_PROPERTY(int favouritesPosition READ favouritesPosition NOTIFY favouritesPositionChanged) + Q_PROPERTY(int folderPosition READ folderPosition NOTIFY folderPositionChanged) + Q_PROPERTY(FolioApplicationFolder *folder READ folder NOTIFY folderChanged) + +public: + enum Location { Pages, Favourites, AppDrawer, Folder }; + Q_ENUM(Location) + + DelegateDragPosition(QObject *parent = nullptr); + ~DelegateDragPosition(); + + void copyFrom(DelegateDragPosition *position); + + Location location() const; + void setLocation(Location location); + + int page() const; + void setPage(int page); + + int pageRow() const; + void setPageRow(int pageRow); + + int pageColumn() const; + void setPageColumn(int pageColumn); + + int favouritesPosition() const; + void setFavouritesPosition(int favouritesPosition); + + int folderPosition() const; + void setFolderPosition(int folderPosition); + + // TODO: what if the folder becomes invalid? we need to clear it + FolioApplicationFolder *folder() const; + void setFolder(FolioApplicationFolder *folder); + +Q_SIGNALS: + void locationChanged(); + void pageChanged(); + void pageRowChanged(); + void pageColumnChanged(); + void favouritesPositionChanged(); + void folderPositionChanged(); + void folderChanged(); + +private: + Location m_location{DelegateDragPosition::Pages}; + int m_page{0}; + int m_pageRow{0}; + int m_pageColumn{0}; + int m_favouritesPosition{0}; + int m_folderPosition{0}; + FolioApplicationFolder *m_folder{nullptr}; +}; + +Q_DECLARE_METATYPE(DelegateDragPosition); + +class DragState : public QObject +{ + Q_OBJECT + Q_PROPERTY(DelegateDragPosition *candidateDropPosition READ candidateDropPosition CONSTANT) + Q_PROPERTY(DelegateDragPosition *startPosition READ startPosition CONSTANT) + Q_PROPERTY(FolioDelegate *dropDelegate READ dropDelegate NOTIFY dropDelegateChanged) + +public: + DragState(HomeScreenState *state = nullptr, QObject *parent = nullptr); + + DelegateDragPosition *candidateDropPosition() const; + DelegateDragPosition *startPosition() const; + FolioDelegate *dropDelegate() const; + void setDropDelegate(FolioDelegate *dropDelegate); + +Q_SIGNALS: + void dropDelegateChanged(); + void delegateDroppedAndPlaced(); + +private Q_SLOTS: + void onDelegateDragPositionChanged(); + void onDelegateDragPositionOverFolderViewChanged(); + void onDelegateDragPositionOverFavouritesChanged(); + void onDelegateDragPositionOverPageViewChanged(); + + void onDelegateDraggingStarted(); + void onDelegateDragFromPageStarted(int page, int row, int column); + void onDelegateDragFromFavouritesStarted(int position); + void onDelegateDragFromAppDrawerStarted(QString storageId); + void onDelegateDragFromFolderStarted(FolioApplicationFolder *folder, int position); + void onDelegateDropped(); + + void onLeaveCurrentFolder(); + + void onChangePageTimerFinished(); + void onOpenFolderTimerFinished(); + void onLeaveFolderTimerFinished(); + void onChangeFolderPageTimerFinished(); + void onFolderInsertBetweenTimerFinished(); + void onFavouritesInsertBetweenTimerFinished(); + +private: + // deletes the delegate at m_startPosition + void deleteStartPositionDelegate(); + + // deletes the delegate at m_candidateDropPosition + void createDropPositionDelegate(); + + // whether m_startPosition = m_candidateDropPosition + bool isStartPositionEqualDropPosition(); + + // we need to adjust so that the coord is in the center of the delegate + qreal getDraggedDelegateX(); + qreal getDraggedDelegateY(); + + QTimer *m_changePageTimer{nullptr}; + QTimer *m_openFolderTimer{nullptr}; + QTimer *m_leaveFolderTimer{nullptr}; + QTimer *m_changeFolderPageTimer{nullptr}; + + // inserting between apps in a folder + QTimer *m_folderInsertBetweenTimer{nullptr}; + int m_folderInsertBetweenIndex{0}; + + // inserting between apps in the favourites strip + QTimer *m_favouritesInsertBetweenTimer{nullptr}; + int m_favouritesInsertBetweenIndex{0}; + + // the delegate that is being dropped + FolioDelegate *m_dropDelegate{nullptr}; + + // where we are hovering over, potentially to drop the delegate + DelegateDragPosition *const m_candidateDropPosition{nullptr}; + + // this is the original start position of the drag + DelegateDragPosition *const m_startPosition{nullptr}; + + HomeScreenState *m_state{nullptr}; +}; diff --git a/containments/homescreens/folio/favouritesmodel.cpp b/containments/homescreens/folio/favouritesmodel.cpp new file mode 100644 index 00000000..1447995b --- /dev/null +++ b/containments/homescreens/folio/favouritesmodel.cpp @@ -0,0 +1,403 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "favouritesmodel.h" +#include "homescreenstate.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +FavouritesModel *FavouritesModel::self() +{ + static FavouritesModel *inst = new FavouritesModel(); + return inst; +} + +FavouritesModel::FavouritesModel(QObject *parent) + : QAbstractListModel{parent} +{ + connect(HomeScreenState::self(), &HomeScreenState::pageWidthChanged, this, [this]() { + evaluateDelegatePositions(true); + }); + connect(HomeScreenState::self(), &HomeScreenState::pageHeightChanged, this, [this]() { + evaluateDelegatePositions(true); + }); + connect(HomeScreenState::self(), &HomeScreenState::pageCellWidthChanged, this, [this]() { + evaluateDelegatePositions(true); + }); + connect(HomeScreenState::self(), &HomeScreenState::pageCellHeightChanged, this, [this]() { + evaluateDelegatePositions(true); + }); + connect(HomeScreenState::self(), &HomeScreenState::favouritesBarLocationChanged, this, [this]() { + evaluateDelegatePositions(true); + }); + connect(HomeScreenState::self(), &HomeScreenState::pageOrientationChanged, this, [this]() { + evaluateDelegatePositions(true); + }); +} + +int FavouritesModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_delegates.count(); +} + +QVariant FavouritesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_delegates.size()) { + return QVariant(); + } + + switch (role) { + case DelegateRole: + return QVariant::fromValue(m_delegates.at(index.row()).delegate); + case XPositionRole: + return QVariant::fromValue(m_delegates.at(index.row()).xPosition); + } + + return QVariant(); +} + +QHash FavouritesModel::roleNames() const +{ + return {{DelegateRole, "delegate"}, {XPositionRole, "xPosition"}}; +} + +void FavouritesModel::removeEntry(int row) +{ + if (row < 0 || row >= m_delegates.size()) { + return; + } + + beginRemoveRows(QModelIndex(), row, row); + // HACK: do not deleteLater(), because the delegate might still be used somewhere else + // m_delegates[row].delegate->deleteLater(); + m_delegates.removeAt(row); + endRemoveRows(); + + evaluateDelegatePositions(); + + save(); +} + +void FavouritesModel::moveEntry(int fromRow, int toRow) +{ + if (fromRow < 0 || toRow < 0 || fromRow >= m_delegates.size() || toRow >= m_delegates.size() || fromRow == toRow) { + return; + } + if (toRow > fromRow) { + ++toRow; + } + + beginMoveRows(QModelIndex(), fromRow, fromRow, QModelIndex(), toRow); + if (toRow > fromRow) { + auto delegate = m_delegates.at(fromRow); + m_delegates.insert(toRow, delegate); + m_delegates.takeAt(fromRow); + + } else { + auto delegate = m_delegates.takeAt(fromRow); + m_delegates.insert(toRow, delegate); + } + endMoveRows(); + + evaluateDelegatePositions(); + + save(); +} + +bool FavouritesModel::addEntry(int row, FolioDelegate *delegate) +{ + if (!delegate) { + return false; + } + + if (row < 0 || row > m_delegates.size()) { + return false; + } + + if (row == m_delegates.size()) { + beginInsertRows(QModelIndex(), row, row); + m_delegates.append({delegate, 0}); + evaluateDelegatePositions(false); + endInsertRows(); + } else if (m_delegates[row].delegate->type() == FolioDelegate::None) { + replaceGhostEntry(delegate); + } else { + beginInsertRows(QModelIndex(), row, row); + m_delegates.insert(row, {delegate, 0}); + evaluateDelegatePositions(false); + endInsertRows(); + } + + evaluateDelegatePositions(); + + save(); + + return true; +} + +FolioDelegate *FavouritesModel::getEntryAt(int row) +{ + if (row < 0 || row >= m_delegates.size()) { + return nullptr; + } + + return m_delegates[row].delegate; +} + +int FavouritesModel::getGhostEntryPosition() +{ + for (int i = 0; i < m_delegates.size(); i++) { + if (m_delegates[i].delegate->type() == FolioDelegate::None) { + return i; + } + } + return -1; +} + +void FavouritesModel::setGhostEntry(int row) +{ + bool found = false; + + // check if a ghost entry already exists, then swap them + for (int i = 0; i < m_delegates.size(); i++) { + if (m_delegates[i].delegate->type() == FolioDelegate::None) { + found = true; + + if (row != i) { + moveEntry(i, row); + } + } + } + + // if it doesn't, add a new empty delegate + if (!found) { + FolioDelegate *ghost = new FolioDelegate{this}; + addEntry(row, ghost); + } +} + +void FavouritesModel::replaceGhostEntry(FolioDelegate *delegate) +{ + for (int i = 0; i < m_delegates.size(); i++) { + if (m_delegates[i].delegate->type() == FolioDelegate::None) { + m_delegates[i].delegate = delegate; + + Q_EMIT dataChanged(createIndex(i, 0), createIndex(i, 0), {DelegateRole}); + break; + } + } +} + +void FavouritesModel::deleteGhostEntry() +{ + for (int i = 0; i < m_delegates.size(); i++) { + if (m_delegates[i].delegate->type() == FolioDelegate::None) { + removeEntry(i); + } + } +} + +void FavouritesModel::save() +{ + if (!m_applet) { + return; + } + + QJsonArray arr; + for (int i = 0; i < m_delegates.size(); i++) { + FolioDelegate *delegate = m_delegates[i].delegate; + + // if this delegate is empty, ignore it + if (!delegate || delegate->type() == FolioDelegate::None) { + continue; + } + + arr.append(delegate->toJson()); + } + QByteArray data = QJsonDocument(arr).toJson(QJsonDocument::Compact); + + m_applet->config().writeEntry("Favourites", QString::fromStdString(data.toStdString())); + Q_EMIT m_applet->configNeedsSaving(); +} + +void FavouritesModel::load() +{ + if (!m_applet) { + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(m_applet->config().readEntry("Favourites", "{}").toUtf8()); + + beginResetModel(); + + m_delegates.clear(); + + for (QJsonValueRef r : doc.array()) { + QJsonObject obj = r.toObject(); + FolioDelegate *delegate = FolioDelegate::fromJson(obj, this); + + if (delegate) { + if (delegate->type() == FolioDelegate::Folder) { + connect(delegate->folder(), &FolioApplicationFolder::saveRequested, this, &FavouritesModel::save); + } + + m_delegates.append({delegate, 0}); + } + } + + evaluateDelegatePositions(false); + endResetModel(); +} + +void FavouritesModel::setApplet(Plasma::Applet *applet) +{ + m_applet = applet; +} + +bool FavouritesModel::dropPositionIsEdge(qreal x, qreal y) const +{ + qreal startPosition = getDelegateRowStartPos(); + bool isLocationBottom = HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom; + qreal cellLength = isLocationBottom ? HomeScreenState::self()->pageCellWidth() : HomeScreenState::self()->pageCellHeight(); + + qreal pos = isLocationBottom ? x : y; + + if (pos < startPosition) { + return true; + } + + qreal currentPos = startPosition; + + for (int i = 0; i < m_delegates.size(); i++) { + // if it is within the centre 70% of a delegate, it is not at an edge + if (pos >= (currentPos + cellLength * 0.15) && pos <= (currentPos + cellLength * 0.85)) { + return false; + } + + currentPos += cellLength; + } + + return true; +} + +int FavouritesModel::dropInsertPosition(qreal x, qreal y) const +{ + qreal startPosition = getDelegateRowStartPos(); + bool isLocationBottom = HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom; + qreal cellLength = isLocationBottom ? HomeScreenState::self()->pageCellWidth() : HomeScreenState::self()->pageCellHeight(); + + qreal pos = isLocationBottom ? x : y; + + if (pos < startPosition) { + return adjustIndex(0); + } + + qreal currentPos = startPosition; + for (int i = 0; i < m_delegates.size(); i++) { + if (pos < currentPos + cellLength * 0.85) { + return adjustIndex(i); + } else if (pos < currentPos + cellLength) { + return adjustIndex(i + 1); + } + + currentPos += cellLength; + } + return adjustIndex(m_delegates.size()); +} + +QPointF FavouritesModel::getDelegateScreenPosition(int position) const +{ + position = adjustIndex(position); + + qreal screenHeight = HomeScreenState::self()->viewHeight(); + qreal screenWidth = HomeScreenState::self()->viewWidth(); + qreal pageHeight = HomeScreenState::self()->pageHeight(); + qreal pageWidth = HomeScreenState::self()->pageWidth(); + qreal screenTopPadding = HomeScreenState::self()->viewTopPadding(); + qreal screenBottomPadding = HomeScreenState::self()->viewBottomPadding(); + qreal screenLeftPadding = HomeScreenState::self()->viewLeftPadding(); + qreal screenRightPadding = HomeScreenState::self()->viewRightPadding(); + qreal cellHeight = HomeScreenState::self()->pageCellHeight(); + qreal cellWidth = HomeScreenState::self()->pageCellWidth(); + + qreal startPosition = getDelegateRowStartPos(); + + switch (HomeScreenState::self()->favouritesBarLocation()) { + case HomeScreenState::Bottom: { + qreal favouritesHeight = screenHeight - pageHeight - screenBottomPadding - screenTopPadding; + qreal x = screenLeftPadding + startPosition + cellWidth * position; + qreal y = screenTopPadding + pageHeight + (favouritesHeight / 2) - (cellHeight / 2); + return {x, y}; + } + case HomeScreenState::Left: { + qreal favouritesWidth = screenWidth - screenLeftPadding - pageWidth - screenRightPadding; + qreal x = screenLeftPadding + (favouritesWidth / 2) - (cellWidth / 2); + qreal y = startPosition + cellHeight * position; + return {x, y}; + } + case HomeScreenState::Right: { + qreal favouritesWidth = screenWidth - screenLeftPadding - pageWidth - screenRightPadding; + qreal x = screenLeftPadding + pageWidth + (favouritesWidth / 2) - (cellWidth / 2); + qreal y = startPosition + cellHeight * position; + return {x, y}; + } + } + return {0, 0}; +} + +void FavouritesModel::evaluateDelegatePositions(bool emitSignal) +{ + bool isLocationBottom = HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom; + qreal cellLength = isLocationBottom ? HomeScreenState::self()->pageCellWidth() : HomeScreenState::self()->pageCellHeight(); + qreal startPosition = getDelegateRowStartPos(); + qreal currentPos = startPosition; + + for (int i = 0; i < m_delegates.size(); ++i) { + m_delegates[adjustIndex(i)].xPosition = qRound(currentPos); + currentPos += cellLength; + } + + if (emitSignal) { + Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_delegates.size() - 1, 0), {XPositionRole}); + } +} + +qreal FavouritesModel::getDelegateRowStartPos() const +{ + const int length = m_delegates.size(); + const bool isLocationBottom = HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom; + const qreal cellLength = isLocationBottom ? HomeScreenState::self()->pageCellWidth() : HomeScreenState::self()->pageCellHeight(); + const qreal pageLength = isLocationBottom ? HomeScreenState::self()->pageWidth() : HomeScreenState::self()->pageHeight(); + + const qreal topMargin = HomeScreenState::self()->viewTopPadding(); + const qreal leftMargin = HomeScreenState::self()->viewLeftPadding(); + const qreal panelOffset = isLocationBottom ? leftMargin : topMargin; + + return (pageLength / 2) - (((qreal)length) / 2) * cellLength + panelOffset; +} + +int FavouritesModel::adjustIndex(int index) const +{ + if (HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Bottom + || HomeScreenState::self()->favouritesBarLocation() == HomeScreenState::Left) { + return index; + } else { + // if it's on the right side of the screen, we flip the order of the delegates + return qMax(0, m_delegates.size() - index - 1); + } +} diff --git a/containments/homescreens/folio/favouritesmodel.h b/containments/homescreens/folio/favouritesmodel.h new file mode 100644 index 00000000..93381759 --- /dev/null +++ b/containments/homescreens/folio/favouritesmodel.h @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include + +#include "foliodelegate.h" + +struct FavouritesDelegate { + FolioDelegate *delegate; + qreal xPosition; +}; + +class FavouritesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + DelegateRole = Qt::UserRole + 1, + XPositionRole, + }; + + FavouritesModel(QObject *parent = nullptr); + static FavouritesModel *self(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + Q_INVOKABLE void removeEntry(int row); + void moveEntry(int fromRow, int toRow); + bool addEntry(int row, FolioDelegate *delegate); + FolioDelegate *getEntryAt(int row); + + // for use with drag and drop, as the delegate is dragged around + // ghost - fake delegate exists at an index, so a gap is created + // invisible - existing delegate looks like it doesn't exist + int getGhostEntryPosition(); + void setGhostEntry(int row); + void replaceGhostEntry(FolioDelegate *delegate); + void deleteGhostEntry(); + + // whether the position given is in between 2 delegates, or at the edge. + // this would return false if dropping should place the delegate into a folder/create a folder. + bool dropPositionIsEdge(qreal x, qreal y) const; + + // the index that dropping at the position given would place the delegate at. + int dropInsertPosition(qreal x, qreal y) const; + + QPointF getDelegateScreenPosition(int position) const; + + Q_INVOKABLE void load(); + + void setApplet(Plasma::Applet *applet); + +private: + void save(); + void evaluateDelegatePositions(bool emitSignal = true); + + // get the x (or y) position where delegates start being placed + qreal getDelegateRowStartPos() const; + + // adjusts the index in relation to the page orientation + // this is so that we only have to calculate positions assuming one orientation + int adjustIndex(int index) const; + + QList m_delegates; + + Plasma::Applet *m_applet{nullptr}; +}; diff --git a/containments/homescreens/folio/folioapplication.cpp b/containments/homescreens/folio/folioapplication.cpp new file mode 100644 index 00000000..e4741e08 --- /dev/null +++ b/containments/homescreens/folio/folioapplication.cpp @@ -0,0 +1,139 @@ + +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "folioapplication.h" +#include "windowlistener.h" + +#include + +#include + +FolioApplication::FolioApplication(QObject *parent, KService::Ptr service) + : QObject{parent} + , m_running{false} + , m_name{service->name()} + , m_icon{service->icon()} + , m_storageId{service->storageId()} +{ + auto windows = WindowListener::instance()->windowsFromStorageId(m_storageId); + if (windows.empty()) { + m_window = nullptr; + } else { + m_window = windows[0]; + } + + connect(WindowListener::instance(), &WindowListener::windowChanged, this, [this](QString storageId) { + if (storageId == m_storageId) { + auto windows = WindowListener::instance()->windowsFromStorageId(m_storageId); + if (windows.empty()) { + setWindow(nullptr); + } else { + setWindow(windows[0]); + } + } + }); +} + +FolioApplication *FolioApplication::fromJson(QJsonObject &obj, QObject *parent) +{ + QString storageId = obj[QStringLiteral("storageId")].toString(); + if (KService::Ptr service = KService::serviceByStorageId(storageId)) { + return new FolioApplication(parent, service); + } + return nullptr; +} + +QJsonObject FolioApplication::toJson() +{ + QJsonObject obj; + obj[QStringLiteral("type")] = "application"; + obj[QStringLiteral("storageId")] = m_storageId; + return obj; +} + +bool FolioApplication::running() const +{ + return m_window != nullptr; +} + +QString FolioApplication::name() const +{ + return m_name; +} + +QString FolioApplication::icon() const +{ + return m_icon; +} + +QString FolioApplication::storageId() const +{ + return m_storageId; +} + +KWayland::Client::PlasmaWindow *FolioApplication::window() const +{ + return m_window; +} + +void FolioApplication::setName(QString &name) +{ + m_name = name; + Q_EMIT nameChanged(); +} + +void FolioApplication::setIcon(QString &icon) +{ + m_icon = icon; + Q_EMIT iconChanged(); +} + +void FolioApplication::setStorageId(QString &storageId) +{ + m_storageId = storageId; + Q_EMIT storageIdChanged(); +} + +void FolioApplication::setWindow(KWayland::Client::PlasmaWindow *window) +{ + m_window = window; + Q_EMIT windowChanged(); +} + +void FolioApplication::setMinimizedDelegate(QQuickItem *delegate) +{ + QWindow *delegateWindow = delegate->window(); + if (!delegateWindow) { + return; + } + if (!m_window) { + return; + } + + KWayland::Client::Surface *surface = KWayland::Client::Surface::fromWindow(delegateWindow); + if (!surface) { + return; + } + + QRect rect = delegate->mapRectToScene(QRectF(0, 0, delegate->width(), delegate->height())).toRect(); + m_window->setMinimizedGeometry(surface, rect); +} + +void FolioApplication::unsetMinimizedDelegate(QQuickItem *delegate) +{ + QWindow *delegateWindow = delegate->window(); + if (!delegateWindow) { + return; + } + if (!m_window) { + return; + } + + KWayland::Client::Surface *surface = KWayland::Client::Surface::fromWindow(delegateWindow); + if (!surface) { + return; + } + + m_window->unsetMinimizedGeometry(surface); +} diff --git a/containments/homescreens/folio/folioapplication.h b/containments/homescreens/folio/folioapplication.h new file mode 100644 index 00000000..e816c562 --- /dev/null +++ b/containments/homescreens/folio/folioapplication.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +/** + * @short Object that represents an application. + */ +class FolioApplication : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool running READ running NOTIFY windowChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged) + Q_PROPERTY(QString storageId READ storageId NOTIFY storageIdChanged) + +public: + FolioApplication(QObject *parent = nullptr, KService::Ptr service = QExplicitlySharedDataPointer{nullptr}); + + static FolioApplication *fromJson(QJsonObject &obj, QObject *parent); // may return nullptr + QJsonObject toJson(); + + bool running() const; + QString name() const; + QString icon() const; + QString storageId() const; + KWayland::Client::PlasmaWindow *window() const; + + void setName(QString &name); + void setIcon(QString &icon); + void setStorageId(QString &storageId); + void setWindow(KWayland::Client::PlasmaWindow *window); + + Q_INVOKABLE void setMinimizedDelegate(QQuickItem *delegate); + Q_INVOKABLE void unsetMinimizedDelegate(QQuickItem *delegate); + +Q_SIGNALS: + void nameChanged(); + void iconChanged(); + void storageIdChanged(); + void windowChanged(); + +private: + bool m_running; + QString m_name; + QString m_icon; + QString m_storageId; + KWayland::Client::PlasmaWindow *m_window{nullptr}; +}; diff --git a/containments/homescreens/folio/folioapplicationfolder.cpp b/containments/homescreens/folio/folioapplicationfolder.cpp new file mode 100644 index 00000000..13eddba0 --- /dev/null +++ b/containments/homescreens/folio/folioapplicationfolder.cpp @@ -0,0 +1,468 @@ +// SPDX-FileCopyrightText: 2022-2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "folioapplicationfolder.h" +#include "homescreenstate.h" + +#include +#include + +FolioApplicationFolder::FolioApplicationFolder(QObject *parent, QString name) + : QObject{parent} + , m_name{name} + , m_applicationFolderModel{new ApplicationFolderModel{this}} +{ +} + +FolioApplicationFolder *FolioApplicationFolder::fromJson(QJsonObject &obj, QObject *parent) +{ + QString name = obj[QStringLiteral("name")].toString(); + QList apps; + for (auto storageId : obj[QStringLiteral("apps")].toArray()) { + if (KService::Ptr service = KService::serviceByStorageId(storageId.toString())) { + apps.append(new FolioApplication(parent, service)); + } + } + + FolioApplicationFolder *folder = new FolioApplicationFolder(parent, name); + folder->setApplications(apps); + return folder; +} + +QJsonObject FolioApplicationFolder::toJson() +{ + QJsonObject obj; + obj[QStringLiteral("type")] = "folder"; + obj[QStringLiteral("name")] = m_name; + + QJsonArray arr; + for (auto delegate : m_delegates) { + if (delegate.delegate->type() != FolioDelegate::Application) { + continue; + } + arr.append(QJsonValue::fromVariant(delegate.delegate->application()->storageId())); + } + + obj[QStringLiteral("apps")] = arr; + + return obj; +} + +QString FolioApplicationFolder::name() const +{ + return m_name; +} + +void FolioApplicationFolder::setName(QString &name) +{ + m_name = name; + Q_EMIT nameChanged(); + Q_EMIT saveRequested(); +} + +QList FolioApplicationFolder::appPreviews() +{ + QList previews; + // we give a maximum of 4 icons + for (int i = 0; i < std::min(m_delegates.size(), 4); ++i) { + if (!m_delegates[i].delegate->application()) { + continue; + } + previews.push_back(m_delegates[i].delegate->application()); + } + return previews; +} + +ApplicationFolderModel *FolioApplicationFolder::applications() +{ + return m_applicationFolderModel; +} + +void FolioApplicationFolder::setApplications(QList applications) +{ + if (m_applicationFolderModel) { + m_applicationFolderModel->deleteLater(); + } + + m_delegates.clear(); + for (auto *app : applications) { + m_delegates.append({new FolioDelegate{app, this}, 0, 0}); + } + m_applicationFolderModel = new ApplicationFolderModel{this}; + m_applicationFolderModel->evaluateDelegatePositions(); + + Q_EMIT applicationsChanged(); + Q_EMIT applicationsReset(); + Q_EMIT saveRequested(); +} + +void FolioApplicationFolder::moveEntry(int fromRow, int toRow) +{ + m_applicationFolderModel->moveEntry(fromRow, toRow); +} + +bool FolioApplicationFolder::addDelegate(FolioDelegate *delegate, int row) +{ + return m_applicationFolderModel->addDelegate(delegate, row); +} + +void FolioApplicationFolder::removeDelegate(int row) +{ + m_applicationFolderModel->removeDelegate(row); +} + +int FolioApplicationFolder::dropInsertPosition(int page, qreal x, qreal y) +{ + return m_applicationFolderModel->dropInsertPosition(page, x, y); +} + +bool FolioApplicationFolder::isDropPositionOutside(qreal x, qreal y) +{ + return m_applicationFolderModel->isDropPositionOutside(x, y); +} + +ApplicationFolderModel::ApplicationFolderModel(FolioApplicationFolder *folder) + : QAbstractListModel{folder} + , m_folder{folder} +{ + connect(HomeScreenState::self(), &HomeScreenState::folderPageWidthChanged, this, [this]() { + evaluateDelegatePositions(); + }); + connect(HomeScreenState::self(), &HomeScreenState::folderPageHeightChanged, this, [this]() { + evaluateDelegatePositions(); + }); + connect(HomeScreenState::self(), &HomeScreenState::folderPageContentWidthChanged, this, [this]() { + evaluateDelegatePositions(); + }); + connect(HomeScreenState::self(), &HomeScreenState::folderPageContentHeightChanged, this, [this]() { + evaluateDelegatePositions(); + }); + connect(HomeScreenState::self(), &HomeScreenState::viewWidthChanged, this, [this]() { + evaluateDelegatePositions(); + }); + connect(HomeScreenState::self(), &HomeScreenState::viewHeightChanged, this, [this]() { + evaluateDelegatePositions(); + }); + connect(HomeScreenState::self(), &HomeScreenState::pageCellWidthChanged, this, [this]() { + evaluateDelegatePositions(); + }); + connect(HomeScreenState::self(), &HomeScreenState::pageCellHeightChanged, this, [this]() { + evaluateDelegatePositions(); + }); +} + +int ApplicationFolderModel::rowCount(const QModelIndex &parent) const +{ + return m_folder->m_delegates.size(); +} + +QVariant ApplicationFolderModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + switch (role) { + case DelegateRole: + return QVariant::fromValue(m_folder->m_delegates.at(index.row()).delegate); + case XPositionRole: + return QVariant::fromValue(m_folder->m_delegates.at(index.row()).xPosition); + case YPositionRole: + return QVariant::fromValue(m_folder->m_delegates.at(index.row()).yPosition); + } + + return QVariant(); +} + +QHash ApplicationFolderModel::roleNames() const +{ + return {{DelegateRole, "delegate"}, {XPositionRole, "xPosition"}, {YPositionRole, "yPosition"}}; +} + +FolioDelegate *ApplicationFolderModel::getDelegate(int index) +{ + if (index < 0 || index >= m_folder->m_delegates.size()) { + return nullptr; + } + return m_folder->m_delegates[index].delegate; +} + +void ApplicationFolderModel::moveEntry(int fromRow, int toRow) +{ + if (fromRow < 0 || toRow < 0 || fromRow >= m_folder->m_delegates.size() || toRow >= m_folder->m_delegates.size() || fromRow == toRow) { + return; + } + if (toRow > fromRow) { + ++toRow; + } + + beginMoveRows(QModelIndex(), fromRow, fromRow, QModelIndex(), toRow); + if (toRow > fromRow) { + auto delegate = m_folder->m_delegates.at(fromRow); + m_folder->m_delegates.insert(toRow, delegate); + m_folder->m_delegates.takeAt(fromRow); + } else { + auto delegate = m_folder->m_delegates.takeAt(fromRow); + m_folder->m_delegates.insert(toRow, delegate); + } + endMoveRows(); + + evaluateDelegatePositions(); + + Q_EMIT m_folder->applicationsChanged(); + Q_EMIT m_folder->saveRequested(); +} + +bool ApplicationFolderModel::addDelegate(FolioDelegate *delegate, int index) +{ + if (index < 0 || index > m_folder->m_delegates.size()) { + return false; + } + + if (!delegate) { + return false; + } + + if (index == m_folder->m_delegates.size()) { + beginInsertRows(QModelIndex(), index, index); + m_folder->m_delegates.append({delegate, 0, 0}); + evaluateDelegatePositions(false); + endInsertRows(); + } else if (m_folder->m_delegates[index].delegate->type() == FolioDelegate::None) { + replaceGhostEntry(delegate); + } else { + beginInsertRows(QModelIndex(), index, index); + m_folder->m_delegates.insert(index, {delegate, 0, 0}); + evaluateDelegatePositions(false); + endInsertRows(); + } + + evaluateDelegatePositions(); + + Q_EMIT m_folder->applicationsChanged(); + Q_EMIT m_folder->saveRequested(); + + return true; +} + +void ApplicationFolderModel::removeDelegate(int index) +{ + if (index < 0 || index >= m_folder->m_delegates.size()) { + return; + } + + beginRemoveRows(QModelIndex(), index, index); + // HACK: do not deleteLater(), because the delegate might still be used somewhere else + // m_folder->m_delegates[index].app->deleteLater(); + m_folder->m_delegates.removeAt(index); + endRemoveRows(); + + evaluateDelegatePositions(); + + Q_EMIT m_folder->applicationsChanged(); + Q_EMIT m_folder->saveRequested(); +} + +QPointF ApplicationFolderModel::getDelegatePosition(int index) +{ + auto delegate = m_folder->m_delegates[index]; + return {delegate.xPosition, delegate.yPosition}; +} + +int ApplicationFolderModel::getGhostEntryPosition() +{ + for (int i = 0; i < m_folder->m_delegates.size(); i++) { + if (m_folder->m_delegates[i].delegate->type() == FolioDelegate::None) { + return i; + } + } + return -1; +} + +void ApplicationFolderModel::setGhostEntry(int index) +{ + FolioDelegate *ghost = nullptr; + + // check if a ghost entry already exists + for (int i = 0; i < m_folder->m_delegates.size(); i++) { + auto delegate = m_folder->m_delegates[i].delegate; + if (delegate->type() == FolioDelegate::None) { + ghost = delegate; + + // remove it + removeDelegate(i); + + // correct index if necessary due to deletion + if (index > i) { + index--; + } + } + } + + if (!ghost) { + ghost = new FolioDelegate{HomeScreenState::self()}; + } + + // add empty delegate at new position + addDelegate(ghost, index); +} + +void ApplicationFolderModel::replaceGhostEntry(FolioDelegate *delegate) +{ + for (int i = 0; i < m_folder->m_delegates.size(); i++) { + if (m_folder->m_delegates[i].delegate->type() == FolioDelegate::None) { + m_folder->m_delegates[i].delegate = delegate; + + Q_EMIT dataChanged(createIndex(i, 0), createIndex(i, 0), {DelegateRole}); + break; + } + } +} + +void ApplicationFolderModel::deleteGhostEntry() +{ + for (int i = 0; i < m_folder->m_delegates.size(); i++) { + if (m_folder->m_delegates[i].delegate->type() == FolioDelegate::None) { + removeDelegate(i); + } + } +} + +int ApplicationFolderModel::dropInsertPosition(int page, qreal x, qreal y) +{ + qreal cellWidth = HomeScreenState::self()->pageCellWidth(); + qreal cellHeight = HomeScreenState::self()->pageCellHeight(); + + int row = (y - topMarginFromScreenEdge()) / cellHeight; + row = std::max(0, std::min(numRowsOnPage(), row)); + + // the index that the position is over + int leftColumn = std::max(0.0, x - leftMarginFromScreenEdge()) / cellWidth; + leftColumn = std::min(numColumnsOnPage() - 1, leftColumn); + + qreal leftColumnPosition = leftColumn * cellWidth + leftMarginFromScreenEdge(); + + int column = leftColumn + 1; + + // if it's the left half of this position or it's the last column on this row, return itself + if ((x < leftColumnPosition + cellWidth * 0.5) || (leftColumn == numColumnsOnPage() - 1)) { + column = leftColumn; + } + + // calculate the position based on the page, row and column it is at + int pos = (page * numRowsOnPage() * numColumnsOnPage()) + (row * numColumnsOnPage()) + column; + // make sure it's in bounds + return std::min((int)m_folder->m_delegates.size(), std::max(0, pos)); +} + +bool ApplicationFolderModel::isDropPositionOutside(qreal x, qreal y) +{ + return (x < leftMarginFromScreenEdge()) || (x > (HomeScreenState::self()->viewWidth() - leftMarginFromScreenEdge())) || (y < topMarginFromScreenEdge()) + || (y > HomeScreenState::self()->viewHeight() - topMarginFromScreenEdge()); +} + +void ApplicationFolderModel::evaluateDelegatePositions(bool emitSignal) +{ + qreal pageWidth = HomeScreenState::self()->folderPageWidth(); + qreal topMargin = verticalPageMargin(); + qreal leftMargin = horizontalPageMargin(); + + qreal cellWidth = HomeScreenState::self()->pageCellWidth(); + qreal cellHeight = HomeScreenState::self()->pageCellHeight(); + + int rows = numRowsOnPage(); + int columns = numColumnsOnPage(); + int numOfDelegates = m_folder->m_delegates.size(); + + int index = 0; + int page = 0; + + while (index < m_folder->m_delegates.size()) { + int prevIndex = index; + + // determine positions page-by-page + for (int row = 0; row < rows && index < numOfDelegates; row++) { + for (int column = 0; column < columns && index < numOfDelegates; column++) { + m_folder->m_delegates[index].xPosition = qRound(page * pageWidth + leftMargin + column * cellWidth); + m_folder->m_delegates[index].yPosition = qRound(topMargin + row * cellHeight); + index++; + } + } + + // prevent infinite loop + if (prevIndex == index) { + break; + } + page++; + } + + if (emitSignal) { + Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_folder->m_delegates.size() - 1, 0), {XPositionRole}); + Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_folder->m_delegates.size() - 1, 0), {YPositionRole}); + } + + Q_EMIT numberOfPagesChanged(); +} + +QPointF ApplicationFolderModel::getDelegateStartPosition(int page) +{ + qreal pageWidth = HomeScreenState::self()->folderPageWidth(); + + qreal x = pageWidth * page + leftMarginFromScreenEdge(); + qreal y = topMarginFromScreenEdge(); + return QPointF{x, y}; +} + +int ApplicationFolderModel::numTotalPages() +{ + int numOfDelegatesOnPage = numRowsOnPage() * numColumnsOnPage(); + return std::ceil(((qreal)m_folder->m_delegates.size()) / numOfDelegatesOnPage); +} + +int ApplicationFolderModel::numRowsOnPage() +{ + qreal contentHeight = HomeScreenState::self()->folderPageContentHeight(); + qreal cellHeight = HomeScreenState::self()->pageCellHeight(); + + return std::max(0.0, contentHeight / cellHeight); +} + +int ApplicationFolderModel::numColumnsOnPage() +{ + qreal contentWidth = HomeScreenState::self()->folderPageContentWidth(); + qreal cellWidth = HomeScreenState::self()->pageCellWidth(); + + return std::max(0.0, contentWidth / cellWidth); +} + +qreal ApplicationFolderModel::leftMarginFromScreenEdge() +{ + qreal viewWidth = HomeScreenState::self()->viewWidth(); + qreal folderPageWidth = HomeScreenState::self()->folderPageWidth(); + + return (viewWidth - folderPageWidth) / 2 + horizontalPageMargin(); +} + +qreal ApplicationFolderModel::topMarginFromScreenEdge() +{ + qreal viewHeight = HomeScreenState::self()->viewHeight(); + qreal folderPageHeight = HomeScreenState::self()->folderPageHeight(); + + return (viewHeight - folderPageHeight) / 2 + verticalPageMargin(); +} + +qreal ApplicationFolderModel::horizontalPageMargin() +{ + qreal pageWidth = HomeScreenState::self()->folderPageWidth(); + qreal pageContentWidth = HomeScreenState::self()->folderPageContentWidth(); + + return (pageWidth - pageContentWidth) / 2; +} + +qreal ApplicationFolderModel::verticalPageMargin() +{ + qreal pageHeight = HomeScreenState::self()->folderPageHeight(); + qreal pageContentHeight = HomeScreenState::self()->folderPageContentHeight(); + + return (pageHeight - pageContentHeight) / 2; +} diff --git a/containments/homescreens/folio/folioapplicationfolder.h b/containments/homescreens/folio/folioapplicationfolder.h new file mode 100644 index 00000000..bcbf16e0 --- /dev/null +++ b/containments/homescreens/folio/folioapplicationfolder.h @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2022-2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "folioapplication.h" +#include "foliodelegate.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +struct ApplicationDelegate; +class ApplicationFolderModel; +class FolioDelegate; + +/** + * @short Object that represents an application folder. + */ + +class FolioApplicationFolder : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QList appPreviews READ appPreviews NOTIFY applicationsChanged) + Q_PROPERTY(ApplicationFolderModel *applications READ applications NOTIFY applicationsReset) + +public: + FolioApplicationFolder(QObject *parent = nullptr, QString name = QString{}); + + static FolioApplicationFolder *fromJson(QJsonObject &obj, QObject *parent); + QJsonObject toJson(); + + QString name() const; + void setName(QString &name); + + QList appPreviews(); + + ApplicationFolderModel *applications(); + void setApplications(QList applications); + + void moveEntry(int fromRow, int toRow); + bool addDelegate(FolioDelegate *delegate, int row); + Q_INVOKABLE void removeDelegate(int row); + + int dropInsertPosition(int page, qreal x, qreal y); + bool isDropPositionOutside(qreal x, qreal y); + +Q_SIGNALS: + void nameChanged(); + void saveRequested(); + void applicationsChanged(); + void applicationsReset(); + +private: + QString m_name; + QList m_delegates; + ApplicationFolderModel *m_applicationFolderModel; + + friend class ApplicationFolderModel; +}; + +struct ApplicationDelegate { + FolioDelegate *delegate; + qreal xPosition; + qreal yPosition; +}; + +class ApplicationFolderModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int numberOfPages READ numTotalPages NOTIFY numberOfPagesChanged) + +public: + enum Roles { + DelegateRole = Qt::UserRole + 1, + XPositionRole, + YPositionRole, + }; + ApplicationFolderModel(FolioApplicationFolder *folder); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + FolioDelegate *getDelegate(int index); + void moveEntry(int fromRow, int toRow); + bool addDelegate(FolioDelegate *delegate, int index); + void removeDelegate(int index); + QPointF getDelegatePosition(int index); + + // for use with drag and drop, as the delegate is dragged around + // ghost - fake delegate exists at an index, so a gap is created + // invisible - existing delegate looks like it doesn't exist + int getGhostEntryPosition(); + void setGhostEntry(int index); + void replaceGhostEntry(FolioDelegate *delegate); + void deleteGhostEntry(); + + // the index that dropping at the position given would place the delegate at. + int dropInsertPosition(int page, qreal x, qreal y); + + // whether this position is outside of the folder area + bool isDropPositionOutside(qreal x, qreal y); + + // distance between page content to screen edge + qreal leftMarginFromScreenEdge(); + qreal topMarginFromScreenEdge(); + + int numTotalPages(); + +Q_SIGNALS: + void numberOfPagesChanged(); + +private: + void evaluateDelegatePositions(bool emitSignal = true); + + // get the position where delegates start being placed + QPointF getDelegateStartPosition(int page); + + int numRowsOnPage(); + int numColumnsOnPage(); + + // distance between folder edge and page content + qreal horizontalPageMargin(); + qreal verticalPageMargin(); + + FolioApplicationFolder *m_folder{nullptr}; + + friend class FolioApplicationFolder; +}; diff --git a/containments/homescreens/folio/foliodelegate.cpp b/containments/homescreens/folio/foliodelegate.cpp new file mode 100644 index 00000000..e2eda4ed --- /dev/null +++ b/containments/homescreens/folio/foliodelegate.cpp @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "foliodelegate.h" +#include "homescreenstate.h" + +FolioDelegate::FolioDelegate(QObject *parent) + : QObject{parent} + , m_type{FolioDelegate::None} + , m_application{nullptr} + , m_folder{nullptr} +{ +} + +FolioDelegate::FolioDelegate(FolioApplication *application, QObject *parent) + : QObject{parent} + , m_type{FolioDelegate::Application} + , m_application{application} + , m_folder{nullptr} +{ +} + +FolioDelegate::FolioDelegate(FolioApplicationFolder *folder, QObject *parent) + : QObject{parent} + , m_type{FolioDelegate::Folder} + , m_application{nullptr} + , m_folder{folder} +{ +} + +FolioDelegate *FolioDelegate::fromJson(QJsonObject &obj, QObject *parent) +{ + const QString type = obj[QStringLiteral("type")].toString(); + if (type == "application") { + // read application + FolioApplication *app = FolioApplication::fromJson(obj, parent); + + if (app) { + return new FolioDelegate{app, parent}; + } + + } else if (type == "folder") { + // read folder + FolioApplicationFolder *folder = FolioApplicationFolder::fromJson(obj, parent); + + if (folder) { + return new FolioDelegate{folder, parent}; + } + + } else if (type == "none") { + return new FolioDelegate{parent}; + } + + return nullptr; +} + +QJsonObject FolioDelegate::toJson() const +{ + switch (m_type) { + case FolioDelegate::Application: + return m_application->toJson(); + case FolioDelegate::Folder: + return m_folder->toJson(); + case FolioDelegate::None: { + QJsonObject obj; + obj[QStringLiteral("type")] = "none"; + return obj; + } + default: + break; + } + return QJsonObject{}; +} + +FolioDelegate::Type FolioDelegate::type() +{ + return m_type; +} + +FolioApplication *FolioDelegate::application() +{ + return m_application; +} + +FolioApplicationFolder *FolioDelegate::folder() +{ + return m_folder; +} diff --git a/containments/homescreens/folio/foliodelegate.h b/containments/homescreens/folio/foliodelegate.h new file mode 100644 index 00000000..58a1b4cd --- /dev/null +++ b/containments/homescreens/folio/foliodelegate.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "folioapplication.h" +#include "folioapplicationfolder.h" + +class FolioApplication; +class FolioApplicationFolder; +class FolioDelegate : public QObject +{ + Q_OBJECT + Q_PROPERTY(FolioDelegate::Type type READ type CONSTANT) + Q_PROPERTY(FolioApplication *application READ application CONSTANT) + Q_PROPERTY(FolioApplicationFolder *folder READ folder CONSTANT) + +public: + enum Type { + None, + Application, + Folder, + }; + Q_ENUM(Type) + + FolioDelegate(QObject *parent = nullptr); + FolioDelegate(FolioApplication *application, QObject *parent); + FolioDelegate(FolioApplicationFolder *folder, QObject *parent); + + static FolioDelegate *fromJson(QJsonObject &obj, QObject *parent); + + virtual QJsonObject toJson() const; + + FolioDelegate::Type type(); + FolioApplication *application(); + FolioApplicationFolder *folder(); + +protected: + FolioDelegate::Type m_type; + FolioApplication *m_application{nullptr}; + FolioApplicationFolder *m_folder{nullptr}; +}; diff --git a/containments/homescreens/folio/foliosettings.cpp b/containments/homescreens/folio/foliosettings.cpp new file mode 100644 index 00000000..d3c2aa5d --- /dev/null +++ b/containments/homescreens/folio/foliosettings.cpp @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "foliosettings.h" + +FolioSettings::FolioSettings(QObject *parent) + : QObject{parent} +{ +} + +FolioSettings *FolioSettings::self() +{ + static FolioSettings *settings = new FolioSettings; + return settings; +} + +int FolioSettings::homeScreenRows() const +{ + // ensure that this is fetched fast and cached (it is called extremely often) + return m_homeScreenRows; +} + +void FolioSettings::setHomeScreenRows(int homeScreenRows) +{ + if (m_homeScreenRows != homeScreenRows) { + m_homeScreenRows = homeScreenRows; + Q_EMIT homeScreenRowsChanged(); + save(); + } +} + +int FolioSettings::homeScreenColumns() const +{ + return m_homeScreenColumns; +} + +void FolioSettings::setHomeScreenColumns(int homeScreenColumns) +{ + if (m_homeScreenColumns != homeScreenColumns) { + m_homeScreenColumns = homeScreenColumns; + Q_EMIT homeScreenColumnsChanged(); + save(); + } +} + +bool FolioSettings::showPagesAppLabels() const +{ + return m_showPagesAppLabels; +} + +void FolioSettings::setShowPagesAppLabels(bool showPagesAppLabels) +{ + if (m_showPagesAppLabels != showPagesAppLabels) { + m_showPagesAppLabels = showPagesAppLabels; + Q_EMIT showPagesAppLabelsChanged(); + save(); + } +} + +bool FolioSettings::showFavouritesAppLabels() const +{ + return m_showFavouritesAppLabels; +} + +void FolioSettings::setShowFavouritesAppLabels(bool showFavouritesAppLabels) +{ + if (m_showFavouritesAppLabels != showFavouritesAppLabels) { + m_showFavouritesAppLabels = showFavouritesAppLabels; + Q_EMIT showFavouritesAppLabelsChanged(); + save(); + } +} + +int FolioSettings::delegateIconSize() const +{ + return m_delegateIconSize; +} + +void FolioSettings::setDelegateIconSize(int delegateIconSize) +{ + if (m_delegateIconSize != delegateIconSize) { + m_delegateIconSize = delegateIconSize; + Q_EMIT delegateIconSizeChanged(); + save(); + } +} + +bool FolioSettings::showFavouritesBarBackground() const +{ + return m_showFavouritesBarBackground; +} + +void FolioSettings::setShowFavouritesBarBackground(bool showFavouritesBarBackground) +{ + if (m_showFavouritesBarBackground != showFavouritesBarBackground) { + m_showFavouritesBarBackground = showFavouritesBarBackground; + Q_EMIT showFavouritesBarBackgroundChanged(); + save(); + } +} + +void FolioSettings::setApplet(Plasma::Applet *applet) +{ + m_applet = applet; +} + +void FolioSettings::save() +{ + if (!m_applet) { + return; + } + + m_applet->config().writeEntry("homeScreenRows", m_homeScreenRows); + m_applet->config().writeEntry("homeScreenColumns", m_homeScreenColumns); + m_applet->config().writeEntry("showPagesAppLabels", m_showPagesAppLabels); + m_applet->config().writeEntry("showFavouritesAppLabels", m_showFavouritesAppLabels); + m_applet->config().writeEntry("delegateIconSize", m_delegateIconSize); + m_applet->config().writeEntry("showFavouritesBarBackground", m_showFavouritesBarBackground); + + Q_EMIT m_applet->configNeedsSaving(); +} + +void FolioSettings::load() +{ + if (!m_applet) { + return; + } + + m_homeScreenRows = m_applet->config().readEntry("homeScreenRows", 5); + m_homeScreenColumns = m_applet->config().readEntry("homeScreenColumns", 4); + m_showPagesAppLabels = m_applet->config().readEntry("showPagesAppLabels", true); + m_showFavouritesAppLabels = m_applet->config().readEntry("showFavoritesAppLabels", false); + m_delegateIconSize = m_applet->config().readEntry("delegateIconSize", 48); + m_showFavouritesBarBackground = m_applet->config().readEntry("showFavoritesBarBackground", true); + + Q_EMIT homeScreenRowsChanged(); + Q_EMIT homeScreenColumnsChanged(); + Q_EMIT showPagesAppLabels(); + Q_EMIT showFavouritesAppLabelsChanged(); + Q_EMIT delegateIconSizeChanged(); +} diff --git a/containments/homescreens/folio/foliosettings.h b/containments/homescreens/folio/foliosettings.h new file mode 100644 index 00000000..fb036e91 --- /dev/null +++ b/containments/homescreens/folio/foliosettings.h @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +class FolioSettings : public QObject +{ + Q_OBJECT + Q_PROPERTY(int homeScreenRows READ homeScreenRows WRITE setHomeScreenRows NOTIFY homeScreenRowsChanged) + Q_PROPERTY(int homeScreenColumns READ homeScreenColumns WRITE setHomeScreenColumns NOTIFY homeScreenColumnsChanged) + Q_PROPERTY(bool showPagesAppLabels READ showPagesAppLabels WRITE setShowPagesAppLabels NOTIFY showPagesAppLabelsChanged) + Q_PROPERTY(bool showFavouritesAppLabels READ showFavouritesAppLabels WRITE setShowFavouritesAppLabels NOTIFY showFavouritesAppLabelsChanged) + Q_PROPERTY(int delegateIconSize READ delegateIconSize WRITE setDelegateIconSize NOTIFY delegateIconSizeChanged) + Q_PROPERTY(bool showFavouritesBarBackground READ showFavouritesBarBackground WRITE setShowFavouritesBarBackground NOTIFY showFavouritesBarBackgroundChanged) + +public: + FolioSettings(QObject *parent = nullptr); + + static FolioSettings *self(); + + // number of rows and columns in the config for the homescreen + // NOTE: use HomeScreenState.pageRows() instead in UI logic since we may have the rows and + // columns swapped (in landscape layouts) + int homeScreenRows() const; + void setHomeScreenRows(int homeScreenRows); + + int homeScreenColumns() const; + void setHomeScreenColumns(int homeScreenColumns); + + bool showPagesAppLabels() const; + void setShowPagesAppLabels(bool showPagesAppLabels); + + bool showFavouritesAppLabels() const; + void setShowFavouritesAppLabels(bool showFavouritesAppLabels); + + int delegateIconSize() const; + void setDelegateIconSize(int delegateIconSize); + + bool showFavouritesBarBackground() const; + void setShowFavouritesBarBackground(bool showFavouritesBarBackground); + + Q_INVOKABLE void load(); + + Q_INVOKABLE void setApplet(Plasma::Applet *applet); + +Q_SIGNALS: + void homeScreenRowsChanged(); + void homeScreenColumnsChanged(); + void showPagesAppLabelsChanged(); + void showFavouritesAppLabelsChanged(); + void delegateIconSizeChanged(); + void showFavouritesBarBackgroundChanged(); + +private: + void save(); + + int m_homeScreenRows{5}; + int m_homeScreenColumns{4}; + bool m_showPagesAppLabels{false}; + bool m_showFavouritesAppLabels{false}; + qreal m_delegateIconSize{48}; + bool m_showFavouritesBarBackground{false}; + + Plasma::Applet *m_applet{nullptr}; +}; diff --git a/containments/homescreens/folio/homescreen.cpp b/containments/homescreens/folio/homescreen.cpp index c315eae0..70515984 100644 --- a/containments/homescreens/folio/homescreen.cpp +++ b/containments/homescreens/folio/homescreen.cpp @@ -1,12 +1,25 @@ // SPDX-FileCopyrightText: 2015 Marco Martin -// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-FileCopyrightText: 2022-2023 Devin Lin // SPDX-License-Identifier: GPL-2.0-or-later #include "homescreen.h" +#include "applicationlistmodel.h" +#include "delegatetoucharea.h" +#include "favouritesmodel.h" +#include "folioapplication.h" +#include "folioapplicationfolder.h" +#include "foliodelegate.h" +#include "foliosettings.h" +#include "homescreenstate.h" +#include "pagelistmodel.h" +#include "pagemodel.h" + #include #include +#include +#include #include #include @@ -14,6 +27,45 @@ HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVari : Plasma::Containment{parent, data, args} { setHasConfigurationInterface(true); + const char *uri = "org.kde.private.mobile.homescreen.folio"; + + // pre-initialize + FolioSettings::self()->setApplet(this); + HomeScreenState::self(); + + // models are loaded in main.qml + ApplicationListModel::self(); + FavouritesModel::self()->setApplet(this); + PageListModel::self()->setApplet(this); + + qmlRegisterSingletonType(uri, 1, 0, "ApplicationListModel", [](QQmlEngine *, QJSEngine *) -> QObject * { + return ApplicationListModel::self(); + }); + + qmlRegisterSingletonType(uri, 1, 0, "FavouritesModel", [](QQmlEngine *, QJSEngine *) -> QObject * { + return FavouritesModel::self(); + }); + + qmlRegisterSingletonType(uri, 1, 0, "PageListModel", [](QQmlEngine *, QJSEngine *) -> QObject * { + return PageListModel::self(); + }); + + qmlRegisterSingletonType(uri, 1, 0, "FolioSettings", [](QQmlEngine *, QJSEngine *) -> QObject * { + return FolioSettings::self(); + }); + + qmlRegisterSingletonType(uri, 1, 0, "HomeScreenState", [](QQmlEngine *, QJSEngine *) -> QObject * { + return HomeScreenState::self(); + }); + + qmlRegisterType(uri, 1, 0, "FolioApplication"); + qmlRegisterType(uri, 1, 0, "FolioApplicationFolder"); + qmlRegisterType(uri, 1, 0, "FolioDelegate"); + qmlRegisterType(uri, 1, 0, "PageModel"); + qmlRegisterType(uri, 1, 0, "FolioPageDelegate"); + qmlRegisterType(uri, 1, 0, "DelegateTouchArea"); + qmlRegisterType(uri, 1, 0, "DelegateDragPosition"); + connect(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged, this, &HomeScreen::showingDesktopChanged); } diff --git a/containments/homescreens/folio/homescreenstate.cpp b/containments/homescreens/folio/homescreenstate.cpp new file mode 100644 index 00000000..dde9802d --- /dev/null +++ b/containments/homescreens/folio/homescreenstate.cpp @@ -0,0 +1,1027 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "homescreenstate.h" +#include "favouritesmodel.h" +#include "foliosettings.h" +#include "pagelistmodel.h" + +#include + +// TODO don't hardcode, use something more device dependent? +constexpr qreal APP_DRAWER_OPEN_DIST = 300; +constexpr qreal SEARCH_WIDGET_OPEN_DIST = 300; + +// pixels to move before we determine the swipe type +constexpr qreal DETERMINE_SWIPE_THRESHOLD = 10; + +constexpr qreal VERTICAL_FAVOURITES_BAR_THRESHOLD = 400; + +HomeScreenState *HomeScreenState::self() +{ + static HomeScreenState *inst = new HomeScreenState{nullptr}; + return inst; +} + +QPropertyAnimation *HomeScreenState::setupAnimation(QByteArray property, int duration, QEasingCurve::Type curve, qreal endValue) +{ + auto anim = new QPropertyAnimation{this, property, this}; + anim->setDuration(duration); + anim->setEndValue(endValue); + anim->setEasingCurve(curve); + return anim; +} + +HomeScreenState::HomeScreenState(QObject *parent) + : QObject{parent} + , m_dragState{new DragState{this, this}} + , m_appDrawerY{APP_DRAWER_OPEN_DIST} + , m_searchWidgetY{SEARCH_WIDGET_OPEN_DIST} +{ + const int expoDuration = 800; + const int cubicDuration = 400; + + m_openAppDrawerAnim = setupAnimation("appDrawerY", expoDuration, QEasingCurve::OutExpo, 0); + + connect(m_openAppDrawerAnim, &QPropertyAnimation::valueChanged, this, [this]() { + // the animation runs too long to connect to QPropertyAnimation::finished + // instead just have the end behaviour execute once we are 90% through + if (m_appDrawerOpenProgress > 0.9) { + setViewState(ViewState::AppDrawerView); + Q_EMIT appDrawerOpened(); + } + }); + + m_closeAppDrawerAnim = setupAnimation("appDrawerY", expoDuration, QEasingCurve::OutExpo, APP_DRAWER_OPEN_DIST); + + connect(m_closeAppDrawerAnim, &QPropertyAnimation::valueChanged, this, [this]() { + // the animation runs too long to connect to QPropertyAnimation::finished + // instead just have the end behaviour execute once we are 90% through + if (m_appDrawerOpenProgress < 0.1) { + setViewState(ViewState::PageView); + Q_EMIT appDrawerClosed(); + } + }); + + m_openSearchWidgetAnim = setupAnimation("searchWidgetY", cubicDuration, QEasingCurve::OutCubic, 0); + + connect(m_openSearchWidgetAnim, &QPropertyAnimation::finished, this, [this]() { + setViewState(ViewState::SearchWidgetView); + }); + + m_closeSearchWidgetAnim = setupAnimation("searchWidgetY", cubicDuration, QEasingCurve::OutCubic, SEARCH_WIDGET_OPEN_DIST); + + connect(m_closeSearchWidgetAnim, &QPropertyAnimation::finished, this, [this]() { + setViewState(ViewState::PageView); + }); + + m_pageAnim = setupAnimation("pageViewX", cubicDuration, QEasingCurve::OutCubic, 0); + + m_openFolderAnim = setupAnimation("folderOpenProgress", cubicDuration, QEasingCurve::OutCubic, 1.0); + + connect(m_openFolderAnim, &QPropertyAnimation::finished, this, [this]() { + setViewState(ViewState::FolderView); + }); + + m_closeFolderAnim = setupAnimation("folderOpenProgress", cubicDuration, QEasingCurve::OutCubic, 0.0); + + connect(m_closeFolderAnim, &QPropertyAnimation::finished, this, [this]() { + setViewState(ViewState::PageView); + setCurrentFolder(nullptr); + setFolderViewX(0); // reset to first page + m_folderPageNum = 0; + Q_EMIT folderPageNumChanged(); + + Q_EMIT leftCurrentFolder(); + }); + + m_folderPageAnim = setupAnimation("folderViewX", cubicDuration, QEasingCurve::OutCubic, 0); + + m_openSettingsAnim = setupAnimation("settingsOpenProgress", cubicDuration, QEasingCurve::OutExpo, 1.0); + + connect(m_openSettingsAnim, &QPropertyAnimation::finished, this, [this]() { + setViewState(ViewState::SettingsView); + }); + + m_closeSettingsAnim = setupAnimation("settingsOpenProgress", cubicDuration, QEasingCurve::InOutExpo, 0.0); + + connect(m_closeSettingsAnim, &QPropertyAnimation::finished, this, [this]() { + setViewState(ViewState::PageView); + }); + + connect(this, &HomeScreenState::viewWidthChanged, this, [this]() { + // TODO: we only support 2 orientations at the moment since we don't know what the device rotated to + setPageOrientation(m_viewWidth > m_viewHeight ? RotateCounterClockwise : RegularPosition); + }); + connect(this, &HomeScreenState::viewHeightChanged, this, [this]() { + setPageOrientation(m_viewWidth > m_viewHeight ? RotateCounterClockwise : RegularPosition); + }); + connect(this, &HomeScreenState::pageOrientationChanged, this, [this]() { + Q_EMIT pageRowsChanged(); + Q_EMIT pageColumnsChanged(); + }); + + connect(this, &HomeScreenState::pageWidthChanged, this, &HomeScreenState::calculatePageContentWidth); + connect(this, &HomeScreenState::pageHeightChanged, this, &HomeScreenState::calculatePageContentHeight); + + connect(this, &HomeScreenState::pageContentWidthChanged, this, &HomeScreenState::calculatePageCellWidth); + connect(this, &HomeScreenState::pageColumnsChanged, this, &HomeScreenState::calculatePageCellWidth); + connect(this, &HomeScreenState::pageContentHeightChanged, this, &HomeScreenState::calculatePageCellHeight); + connect(this, &HomeScreenState::pageRowsChanged, this, &HomeScreenState::calculatePageCellHeight); + + connect(this, &HomeScreenState::viewWidthChanged, this, [this]() { + Q_EMIT favouritesBarLocationChanged(); + }); + connect(this, &HomeScreenState::viewHeightChanged, this, [this]() { + Q_EMIT favouritesBarLocationChanged(); + }); + + connect(FolioSettings::self(), &FolioSettings::homeScreenRowsChanged, this, [this]() { + Q_EMIT pageRowsChanged(); + Q_EMIT pageColumnsChanged(); + }); + connect(FolioSettings::self(), &FolioSettings::homeScreenColumnsChanged, this, [this]() { + Q_EMIT pageRowsChanged(); + Q_EMIT pageColumnsChanged(); + }); +} + +HomeScreenState::ViewState HomeScreenState::viewState() const +{ + return m_viewState; +} + +void HomeScreenState::setViewState(ViewState viewState) +{ + if (viewState != m_viewState) { + m_viewState = viewState; + Q_EMIT viewStateChanged(); + } +} + +HomeScreenState::SwipeState HomeScreenState::swipeState() const +{ + return m_swipeState; +} + +void HomeScreenState::setSwipeState(SwipeState swipeState) +{ + if (swipeState != m_swipeState) { + m_swipeState = swipeState; + Q_EMIT swipeStateChanged(); + } +} + +DragState *HomeScreenState::dragState() const +{ + return m_dragState; +} + +qreal HomeScreenState::viewWidth() const +{ + return m_viewWidth; +} + +void HomeScreenState::setViewWidth(qreal viewWidth) +{ + if (m_viewWidth != viewWidth) { + m_viewWidth = viewWidth; + Q_EMIT viewWidthChanged(); + } +} + +qreal HomeScreenState::viewHeight() const +{ + return m_viewHeight; +} + +void HomeScreenState::setViewHeight(qreal viewHeight) +{ + if (m_viewHeight != viewHeight) { + m_viewHeight = viewHeight; + Q_EMIT viewHeightChanged(); + } +} + +qreal HomeScreenState::viewTopPadding() const +{ + return m_viewTopPadding; +} + +void HomeScreenState::setViewTopPadding(qreal viewTopPadding) +{ + if (m_viewTopPadding != viewTopPadding) { + m_viewTopPadding = viewTopPadding; + Q_EMIT viewTopPaddingChanged(); + } +} + +qreal HomeScreenState::viewBottomPadding() const +{ + return m_viewBottomPadding; +} + +void HomeScreenState::setViewBottomPadding(qreal viewBottomPadding) +{ + if (m_viewBottomPadding != viewBottomPadding) { + m_viewBottomPadding = viewBottomPadding; + Q_EMIT viewBottomPaddingChanged(); + } +} + +qreal HomeScreenState::viewLeftPadding() const +{ + return m_viewLeftPadding; +} + +void HomeScreenState::setViewLeftPadding(qreal viewLeftPadding) +{ + if (m_viewLeftPadding != viewLeftPadding) { + m_viewLeftPadding = viewLeftPadding; + Q_EMIT viewLeftPaddingChanged(); + } +} + +qreal HomeScreenState::viewRightPadding() const +{ + return m_viewRightPadding; +} + +void HomeScreenState::setViewRightPadding(qreal viewRightPadding) +{ + if (m_viewRightPadding != viewRightPadding) { + m_viewRightPadding = viewRightPadding; + Q_EMIT viewRightPaddingChanged(); + } +} + +HomeScreenState::PageOrientation HomeScreenState::pageOrientation() const +{ + return m_pageOrientation; +} + +void HomeScreenState::setPageOrientation(PageOrientation pageOrientation) +{ + if (m_pageOrientation != pageOrientation) { + m_pageOrientation = pageOrientation; + Q_EMIT pageOrientationChanged(); + } +} + +HomeScreenState::FavouritesBarLocation HomeScreenState::favouritesBarLocation() const +{ + // TODO need to determine screen rotation and bottom of screen to have Left and Right accordingly + return m_viewHeight < VERTICAL_FAVOURITES_BAR_THRESHOLD && m_viewWidth > m_viewHeight ? Right : Bottom; +} + +int HomeScreenState::pageRows() const +{ + if (m_pageOrientation == RegularPosition || m_pageOrientation == RotateUpsideDown) { + return FolioSettings::self()->homeScreenRows(); + } else { + return FolioSettings::self()->homeScreenColumns(); + } +} + +int HomeScreenState::pageColumns() const +{ + if (m_pageOrientation == RegularPosition || m_pageOrientation == RotateUpsideDown) { + return FolioSettings::self()->homeScreenColumns(); + } else { + return FolioSettings::self()->homeScreenRows(); + } +} + +qreal HomeScreenState::pageViewX() const +{ + return m_pageViewX; +} + +void HomeScreenState::setPageViewX(qreal pageViewX) +{ + if (m_pageViewX != pageViewX) { + m_pageViewX = pageViewX; + Q_EMIT pageViewXChanged(); + } +} + +qreal HomeScreenState::pageWidth() const +{ + return m_pageWidth; +} + +void HomeScreenState::setPageWidth(qreal pageWidth) +{ + if (m_pageWidth != pageWidth) { + m_pageWidth = pageWidth; + Q_EMIT pageWidthChanged(); + + // make sure we snap + snapPage(); + } +} + +qreal HomeScreenState::pageHeight() const +{ + return m_pageHeight; +} + +void HomeScreenState::setPageHeight(qreal pageHeight) +{ + if (m_pageHeight != pageHeight) { + m_pageHeight = pageHeight; + Q_EMIT pageHeightChanged(); + } +} + +qreal HomeScreenState::pageContentWidth() const +{ + return m_pageContentWidth; +} + +void HomeScreenState::calculatePageContentWidth() +{ + const qreal pageContentWidth = std::round(m_pageWidth * 0.95); // 0.05 on both sides + + if (m_pageContentWidth != pageContentWidth) { + m_pageContentWidth = pageContentWidth; + Q_EMIT pageContentWidthChanged(); + } +} + +qreal HomeScreenState::pageContentHeight() const +{ + return m_pageContentHeight; +} + +void HomeScreenState::calculatePageContentHeight() +{ + const qreal pageContentHeight = std::round(m_pageHeight * 0.95); // 0.05 on both sides + + if (m_pageContentHeight != pageContentHeight) { + m_pageContentHeight = pageContentHeight; + Q_EMIT pageContentHeightChanged(); + } +} + +qreal HomeScreenState::pageCellWidth() const +{ + return m_pageCellWidth; +} + +void HomeScreenState::calculatePageCellWidth() +{ + qreal pageCellWidth = (pageColumns() == 0) ? 0 : qMax(0.0, std::round(m_pageContentWidth / pageColumns())); + + if (m_pageCellWidth != pageCellWidth) { + m_pageCellWidth = pageCellWidth; + Q_EMIT pageCellWidthChanged(); + } +} + +qreal HomeScreenState::pageCellHeight() const +{ + return m_pageCellHeight; +} + +void HomeScreenState::calculatePageCellHeight() +{ + qreal pageCellHeight = (pageRows() == 0) ? 0 : std::round(m_pageContentHeight / pageRows()); + + if (m_pageCellHeight != pageCellHeight) { + m_pageCellHeight = pageCellHeight; + Q_EMIT pageCellHeightChanged(); + } +} + +qreal HomeScreenState::pageDelegateLabelHeight() const +{ + return m_pageDelegateLabelHeight; +} + +void HomeScreenState::setPageDelegateLabelHeight(qreal pageDelegateLabelHeight) +{ + if (m_pageDelegateLabelHeight != pageDelegateLabelHeight) { + m_pageDelegateLabelHeight = pageDelegateLabelHeight; + Q_EMIT pageDelegateLabelHeightChanged(); + } +} + +qreal HomeScreenState::pageDelegateLabelSpacing() const +{ + return m_pageDelegateLabelSpacing; +} + +void HomeScreenState::setPageDelegateLabelSpacing(qreal pageDelegateLabelSpacing) +{ + if (m_pageDelegateLabelSpacing != pageDelegateLabelSpacing) { + m_pageDelegateLabelSpacing = pageDelegateLabelSpacing; + Q_EMIT pageDelegateLabelSpacingChanged(); + } +} + +qreal HomeScreenState::folderViewX() const +{ + return m_folderViewX; +} + +void HomeScreenState::setFolderViewX(qreal folderViewX) +{ + if (m_folderViewX != folderViewX) { + m_folderViewX = folderViewX; + Q_EMIT folderViewXChanged(); + } +} + +qreal HomeScreenState::folderPageWidth() const +{ + return m_folderPageWidth; +} + +void HomeScreenState::setFolderPageWidth(qreal folderPageWidth) +{ + if (m_folderPageWidth != folderPageWidth) { + m_folderPageWidth = folderPageWidth; + Q_EMIT folderPageWidthChanged(); + } +} + +qreal HomeScreenState::folderPageHeight() const +{ + return m_folderPageHeight; +} + +void HomeScreenState::setFolderPageHeight(qreal folderPageHeight) +{ + if (m_folderPageHeight != folderPageHeight) { + m_folderPageHeight = folderPageHeight; + Q_EMIT folderPageHeightChanged(); + } +} + +qreal HomeScreenState::folderPageContentWidth() const +{ + return m_folderPageContentWidth; +} + +void HomeScreenState::setFolderPageContentWidth(qreal folderPageContentWidth) +{ + if (m_folderPageContentWidth != folderPageContentWidth) { + m_folderPageContentWidth = folderPageContentWidth; + Q_EMIT folderPageContentWidthChanged(); + } +} + +qreal HomeScreenState::folderPageContentHeight() const +{ + return m_folderPageContentHeight; +} + +void HomeScreenState::setFolderPageContentHeight(qreal folderPageContentHeight) +{ + if (m_folderPageContentHeight != folderPageContentHeight) { + m_folderPageContentHeight = folderPageContentHeight; + Q_EMIT folderPageContentHeightChanged(); + } +} + +qreal HomeScreenState::folderOpenProgress() const +{ + return m_folderOpenProgress; +} + +void HomeScreenState::setFolderOpenProgress(qreal folderOpenProgress) +{ + if (m_folderOpenProgress != folderOpenProgress) { + m_folderOpenProgress = folderOpenProgress; + Q_EMIT folderOpenProgressChanged(); + } +} + +FolioApplicationFolder *HomeScreenState::currentFolder() const +{ + return m_currentFolder; +} + +void HomeScreenState::setCurrentFolder(FolioApplicationFolder *folder) +{ + if (m_currentFolder != folder) { + m_currentFolder = folder; + Q_EMIT currentFolderChanged(); + } +} + +qreal HomeScreenState::settingsOpenProgress() +{ + return m_settingsOpenProgress; +} + +void HomeScreenState::setSettingsOpenProgress(qreal settingsOpenProgress) +{ + if (m_settingsOpenProgress != settingsOpenProgress) { + m_settingsOpenProgress = settingsOpenProgress; + Q_EMIT settingsOpenProgressChanged(); + } +} + +qreal HomeScreenState::appDrawerOpenProgress() +{ + return m_appDrawerOpenProgress; +} + +qreal HomeScreenState::appDrawerY() +{ + return m_appDrawerY; +} + +void HomeScreenState::setAppDrawerY(qreal appDrawerY) +{ + m_appDrawerY = appDrawerY; + m_appDrawerOpenProgress = 1 - qBound(0.0, m_appDrawerY, APP_DRAWER_OPEN_DIST) / APP_DRAWER_OPEN_DIST; + Q_EMIT appDrawerYChanged(); + Q_EMIT appDrawerOpenProgressChanged(); +} + +qreal HomeScreenState::searchWidgetOpenProgress() +{ + return m_searchWidgetOpenProgress; +} + +qreal HomeScreenState::searchWidgetY() +{ + return m_searchWidgetOpenProgress; +} + +void HomeScreenState::setSearchWidgetY(qreal searchWidgetY) +{ + m_searchWidgetY = searchWidgetY; + m_searchWidgetOpenProgress = 1 - qBound(0.0, m_searchWidgetY, SEARCH_WIDGET_OPEN_DIST) / SEARCH_WIDGET_OPEN_DIST; + Q_EMIT searchWidgetYChanged(); + Q_EMIT searchWidgetOpenProgressChanged(); +} + +qreal HomeScreenState::delegateDragX() +{ + return m_delegateDragX; +} + +void HomeScreenState::setDelegateDragX(qreal delegateDragX) +{ + m_delegateDragX = delegateDragX; + Q_EMIT delegateDragXChanged(); +} + +qreal HomeScreenState::delegateDragY() +{ + return m_delegateDragY; +} + +void HomeScreenState::setDelegateDragY(qreal delegateDragY) +{ + m_delegateDragY = delegateDragY; + Q_EMIT delegateDragYChanged(); +} + +int HomeScreenState::currentPage() +{ + return m_pageNum; +} + +void HomeScreenState::setCurrentPage(int currentPage) +{ + if (m_pageNum != currentPage) { + m_pageNum = currentPage; + Q_EMIT pageNumChanged(); + } +} + +int HomeScreenState::currentFolderPage() +{ + return m_folderPageNum; +} + +FolioDelegate *HomeScreenState::getPageDelegateAt(int page, int row, int column) +{ + PageModel *pageModel = PageListModel::self()->getPage(page); + if (!pageModel) { + return nullptr; + } + + FolioDelegate *delegate = pageModel->getDelegate(row, column); + if (!delegate) { + return nullptr; + } + + return delegate; +} + +FolioDelegate *HomeScreenState::getFavouritesDelegateAt(int position) +{ + return FavouritesModel::self()->getEntryAt(position); +} + +FolioDelegate *HomeScreenState::getFolderDelegateAt(int position) +{ + if (!m_currentFolder) { + return nullptr; + } + + return m_currentFolder->applications()->getDelegate(position); +} + +QPointF HomeScreenState::getPageDelegateScreenPosition(int page, int row, int column) +{ + Q_UNUSED(page) + qreal x = m_viewLeftPadding + ((m_pageWidth - m_pageContentWidth) / 2) + (m_pageCellWidth * column); + qreal y = m_viewTopPadding + ((m_pageHeight - m_pageContentHeight) / 2) + (m_pageCellHeight * row); + return QPointF{x, y}; +} + +QPointF HomeScreenState::getFavouritesDelegateScreenPosition(int position) +{ + return FavouritesModel::self()->getDelegateScreenPosition(position); +} + +QPointF HomeScreenState::getFolderDelegateScreenPosition(int position) +{ + if (!m_currentFolder) { + return {0, 0}; + } + auto pos = m_currentFolder->applications()->getDelegatePosition(position); + qreal x = pos.x() + (m_viewWidth - m_viewLeftPadding - m_viewRightPadding - m_folderPageWidth) / 2; + qreal y = pos.y() + (m_viewHeight - m_viewTopPadding - m_viewBottomPadding - m_folderPageHeight) / 2; + x += m_viewLeftPadding; + y += m_viewTopPadding; + + // adjust for the current page + x -= currentFolderPage() * m_folderPageWidth; + + return {x, y}; +} + +void HomeScreenState::openAppDrawer() +{ + cancelAppDrawerAnimations(); + m_openAppDrawerAnim->setStartValue(m_appDrawerY); + m_openAppDrawerAnim->start(); +} + +void HomeScreenState::closeAppDrawer() +{ + cancelAppDrawerAnimations(); + m_closeAppDrawerAnim->setStartValue(m_appDrawerY); + m_closeAppDrawerAnim->start(); +} + +void HomeScreenState::openSearchWidget() +{ + cancelSearchWidgetAnimations(); + m_openSearchWidgetAnim->setStartValue(m_searchWidgetY); + m_openSearchWidgetAnim->start(); +} + +void HomeScreenState::closeSearchWidget() +{ + cancelSearchWidgetAnimations(); + m_closeSearchWidgetAnim->setStartValue(m_searchWidgetY); + m_closeSearchWidgetAnim->start(); +} + +void HomeScreenState::snapPage() +{ + const int numOfPages = PageListModel::self()->rowCount(); + + const int leftPage = qBound(0.0, (m_pageViewX / m_pageWidth), numOfPages - 1.0); + const qreal leftPagePos = -leftPage * m_pageWidth; + + if (leftPage == numOfPages + 1) { + // if we are past the last page + goToPage(leftPage); + } else { + const qreal rightPagePos = leftPagePos - m_pageWidth; + + // go to the closer page (right or left) + if (qAbs(rightPagePos - m_pageViewX) < qAbs(leftPagePos - m_pageViewX)) { + goToPage(leftPage + 1); + } else { + goToPage(leftPage); + } + } +} + +void HomeScreenState::goToPage(int page) +{ + if (page < 0) { + page = 0; + } + + const int numOfPages = PageListModel::self()->rowCount(); + if (page >= numOfPages) { + page = std::max(0, numOfPages - 1); + } + + setCurrentPage(page); + + m_pageAnim->setStartValue(m_pageViewX); + m_pageAnim->setEndValue(-page * m_pageWidth); + m_pageAnim->start(); +} + +void HomeScreenState::goToFolderPage(int page) +{ + if (!m_currentFolder) { + return; + } + + if (page < 0) { + page = 0; + } + + int numOfPages = m_currentFolder->applications()->numTotalPages(); + if (page >= numOfPages) { + page = std::max(0, numOfPages - 1); + } + + m_folderPageNum = page; + Q_EMIT folderPageNumChanged(); + + m_folderPageAnim->setStartValue(m_folderViewX); + m_folderPageAnim->setEndValue(-page * m_folderPageWidth); + m_folderPageAnim->start(); +} + +void HomeScreenState::openFolder(qreal delegateX, qreal delegateY, FolioApplicationFolder *folder) +{ + setCurrentFolder(folder); + + m_openFolderAnim->stop(); + m_closeFolderAnim->stop(); + m_openFolderAnim->setStartValue(m_folderOpenProgress); + m_openFolderAnim->start(); + + Q_EMIT folderAboutToOpen(delegateX, delegateY); +} + +void HomeScreenState::closeFolder() +{ + m_openFolderAnim->stop(); + m_closeFolderAnim->stop(); + m_closeFolderAnim->setStartValue(m_folderOpenProgress); + m_closeFolderAnim->start(); +} + +void HomeScreenState::openSettingsView() +{ + m_closeSettingsAnim->stop(); + m_openSettingsAnim->stop(); + m_openSettingsAnim->setStartValue(m_settingsOpenProgress); + m_openSettingsAnim->start(); +} + +void HomeScreenState::closeSettingsView() +{ + m_openSettingsAnim->stop(); + m_closeSettingsAnim->stop(); + m_closeSettingsAnim->setStartValue(m_settingsOpenProgress); + m_closeSettingsAnim->start(); +} + +void HomeScreenState::startDelegateDrag(qreal startX, qreal startY) +{ + // start drag and drop positions + setDelegateDragX(startX); + setDelegateDragY(startY); + + // end current swipe + swipeEnded(); + + // start the delegate drag + setSwipeState(SwipeState::AwaitingDraggingDelegate); +} + +void HomeScreenState::startDelegatePageDrag(qreal startX, qreal startY, int page, int row, int column) +{ + startDelegateDrag(startX, startY); + Q_EMIT delegateDragFromPageStarted(page, row, column); +} + +void HomeScreenState::startDelegateFavouritesDrag(qreal startX, qreal startY, int position) +{ + startDelegateDrag(startX, startY); + Q_EMIT delegateDragFromFavouritesStarted(position); +} + +void HomeScreenState::startDelegateAppDrawerDrag(qreal startX, qreal startY, QString storageId) +{ + startDelegateDrag(startX, startY); + Q_EMIT delegateDragFromAppDrawerStarted(storageId); + + // we start dragging the delegate immediately from the app drawer, because we don't have a context menu to deal with! + // NOTE: this has to happen after delegateDragFromAppDrawerStarted, because slots for that expect SwipeState::AwaitingDraggingDelegate + setSwipeState(SwipeState::DraggingDelegate); +} + +void HomeScreenState::startDelegateFolderDrag(qreal startX, qreal startY, FolioApplicationFolder *folder, int position) +{ + startDelegateDrag(startX, startY); + Q_EMIT delegateDragFromFolderStarted(folder, position); +} + +void HomeScreenState::cancelDelegateDrag() +{ + swipeEnded(); +} + +void HomeScreenState::swipeStarted() +{ + if (m_swipeState != SwipeState::None) { + return; + } + + setSwipeState(SwipeState::DeterminingSwipeType); +} + +void HomeScreenState::swipeEnded() +{ + switch (m_swipeState) { + case SwipeState::SwipingOpenAppDrawer: + case SwipeState::SwipingCloseAppDrawer: + if (m_movingUp) { + closeAppDrawer(); + } else { + openAppDrawer(); + } + break; + case SwipeState::SwipingAppDrawerGrid: + Q_EMIT appDrawerGridFlickRequested(); + break; + case SwipeState::SwipingOpenSearchWidget: + case SwipeState::SwipingCloseSearchWidget: + if (m_movingUp) { + openSearchWidget(); + } else { + closeSearchWidget(); + } + break; + case SwipeState::SwipingPages: { + int page = std::max(0.0, -m_pageViewX) / m_pageWidth; + + // m_movingRight refers to finger movement + if (m_movingRight || m_pageViewX > 0) { + goToPage(page); + } else { + goToPage(page + 1); + } + break; + } + case SwipeState::SwipingFolderPages: { + int page = std::max(0.0, -m_folderViewX) / m_folderPageWidth; + + // m_movingRight refers to finger movement + if (m_movingRight || m_folderViewX > 0) { + goToFolderPage(page); + } else { + goToFolderPage(page + 1); + } + break; + } + case SwipeState::DraggingDelegate: + Q_EMIT delegateDragEnded(); + break; + case SwipeState::AwaitingDraggingDelegate: + case SwipeState::DeterminingSwipeType: + break; + default: + break; + } + + setSwipeState(SwipeState::None); +} + +void HomeScreenState::swipeMoved(qreal totalDeltaX, qreal totalDeltaY, qreal deltaX, qreal deltaY) +{ + m_movingUp = deltaY > 0; + + switch (m_swipeState) { + case SwipeState::DeterminingSwipeType: + // check if we can determine the type of swipe this is + determineSwipeTypeAfterThreshold(totalDeltaX, totalDeltaY); + break; + case SwipeState::SwipingOpenSearchWidget: + case SwipeState::SwipingCloseSearchWidget: + setSearchWidgetY(m_searchWidgetY - deltaY); + break; + case SwipeState::SwipingOpenAppDrawer: + case SwipeState::SwipingCloseAppDrawer: + setAppDrawerY(m_appDrawerY + deltaY); + break; + case SwipeState::SwipingAppDrawerGrid: + Q_EMIT appDrawerGridYChanged(deltaY); + break; + case SwipeState::SwipingPages: + m_movingRight = deltaX > 0; + setPageViewX(m_pageViewX + deltaX); + break; + case SwipeState::SwipingFolderPages: + m_movingRight = deltaX > 0; + setFolderViewX(m_folderViewX + deltaX); + break; + case SwipeState::AwaitingDraggingDelegate: + setSwipeState(SwipeState::DraggingDelegate); + break; + case SwipeState::DraggingDelegate: + setDelegateDragX(m_delegateDragX + deltaX); + setDelegateDragY(m_delegateDragY + deltaY); + break; + default: + break; + } +} + +void HomeScreenState::determineSwipeTypeAfterThreshold(qreal totalDeltaX, qreal totalDeltaY) +{ + // we check if the x or y movement has passed a certain threshold before determining the swipe type + + if (qAbs(totalDeltaX) >= DETERMINE_SWIPE_THRESHOLD && (m_viewState == ViewState::PageView || m_viewState == ViewState::SettingsView)) { + // select horizontal swipe mode (only if in page view) + setSwipeState(SwipeState::SwipingPages); + + // ensure no animations are running when starting a swipe + m_pageAnim->stop(); + + } else if (qAbs(totalDeltaX) >= DETERMINE_SWIPE_THRESHOLD && m_viewState == ViewState::FolderView) { + // select horizontal swipe mode (only if in page view) + setSwipeState(SwipeState::SwipingFolderPages); + + // ensure no animations are running when starting a swipe + m_folderPageAnim->stop(); + + } else if (qAbs(totalDeltaY) >= DETERMINE_SWIPE_THRESHOLD) { + // select vertical swipe mode + + if (m_movingUp) { + // moving up + switch (m_viewState) { + case ViewState::PageView: + // if the app drawer is still being opened + if (m_openAppDrawerAnim->state() == QPropertyAnimation::Running) { + setSwipeState(SwipeState::SwipingOpenAppDrawer); + cancelAppDrawerAnimations(); + } else { + setSwipeState(SwipeState::SwipingOpenSearchWidget); + cancelSearchWidgetAnimations(); + } + break; + case ViewState::AppDrawerView: + setSwipeState(SwipeState::SwipingCloseAppDrawer); + cancelAppDrawerAnimations(); + break; + case ViewState::SearchWidgetView: + setSwipeState(SwipeState::SwipingCloseSearchWidget); + cancelSearchWidgetAnimations(); + case ViewState::FolderView: + case ViewState::SettingsView: + // no vertical behaviour in folder or settings view + default: + break; + } + } else { + // moving down + switch (m_viewState) { + case ViewState::PageView: + if (m_openSearchWidgetAnim->state() == QPropertyAnimation::Running) { + setSwipeState(SwipeState::SwipingOpenSearchWidget); + cancelSearchWidgetAnimations(); + } else { + setSwipeState(SwipeState::SwipingOpenAppDrawer); + cancelAppDrawerAnimations(); + } + break; + case ViewState::SearchWidgetView: + setSwipeState(SwipeState::SwipingCloseSearchWidget); + cancelSearchWidgetAnimations(); + break; + case ViewState::AppDrawerView: + setSwipeState(SwipeState::SwipingAppDrawerGrid); + cancelAppDrawerAnimations(); + case ViewState::FolderView: + case ViewState::SettingsView: + // no vertical behaviour in folder or settings view + default: + break; + } + } + } +} + +void HomeScreenState::cancelAppDrawerAnimations() +{ + m_openAppDrawerAnim->stop(); + m_closeAppDrawerAnim->stop(); +} + +void HomeScreenState::cancelSearchWidgetAnimations() +{ + m_openSearchWidgetAnim->stop(); + m_closeSearchWidgetAnim->stop(); +} diff --git a/containments/homescreens/folio/homescreenstate.h b/containments/homescreens/folio/homescreenstate.h new file mode 100644 index 00000000..15d6007e --- /dev/null +++ b/containments/homescreens/folio/homescreenstate.h @@ -0,0 +1,400 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "qqml.h" +#include +#include + +#include + +#include "dragstate.h" + +class DragState; + +/** + * @short The homescreen state, containing information on positioning panels as well as any swipe events. + * + * @author Devin Lin + */ + +class HomeScreenState : public QObject +{ + Q_OBJECT + Q_PROPERTY(HomeScreenState::SwipeState swipeState READ swipeState NOTIFY swipeStateChanged) + Q_PROPERTY(HomeScreenState::ViewState viewState READ viewState NOTIFY viewStateChanged) + Q_PROPERTY(DragState *dragState READ dragState CONSTANT) + + Q_PROPERTY(qreal viewWidth READ viewWidth WRITE setViewWidth NOTIFY viewWidthChanged) + Q_PROPERTY(qreal viewHeight READ viewHeight WRITE setViewHeight NOTIFY viewHeightChanged) + Q_PROPERTY(qreal viewTopPadding READ viewTopPadding WRITE setViewTopPadding NOTIFY viewTopPaddingChanged) + Q_PROPERTY(qreal viewBottomPadding READ viewBottomPadding WRITE setViewBottomPadding NOTIFY viewBottomPaddingChanged) + Q_PROPERTY(qreal viewLeftPadding READ viewLeftPadding WRITE setViewLeftPadding NOTIFY viewLeftPaddingChanged) + Q_PROPERTY(qreal viewRightPadding READ viewRightPadding WRITE setViewRightPadding NOTIFY viewRightPaddingChanged) + + Q_PROPERTY(HomeScreenState::PageOrientation pageOrientation READ pageOrientation NOTIFY pageOrientationChanged) + Q_PROPERTY(HomeScreenState::FavouritesBarLocation favouritesBarLocation READ favouritesBarLocation NOTIFY favouritesBarLocationChanged) + Q_PROPERTY(int pageRows READ pageRows NOTIFY pageRowsChanged) + Q_PROPERTY(int pageColumns READ pageColumns NOTIFY pageColumnsChanged) + + // page measurements + Q_PROPERTY(qreal pageViewX READ pageViewX WRITE setPageViewX NOTIFY pageViewXChanged) + Q_PROPERTY(qreal pageWidth READ pageWidth WRITE setPageWidth NOTIFY pageWidthChanged) + Q_PROPERTY(qreal pageHeight READ pageHeight WRITE setPageHeight NOTIFY pageHeightChanged) + Q_PROPERTY(qreal pageContentWidth READ pageContentWidth NOTIFY pageContentWidthChanged) + Q_PROPERTY(qreal pageContentHeight READ pageContentHeight NOTIFY pageContentHeightChanged) + + // cell measurements + Q_PROPERTY(qreal pageCellWidth READ pageCellWidth NOTIFY pageCellWidthChanged) + Q_PROPERTY(qreal pageCellHeight READ pageCellHeight NOTIFY pageCellHeightChanged) + Q_PROPERTY(qreal pageDelegateLabelHeight READ pageDelegateLabelHeight WRITE setPageDelegateLabelHeight NOTIFY pageDelegateLabelHeightChanged) + Q_PROPERTY(qreal pageDelegateLabelSpacing READ pageDelegateLabelSpacing WRITE setPageDelegateLabelSpacing NOTIFY pageDelegateLabelSpacingChanged) + + // folder measurements and state + Q_PROPERTY(qreal folderViewX READ folderViewX WRITE setFolderViewX NOTIFY folderViewXChanged) + Q_PROPERTY(qreal folderPageWidth READ folderPageWidth WRITE setFolderPageWidth NOTIFY folderPageWidthChanged) + Q_PROPERTY(qreal folderPageHeight READ folderPageHeight WRITE setFolderPageHeight NOTIFY folderPageHeightChanged) + Q_PROPERTY(qreal folderPageContentWidth READ folderPageContentWidth WRITE setFolderPageContentWidth NOTIFY folderPageContentWidthChanged) + Q_PROPERTY(qreal folderPageContentHeight READ folderPageContentHeight WRITE setFolderPageContentHeight NOTIFY folderPageContentHeightChanged) + Q_PROPERTY(qreal folderOpenProgress READ folderOpenProgress WRITE setFolderOpenProgress NOTIFY folderOpenProgressChanged) + Q_PROPERTY(FolioApplicationFolder *currentFolder READ currentFolder NOTIFY currentFolderChanged) + + Q_PROPERTY(qreal settingsOpenProgress READ settingsOpenProgress WRITE setSettingsOpenProgress NOTIFY settingsOpenProgressChanged) + + Q_PROPERTY(qreal appDrawerOpenProgress READ appDrawerOpenProgress NOTIFY appDrawerOpenProgressChanged) + Q_PROPERTY(qreal appDrawerY READ appDrawerY WRITE setAppDrawerY NOTIFY appDrawerYChanged) + + Q_PROPERTY(qreal searchWidgetOpenProgress READ searchWidgetOpenProgress NOTIFY searchWidgetOpenProgressChanged) + Q_PROPERTY(qreal searchWidgetY READ searchWidgetY WRITE setSearchWidgetY NOTIFY searchWidgetYChanged) + + Q_PROPERTY(qreal delegateDragX READ delegateDragX NOTIFY delegateDragXChanged) + Q_PROPERTY(qreal delegateDragY READ delegateDragY NOTIFY delegateDragYChanged) + + Q_PROPERTY(int currentPage READ currentPage NOTIFY pageNumChanged) + Q_PROPERTY(int currentFolderPage READ currentFolderPage NOTIFY folderPageNumChanged) + +public: + enum SwipeState { + None, + DeterminingSwipeType, + SwipingPages, // main homescreen view + SwipingOpenAppDrawer, + SwipingCloseAppDrawer, + SwipingAppDrawerGrid, + SwipingOpenSearchWidget, + SwipingCloseSearchWidget, + SwipingFolderPages, + AwaitingDraggingDelegate, + DraggingDelegate, + }; + Q_ENUM(SwipeState) + + enum ViewState { + SearchWidgetView, + PageView, + AppDrawerView, + FolderView, + SettingsView, + }; + Q_ENUM(ViewState) + + enum FavouritesBarLocation { Bottom, Left, Right }; + Q_ENUM(FavouritesBarLocation) + + enum PageOrientation { + RegularPosition, // rows and columns are read as normal + RotateClockwise, // swap the rows and columns + RotateCounterClockwise, // swap the rows and columns, and then flip the rows + RotateUpsideDown, // flip the rows and flip the columns + }; + Q_ENUM(PageOrientation) + + static HomeScreenState *self(); + + HomeScreenState(QObject *parent = nullptr); + + // the current state of swipe interaction + SwipeState swipeState() const; + + // the current view + ViewState viewState() const; + + // drag state object + DragState *dragState() const; + + qreal viewWidth() const; + void setViewWidth(qreal viewWidth); + + qreal viewHeight() const; + void setViewHeight(qreal viewHeight); + + qreal viewTopPadding() const; + void setViewTopPadding(qreal viewTopPadding); + + qreal viewBottomPadding() const; + void setViewBottomPadding(qreal viewBottomPadding); + + qreal viewLeftPadding() const; + void setViewLeftPadding(qreal viewLeftPadding); + + qreal viewRightPadding() const; + void setViewRightPadding(qreal viewRightPadding); + + // whether to swap rows and columns in the layout + // this happens if the width of the screen is larger than the height + PageOrientation pageOrientation() const; + void setPageOrientation(PageOrientation pageOrientation); + + FavouritesBarLocation favouritesBarLocation() const; + + // the number of rows on a page + int pageRows() const; + + // the number of columns on a page + int pageColumns() const; + + // the current horizontal position of the pageview + // starts at 0, each page is m_pageWidth wide + // first page is at -m_pageWidth, second is at -m_pageWidth * 2, etc. + qreal pageViewX() const; + void setPageViewX(qreal pageViewX); + + // the width of a single pageview page (set from QML) + qreal pageWidth() const; + void setPageWidth(qreal pageWidth); + + qreal pageHeight() const; + void setPageHeight(qreal pageHeight); + + qreal pageContentWidth() const; + void calculatePageContentWidth(); + + qreal pageContentHeight() const; + void calculatePageContentHeight(); + + qreal pageCellWidth() const; + void calculatePageCellWidth(); + + qreal pageCellHeight() const; + void calculatePageCellHeight(); + + qreal pageDelegateLabelHeight() const; + void setPageDelegateLabelHeight(qreal pageDelegateLabelHeight); + + qreal pageDelegateLabelSpacing() const; + void setPageDelegateLabelSpacing(qreal pageDelegateLabelSpacing); + + qreal folderViewX() const; + void setFolderViewX(qreal folderViewX); + + qreal folderPageWidth() const; + void setFolderPageWidth(qreal folderPageWidth); + + qreal folderPageHeight() const; + void setFolderPageHeight(qreal folderPageHeight); + + qreal folderPageContentWidth() const; + void setFolderPageContentWidth(qreal folderPageContentWidth); + + qreal folderPageContentHeight() const; + void setFolderPageContentHeight(qreal folderPageContentHeight); + + qreal folderOpenProgress() const; + void setFolderOpenProgress(qreal folderOpenProgress); + + FolioApplicationFolder *currentFolder() const; + void setCurrentFolder(FolioApplicationFolder *folder); + + // the progress for the opening of the settings view + qreal settingsOpenProgress(); + void setSettingsOpenProgress(qreal settingsOpenProgress); + + // between 0-1, the progress for the opening of the app drawer + qreal appDrawerOpenProgress(); + + // the position of the app drawer + // 0: the app drawer is open + // APP_DRAWER_OPEN_DIST: - the app drawer is closed + qreal appDrawerY(); + void setAppDrawerY(qreal appDrawerY); + + // between 0-1, the progress for the opening of the search widget + qreal searchWidgetOpenProgress(); + + // the position of the search widget + // 0: the search widget + // SEARCH_WIDGET_OPEN_DIST: - the app drawer is closed + qreal searchWidgetY(); + void setSearchWidgetY(qreal searchWidgetY); + + qreal delegateDragX(); + void setDelegateDragX(qreal delegateDragX); + + qreal delegateDragY(); + void setDelegateDragY(qreal delegateDragY); + + int currentPage(); + void setCurrentPage(int currentPage); + + int currentFolderPage(); + + // QML helpers + Q_INVOKABLE FolioDelegate *getPageDelegateAt(int page, int row, int column); + Q_INVOKABLE FolioDelegate *getFavouritesDelegateAt(int position); + Q_INVOKABLE FolioDelegate *getFolderDelegateAt(int position); + Q_INVOKABLE QPointF getPageDelegateScreenPosition(int page, int row, int column); + Q_INVOKABLE QPointF getFavouritesDelegateScreenPosition(int position); + Q_INVOKABLE QPointF getFolderDelegateScreenPosition(int position); + +Q_SIGNALS: + void swipeStateChanged(); + void viewStateChanged(); + void viewWidthChanged(); + void viewHeightChanged(); + void viewTopPaddingChanged(); + void viewBottomPaddingChanged(); + void viewLeftPaddingChanged(); + void viewRightPaddingChanged(); + void pageOrientationChanged(); + void favouritesBarLocationChanged(); + void pageRowsChanged(); + void pageColumnsChanged(); + void pageViewXChanged(); + void pageWidthChanged(); + void pageHeightChanged(); + void pageContentWidthChanged(); + void pageContentHeightChanged(); + void pageCellWidthChanged(); + void pageCellHeightChanged(); + void pageDelegateLabelHeightChanged(); + void pageDelegateLabelSpacingChanged(); + void folderViewXChanged(); + void folderPageWidthChanged(); + void folderPageHeightChanged(); + void folderPageContentWidthChanged(); + void folderPageContentHeightChanged(); + void folderOpenProgressChanged(); + void currentFolderChanged(); + void settingsOpenProgressChanged(); + void appDrawerOpenProgressChanged(); + void appDrawerYChanged(); + void appDrawerClosed(); + void appDrawerOpened(); + void searchWidgetOpenProgressChanged(); + void searchWidgetYChanged(); + void delegateDragXChanged(); + void delegateDragYChanged(); + void delegateDragEnded(); + void delegateDragFromPageStarted(int page, int row, int column); + void delegateDragFromFavouritesStarted(int position); + void delegateDragFromAppDrawerStarted(QString storageId); + void delegateDragFromFolderStarted(FolioApplicationFolder *folder, int position); + void pageNumChanged(); + void folderPageNumChanged(); + + void leftCurrentFolder(); + void folderAboutToOpen(qreal x, qreal y); // the position on the screen where the delegate is at, for animations + void appDrawerGridYChanged(qreal y); + void appDrawerGridFlickRequested(); + +public Q_SLOTS: + void openAppDrawer(); + void closeAppDrawer(); + void openSearchWidget(); + void closeSearchWidget(); + + void snapPage(); // snaps to closest page + void goToPage(int page); + + void goToFolderPage(int page); + void openFolder(qreal delegateX, qreal delegateY, FolioApplicationFolder *folder); + void closeFolder(); + + void openSettingsView(); + void closeSettingsView(); + + void startDelegatePageDrag(qreal startX, qreal startY, int page, int row, int column); + void startDelegateFavouritesDrag(qreal startX, qreal startY, int position); + void startDelegateAppDrawerDrag(qreal startX, qreal startY, QString storageId); + void startDelegateFolderDrag(qreal startX, qreal startY, FolioApplicationFolder *folder, int position); + void cancelDelegateDrag(); + + // from SwipeArea + void swipeStarted(); + void swipeEnded(); + void swipeMoved(qreal totalDeltaX, qreal totalDeltaY, qreal deltaX, qreal deltaY); + +private: + void setViewState(ViewState viewState); + void setSwipeState(SwipeState swipeState); + + void startDelegateDrag(qreal startX, qreal startY); + + void cancelAppDrawerAnimations(); + void cancelSearchWidgetAnimations(); + + // check if we passed the swipe threshold, and determine the swipe type after + void determineSwipeTypeAfterThreshold(qreal totalDeltaX, qreal totalDeltaY); + + QPropertyAnimation *setupAnimation(QByteArray property, int duration, QEasingCurve::Type curve, qreal endValue); + + SwipeState m_swipeState{SwipeState::None}; + ViewState m_viewState{ViewState::PageView}; + + DragState *m_dragState{nullptr}; + + qreal m_viewWidth{0}; + qreal m_viewHeight{0}; + qreal m_viewTopPadding{0}; + qreal m_viewBottomPadding{0}; + qreal m_viewLeftPadding{0}; + qreal m_viewRightPadding{0}; + + PageOrientation m_pageOrientation{PageOrientation::RegularPosition}; + + qreal m_pageViewX{0}; + qreal m_pageWidth{0}; + qreal m_pageHeight{0}; + qreal m_pageContentWidth{0}; + qreal m_pageContentHeight{0}; + + qreal m_pageCellWidth{0}; + qreal m_pageCellHeight{0}; + qreal m_pageDelegateLabelHeight{0}; + qreal m_pageDelegateLabelSpacing{0}; + + qreal m_folderViewX{0}; + qreal m_folderPageWidth{0}; + qreal m_folderPageHeight{0}; + qreal m_folderPageContentWidth{0}; + qreal m_folderPageContentHeight{0}; + qreal m_folderOpenProgress{0}; + FolioApplicationFolder *m_currentFolder{nullptr}; + + qreal m_settingsOpenProgress{0}; + + qreal m_appDrawerOpenProgress{0}; + qreal m_appDrawerY{0}; + qreal m_searchWidgetOpenProgress{0}; + qreal m_searchWidgetY{0}; + qreal m_delegateDragX{0}; + qreal m_delegateDragY{0}; + + int m_pageNum{0}; + int m_folderPageNum{0}; + + bool m_movingUp{false}; + bool m_movingRight{false}; + + QPropertyAnimation *m_openAppDrawerAnim{nullptr}; + QPropertyAnimation *m_closeAppDrawerAnim{nullptr}; + QPropertyAnimation *m_openSearchWidgetAnim{nullptr}; + QPropertyAnimation *m_closeSearchWidgetAnim{nullptr}; + QPropertyAnimation *m_pageAnim{nullptr}; + QPropertyAnimation *m_openFolderAnim{nullptr}; + QPropertyAnimation *m_closeFolderAnim{nullptr}; + QPropertyAnimation *m_folderPageAnim{nullptr}; + QPropertyAnimation *m_openSettingsAnim{nullptr}; + QPropertyAnimation *m_closeSettingsAnim{nullptr}; +}; diff --git a/containments/homescreens/folio/package/contents/config/main.xml b/containments/homescreens/folio/package/contents/config/main.xml deleted file mode 100644 index 7b1e297c..00000000 --- a/containments/homescreens/folio/package/contents/config/main.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - org.kde.phone.dialer.desktop - - - - diff --git a/containments/homescreens/folio/package/contents/ui/AppDrawer.qml b/containments/homescreens/folio/package/contents/ui/AppDrawer.qml new file mode 100644 index 00000000..406722f7 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/AppDrawer.qml @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import Qt5Compat.GraphicalEffects + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.components 3.0 as PC3 +import org.kde.kirigami 2.10 as Kirigami + +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +Item { + id: root + + property var homeScreen + + property real leftPadding: 0 + property real topPadding: 0 + property real bottomPadding: 0 + property real rightPadding: 0 + + required property int headerHeight + required property var headerItem + + // height from top of screen that the drawer starts + readonly property real drawerTopMargin: height - topPadding - bottomPadding + + property alias flickable: appDrawerGrid + + Item { + anchors.fill: parent + + anchors.leftMargin: root.leftPadding + anchors.topMargin: root.topPadding + anchors.rightMargin: root.rightPadding + anchors.bottomMargin: root.bottomPadding + + // drawer header + MobileShell.BaseItem { + id: drawerHeader + height: root.headerHeight + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + contentItem: root.headerItem + } + + AppDrawerGrid { + id: appDrawerGrid + homeScreen: root.homeScreen + height: parent.height - drawerHeader.height + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + opacity: 0 + } + + // opacity gradient at grid edges + OpacityMask { + anchors.fill: appDrawerGrid + source: appDrawerGrid + maskSource: Rectangle { + id: mask + width: appDrawerGrid.width + height: appDrawerGrid.height + + property real gradientPct: (Kirigami.Units.gridUnit * 2) / appDrawerGrid.height + + gradient: Gradient { + GradientStop { position: 0.0; color: appDrawerGrid.atYBeginning ? 'white' : 'transparent' } + GradientStop { position: mask.gradientPct; color: 'white' } + GradientStop { position: 1.0 - mask.gradientPct; color: 'white' } + GradientStop { position: 1.0; color: appDrawerGrid.atYEnd ? 'white' : 'transparent' } + } + } + } + } +} + + diff --git a/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml b/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml new file mode 100644 index 00000000..03b972d2 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.components 3.0 as PC3 +import org.kde.kirigami as Kirigami + +import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +import "./delegate" + +MobileShell.GridView { + id: root + cacheBuffer: cellHeight * 20 + reuseItems: true + layer.enabled: true + + property var homeScreen + + + readonly property int reservedSpaceForLabel: Folio.HomeScreenState.pageDelegateLabelHeight + readonly property real effectiveContentWidth: width - leftMargin - rightMargin + readonly property real horizontalMargin: Math.round(width * 0.05) + + leftMargin: horizontalMargin + rightMargin: horizontalMargin + + cellWidth: effectiveContentWidth / Math.min(Math.floor(effectiveContentWidth / (Folio.FolioSettings.delegateIconSize + Kirigami.Units.largeSpacing * 3)), 8) + cellHeight: cellWidth + reservedSpaceForLabel + + boundsBehavior: Flickable.OvershootBounds + + readonly property int columns: Math.floor(effectiveContentWidth / cellWidth) + readonly property int rows: Math.ceil(root.count / columns) + + // HACK: the first swipe from the top of the app drawer is done from HomeScreenState, not the flickable + // due to issues with Flickable getting its swipe stolen by SwipeArea + interactive: !atYBeginning && Folio.HomeScreenState.swipeState !== Folio.HomeScreenState.SwipingAppDrawerGrid + Connections { + target: Folio.HomeScreenState + + function onSwipeStateChanged() { + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.SwipingAppDrawerGrid) { + velocityCalculator.startMeasure(); + velocityCalculator.changePosition(root.contentY); + } + } + + function onAppDrawerGridYChanged(y) { + root.contentY = Math.max(0, root.contentY - y); + velocityCalculator.changePosition(root.contentY); + } + + function onAppDrawerGridFlickRequested() { + root.returnToBounds(); + root.flick(0, -velocityCalculator.velocity); + } + } + + MobileShell.VelocityCalculator { + id: velocityCalculator + } + + model: Folio.ApplicationListModel + + delegate: AppDelegate { + id: delegate + shadow: false + application: model.delegate.application + + width: root.cellWidth + height: root.cellHeight + + onPressAndHold: { + Folio.HomeScreenState.closeAppDrawer(); + let mappedCoords = root.homeScreen.prepareStartDelegateDrag(model.delegate, delegate.delegateItem); + Folio.HomeScreenState.startDelegateAppDrawerDrag( + mappedCoords.x, + mappedCoords.y, + model.delegate.application.storageId + ); + } + } + + PC3.ScrollBar.vertical: PC3.ScrollBar { + id: scrollBar + interactive: true + enabled: true + implicitWidth: Kirigami.Units.smallSpacing + + Behavior on opacity { + OpacityAnimator { + duration: Kirigami.Units.longDuration * 2 + easing.type: Easing.InOutQuad + } + } + + contentItem: Rectangle { + radius: width / 2 + color: Qt.rgba(1, 1, 1, 0.3) + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/AppDrawerHeader.qml b/containments/homescreens/folio/package/contents/ui/AppDrawerHeader.qml new file mode 100644 index 00000000..280ad0ac --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/AppDrawerHeader.qml @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2021-2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.plasma.components 3.0 as PlasmaComponents + +Item { + id: root + + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + Kirigami.Theme.inherit: false + + RowLayout { + anchors.topMargin: Kirigami.Units.smallSpacing + anchors.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing + anchors.rightMargin: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing + anchors.fill: parent + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + color: "white" + text: i18n("Applications") + font.weight: Font.Bold + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.5 + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/ContextMenuLoader.qml b/containments/homescreens/folio/package/contents/ui/ContextMenuLoader.qml new file mode 100644 index 00000000..589d3093 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/ContextMenuLoader.qml @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Window +import QtQuick.Layouts + +import org.kde.plasma.components 3.0 as PC3 +import org.kde.kirigami 2.10 as Kirigami + +Loader { + id: root + active: false + + property list actions + + function open() { + root.active = true; + root.item.open(); + } + + function close() { + if (root.item) { + root.item.close(); + } + } + + sourceComponent: PC3.Menu { + id: menu + title: "Context Menu" + closePolicy: PC3.Menu.CloseOnReleaseOutside | PC3.Menu.CloseOnEscape + + Repeater { + model: root.actions + delegate: PC3.MenuItem { + icon.name: modelData.iconName + text: modelData.text + onClicked: modelData.triggered() + } + } + + onClosed: root.active = false + } +} diff --git a/containments/homescreens/folio/package/contents/ui/DelegateDragItem.qml b/containments/homescreens/folio/package/contents/ui/DelegateDragItem.qml new file mode 100644 index 00000000..6379c107 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/DelegateDragItem.qml @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts + +import org.kde.kirigami 2.20 as Kirigami +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +import "./delegate" + +Item { + id: root + width: Folio.HomeScreenState.pageCellWidth + height: Folio.HomeScreenState.pageCellHeight + + property Folio.FolioDelegate delegate + + readonly property real dropAnimationRunning: dragXAnim.running || dragYAnim.running + + visible: false + x: Math.round(Folio.HomeScreenState.delegateDragX) + y: Math.round(Folio.HomeScreenState.delegateDragY) + + function setXBinding() { + x = Qt.binding(() => Math.round(Folio.HomeScreenState.delegateDragX)); + } + function setYBinding() { + y = Qt.binding(() => Math.round(Folio.HomeScreenState.delegateDragY)); + } + + // animate drop x + XAnimator on x { + id: dragXAnim + running: false + duration: Kirigami.Units.longDuration + easing.type: Easing.OutCubic + onFinished: { + root.visible = false; + root.setXBinding(); + } + } + + // animate drop y + YAnimator on y { + id: dragYAnim + running: false + duration: Kirigami.Units.longDuration + easing.type: Easing.OutCubic + onFinished: { + root.visible = false; + root.setYBinding(); + } + } + + // animate scale if it's an app being placed into a folder + ScaleAnimator on scale { + id: scaleAnim + to: 0 + running: false + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutCubic + } + + Connections { + id: stateWatcher + target: Folio.HomeScreenState + + property var delegateDroppedOn: null + + // reset and show drag item + function onSwipeStateChanged() { + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) { + root.scale = 1.0; + root.visible = true; + } + } + + // save the existing delegate at the spot (this is called before the delegate is dropped) + function onDelegateDragEnded() { + let dragState = Folio.HomeScreenState.dragState; + let dropPosition = dragState.candidateDropPosition; + + switch (dropPosition.location) { + case Folio.DelegateDragPosition.Pages: + stateWatcher.delegateDroppedOn = Folio.HomeScreenState.getPageDelegateAt(dropPosition.page, dropPosition.pageRow, dropPosition.pageColumn); + break; + case Folio.DelegateDragPosition.Favourites: + stateWatcher.delegateDroppedOn = Folio.HomeScreenState.getFavouritesDelegateAt(dropPosition.favouritesPosition); + break; + case Folio.DelegateDragPosition.Folder: + stateWatcher.delegateDroppedOn = null + break; + } + } + } + + Connections { + target: Folio.HomeScreenState.dragState + + // animate from when the delegate is dropped to its drop position + function onDelegateDroppedAndPlaced() { + let dragState = Folio.HomeScreenState.dragState; + let dropPosition = dragState.candidateDropPosition; + + let pos = null; + + switch (dropPosition.location) { + case Folio.DelegateDragPosition.Pages: + pos = Folio.HomeScreenState.getPageDelegateScreenPosition(dropPosition.page, dropPosition.pageRow, dropPosition.pageColumn); + break; + case Folio.DelegateDragPosition.Favourites: + pos = Folio.HomeScreenState.getFavouritesDelegateScreenPosition(dropPosition.favouritesPosition); + break; + case Folio.DelegateDragPosition.Folder: + pos = Folio.HomeScreenState.getFolderDelegateScreenPosition(dropPosition.folderPosition); + break; + } + + dragXAnim.to = pos.x; + dragYAnim.to = pos.y; + dragXAnim.restart(); + dragYAnim.restart(); + + if (stateWatcher.delegateDroppedOn && + stateWatcher.delegateDroppedOn.type != Folio.FolioDelegate.None && + dragState.dropDelegate.type === Folio.FolioDelegate.Application) { + + // scale animation if we are creating, or inserting into a folder + scaleAnim.restart(); + } + } + } + + // simulate an icon delegate + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // icon + DelegateIconLoader { + id: loader + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + Layout.minimumWidth: Folio.FolioSettings.delegateIconSize + Layout.minimumHeight: Folio.FolioSettings.delegateIconSize + Layout.preferredHeight: Layout.minimumHeight + + delegate: root.delegate + + layer.enabled: true + layer.effect: DelegateShadow {} + } + + // simulate the delegate label for positioning purposes + DelegateLabel { + id: label + opacity: 0 + + Layout.fillWidth: true + Layout.preferredHeight: Folio.HomeScreenState.pageDelegateLabelHeight + Layout.topMargin: Folio.HomeScreenState.pageDelegateLabelSpacing + Layout.leftMargin: -parent.anchors.leftMargin + Kirigami.Units.smallSpacing + Layout.rightMargin: -parent.anchors.rightMargin + Kirigami.Units.smallSpacing + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/FavoriteStrip.qml b/containments/homescreens/folio/package/contents/ui/FavoriteStrip.qml deleted file mode 100644 index 1668da5f..00000000 --- a/containments/homescreens/folio/package/contents/ui/FavoriteStrip.qml +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.4 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.3 as Controls - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.kquickcontrolsaddons 2.0 - - -LauncherContainer { - id: root - - readonly property int count: flow.width / cellWidth - - flow.flow: Flow.TopToBottom - - height: visible ? cellHeight : 0 - - frame.implicitWidth: cellWidth * Math.max(1, flow.children.length) + frame.leftPadding + frame.rightPadding - - Behavior on height { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - } - Behavior on opacity { - OpacityAnimator { - duration: Kirigami.Units.longDuration * 4 - easing.type: Easing.InOutQuad - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/FavouritesBar.qml b/containments/homescreens/folio/package/contents/ui/FavouritesBar.qml new file mode 100644 index 00000000..3466594a --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/FavouritesBar.qml @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.components 3.0 as PC3 +import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState +import org.kde.private.mobile.homescreen.folio 1.0 as Folio +import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.kirigami 2.10 as Kirigami + +import "./delegate" + +Item { + id: root + + property var homeScreen + + // use to account for x-y positioning, because delegate x and y will include the screen margins + property real leftMargin + property real topMargin + + signal delegateDragRequested(var item) + + Repeater { + model: Folio.FavouritesModel + + delegate: Item { + id: delegate + + property var delegateModel: model.delegate + property int index: model.index + + property var dragState: Folio.HomeScreenState.dragState + property bool isDropPositionThis: dragState.candidateDropPosition.location === Folio.DelegateDragPosition.Favourites && + dragState.candidateDropPosition.favouritesPosition === delegate.index + property bool isAppHoveredOver: Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && + dragState.dropDelegate && + dragState.dropDelegate.type === Folio.FolioDelegate.Application && + isDropPositionThis + + // only one of them will be used, because of the anchors below + // this is used due to the ability for the favourites bar to be in multiple locations + x: model.xPosition - leftMargin + y: model.xPosition - topMargin + + anchors.verticalCenter: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? parent.verticalCenter : undefined + anchors.horizontalCenter: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? undefined : parent.horizontalCenter + + Behavior on x { + NumberAnimation { duration: 250; easing.type: Easing.InOutQuad } + } + Behavior on y { + NumberAnimation { duration: 250; easing.type: Easing.InOutQuad } + } + + implicitWidth: Folio.HomeScreenState.pageCellWidth + implicitHeight: Folio.HomeScreenState.pageCellHeight + width: Folio.HomeScreenState.pageCellWidth + height: Folio.HomeScreenState.pageCellHeight + + Loader { + anchors.fill: parent + + sourceComponent: { + if (delegate.delegateModel.type === Folio.FolioDelegate.Application) { + return appComponent; + } else if (delegate.delegateModel.type === Folio.FolioDelegate.Folder) { + return folderComponent; + } else { + // ghost entry + return placeholderComponent; + } + } + } + + Component { + id: placeholderComponent + + Item {} + } + + Component { + id: appComponent + + AppDelegate { + id: appDelegate + application: delegate.delegateModel.application + name: Folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.application.name : "" + shadow: true + + turnToFolder: delegate.isAppHoveredOver + turnToFolderAnimEnabled: Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate + + // do not show if the drop animation is running to this delegate + visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis) + + // don't show label in drag and drop mode + labelOpacity: delegate.opacity + + onPressAndHold: { + let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.delegateModel, appDelegate.delegateItem); + Folio.HomeScreenState.startDelegateFavouritesDrag( + mappedCoords.x, + mappedCoords.y, + delegate.index + ); + + contextMenu.open(); + } + + onPressAndHoldReleased: { + // cancel the event if the delegate is not dragged + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) { + homeScreen.cancelDelegateDrag(); + } + } + + onRightMousePress: { + contextMenu.open(); + } + + ContextMenuLoader { + id: contextMenu + + // close menu when drag starts + Connections { + target: Folio.HomeScreenState + + function onSwipeStateChanged() { + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) { + contextMenu.close(); + } + } + } + + actions: [ + Kirigami.Action { + icon.name: "emblem-favorite" + text: i18n("Remove") + onTriggered: Folio.FavouritesModel.removeEntry(delegate.index) + } + ] + } + } + } + + Component { + id: folderComponent + + AppFolderDelegate { + id: appFolderDelegate + shadow: true + folder: delegate.delegateModel.folder + name: Folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.folder.name : "" + + // do not show if the drop animation is running to this delegate, and the drop delegate is a folder + visible: !(root.homeScreen.dropAnimationRunning && + delegate.isDropPositionThis && + delegate.dragState.dropDelegate.type === Folio.FolioDelegate.Folder) + + appHoveredOver: delegate.isAppHoveredOver + + // don't show label in drag and drop mode + labelOpacity: delegate.opacity + + onAfterClickAnimation: { + const pos = homeScreen.prepareFolderOpen(appFolderDelegate.contentItem); + Folio.HomeScreenState.openFolder(pos.x, pos.y, delegate.delegateModel.folder); + } + + onPressAndHold: { + let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.delegateModel, appFolderDelegate.delegateItem); + Folio.HomeScreenState.startDelegateFavouritesDrag( + mappedCoords.x, + mappedCoords.y, + delegate.index + ); + + contextMenu.open(); + } + + onPressAndHoldReleased: { + // cancel the event if the delegate is not dragged + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) { + root.homeScreen.cancelDelegateDrag(); + } + } + + onRightMousePress: { + contextMenu.open(); + } + + // TODO don't use loader, and move outside to a page to make it more performant + ContextMenuLoader { + id: contextMenu + + // close menu when drag starts + Connections { + target: Folio.HomeScreenState + + function onSwipeStateChanged() { + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) { + contextMenu.close(); + } + } + } + + actions: [ + Kirigami.Action { + icon.name: "emblem-favorite" + text: i18n("Remove") + onTriggered: Folio.FavouritesModel.removeEntry(delegate.index) + } + ] + } + } + } + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/FlickContainer.qml b/containments/homescreens/folio/package/contents/ui/FlickContainer.qml deleted file mode 100644 index 1c438235..00000000 --- a/containments/homescreens/folio/package/contents/ui/FlickContainer.qml +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.15 -import QtQuick.Layouts 1.15 - -import org.kde.taskmanager 0.1 as TaskManager -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.plasma.private.mobileshell 1.0 as MobileShell - -MobileShell.Flickable { - id: root - - required property var homeScreenState - - // we use flickable solely for capturing flicks, not positioning elements - contentWidth: width + 99999 - contentHeight: height + 99999 - contentX: startContentX - contentY: startContentY - - readonly property real startContentX: contentWidth / 2 - readonly property real startContentY: contentHeight / 2 - - // update position from flickable movement - property real oldContentX - property real oldContentY - onContentXChanged: { - homeScreenState.updatePositionWithOffset(contentX - oldContentX, 0); - oldContentX = contentX; - } - onContentYChanged: { - homeScreenState.updatePositionWithOffset(0, -(contentY - oldContentY)); - oldContentY = contentY; - } - - onMovementStarted: homeScreenState.cancelAnimations(); - onMovementEnded: { - if (!homeScreenState.animationsRunning) { - homeScreenState.updateState(); - } - resetPosition(); - } - onFlickEnded: { - homeScreenState.cancelEditModeForItemsRequested() - resetPosition(); - } - - onDragStarted: homeScreenState.cancelEditModeForItemsRequested() - onDragEnded: homeScreenState.cancelEditModeForItemsRequested() - onFlickStarted: { - homeScreenState.cancelEditModeForItemsRequested(); - root.cancelFlick(); - } - - onDraggingChanged: { - if (!dragging) { - resetPosition(); - if (!homeScreenState.animationsRunning) { - homeScreenState.updateState(); - } - } else { - homeScreenState.cancelAnimations(); - } - } - - function resetPosition() { - oldContentX = startContentX; - contentX = startContentX; - oldContentY = startContentY; - contentY = startContentY; - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/FolderView.qml b/containments/homescreens/folio/package/contents/ui/FolderView.qml new file mode 100644 index 00000000..1f0100ec --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/FolderView.qml @@ -0,0 +1,297 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Controls as QQC2 + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +import "./delegate" + +Folio.DelegateTouchArea { + id: root + + property var homeScreen + + // the position on the screen for animations to start from + property real folderPositionX + property real folderPositionY + + property Folio.FolioApplicationFolder folder: Folio.HomeScreenState.currentFolder + + onClicked: close(); + + function close() { + Folio.HomeScreenState.closeFolder(); + } + + Connections { + target: Folio.HomeScreenState + + function onFolderAboutToOpen(x, y) { + root.folderPositionX = x - Folio.HomeScreenState.viewLeftPadding; + root.folderPositionY = y - Folio.HomeScreenState.viewRightPadding; + } + } + + FolderViewTitle { + id: titleText + width: root.width + + // have to use y instead of anchors to avoid animations + y: Math.round((root.height / 2) - (folderBackground.height / 2) - Kirigami.Units.gridUnit - height) + anchors.left: parent.left + anchors.right: parent.right + + folder: root.folder + + opacity: (root.opacity === 1) ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.shortDuration } + } + } + + function updateContentWidth() { + let margin = folderBackground.margin; + let columns = Math.floor((folderBackground.width - margin * 2) / Folio.HomeScreenState.pageCellWidth); + Folio.HomeScreenState.folderPageContentWidth = columns * Folio.HomeScreenState.pageCellWidth; + } + + function updateContentHeight() { + let margin = folderBackground.margin; + let rows = Math.floor((folderBackground.height - margin * 2) / Folio.HomeScreenState.pageCellHeight); + Folio.HomeScreenState.folderPageContentHeight = rows * Folio.HomeScreenState.pageCellHeight; + } + + Connections { + target: Folio.HomeScreenState + + function onPageCellWidthChanged() { + root.updateContentWidth(); + root.updateContentHeight(); + } + + function onPageCellHeightChanged() { + root.updateContentWidth(); + root.updateContentHeight(); + } + } + + Rectangle { + id: folderBackground + color: Qt.rgba(255, 255, 255, 0.3) + radius: Kirigami.Units.gridUnit + + readonly property real margin: Kirigami.Units.largeSpacing + readonly property real maxLength: Math.min(root.width * 0.9, root.height * 0.9) + + width: { + let perRow = 0; + if (root.width < root.height) { + perRow = Math.floor((maxLength - margin * 2) / Folio.HomeScreenState.pageCellWidth); + } else { + // try to get the same number of rows as columns + perRow = Math.floor((maxLength - margin * 2) / Folio.HomeScreenState.pageCellHeight); + } + return Math.min(root.width * 0.9, perRow * Folio.HomeScreenState.pageCellWidth + margin * 2); + } + height: { + let perRow = 0; + if (root.width < root.height) { + // try to get the same number of rows as columns + perRow = Math.floor((maxLength - margin * 2) / Folio.HomeScreenState.pageCellWidth); + } else { + perRow = Math.floor((maxLength - margin * 2) / Folio.HomeScreenState.pageCellHeight); + } + return Math.min(root.height * 0.9, perRow * Folio.HomeScreenState.pageCellHeight + margin * 2); + } + + onWidthChanged: { + Folio.HomeScreenState.folderPageWidth = width; + root.updateContentHeight(); + root.updateContentHeight(); + } + onHeightChanged: { + Folio.HomeScreenState.folderPageHeight = height; + root.updateContentWidth(); + root.updateContentHeight(); + } + + x: { + const folderPos = root.folderPositionX; + const centerX = (root.width / 2) - (width / 2); + return Math.round(folderPos + (centerX - folderPos) * Folio.HomeScreenState.folderOpenProgress); + } + y: { + const folderPos = root.folderPositionY; + const centerY = (root.height / 2) - (height / 2); + return Math.round(folderPos + (centerY - folderPos) * Folio.HomeScreenState.folderOpenProgress); + } + + transform: [ + Scale { + origin.x: 0 + origin.y: 0 + + xScale: { + const iconSize = Folio.FolioSettings.delegateIconSize; + const fullWidth = folderBackground.width; + const candidate = iconSize + (fullWidth - iconSize) * Folio.HomeScreenState.folderOpenProgress; + return Math.max(0, Math.min(1, candidate / fullWidth)); + } + yScale: { + const iconSize = Folio.FolioSettings.delegateIconSize; + const fullHeight = folderBackground.height; + const candidate = iconSize + (fullHeight - iconSize) * Folio.HomeScreenState.folderOpenProgress; + return Math.max(0, Math.min(1, candidate / fullHeight)); + } + } + ] + + MouseArea { + id: captureTouches + anchors.fill: parent + + // clip the pages + layer.enabled: true + + Item { + id: contentContainer + x: Folio.HomeScreenState.folderViewX + + Repeater { + model: root.folder ? root.folder.applications : [] + + delegate: Item { + id: delegate + + property var delegateModel: model.delegate + property int index: model.index + + property var dragState: Folio.HomeScreenState.dragState + property bool isDropPositionThis: dragState.candidateDropPosition.location === Folio.DelegateDragPosition.Folder && + dragState.candidateDropPosition.folderPosition === index + + x: model.xPosition + y: model.yPosition + + Behavior on x { + NumberAnimation { duration: 250; easing.type: Easing.InOutQuad } + } + Behavior on y { + NumberAnimation { duration: 250; easing.type: Easing.InOutQuad } + } + + implicitWidth: Folio.HomeScreenState.pageCellWidth + implicitHeight: Folio.HomeScreenState.pageCellHeight + width: Folio.HomeScreenState.pageCellWidth + height: Folio.HomeScreenState.pageCellHeight + + Loader { + id: delegateLoader + anchors.fill: parent + + sourceComponent: { + if (delegate.delegateModel.type === Folio.FolioDelegate.Application) { + return appComponent; + } else { + return noneComponent; + } + } + } + + Component { + id: noneComponent + + Item {} + } + + Component { + id: appComponent + + AppDelegate { + id: appDelegate + application: delegate.delegateModel.application + + // do not show if the drop animation is running to this delegate + visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis) + + // don't show label in drag and drop mode + labelOpacity: delegate.opacity + + onPressAndHold: { + let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.delegateModel, appDelegate.delegateItem); + Folio.HomeScreenState.startDelegateFolderDrag( + mappedCoords.x, + mappedCoords.y, + root.folder, + delegate.index + ); + + contextMenu.open(); + } + + onPressAndHoldReleased: { + // cancel the event if the delegate is not dragged + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) { + homeScreen.cancelDelegateDrag(); + } + } + + onRightMousePress: { + contextMenu.open(); + } + + ContextMenuLoader { + id: contextMenu + + // close menu when drag starts + Connections { + target: Folio.HomeScreenState + + function onSwipeStateChanged() { + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) { + contextMenu.close(); + } + } + } + + actions: [ + Kirigami.Action { + icon.name: "emblem-favorite" + text: i18n("Remove") + onTriggered: root.folder.removeApp(delegate.index) + } + ] + } + } + } + } + } + } + } + } + + QQC2.PageIndicator { + visible: count > 1 + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + + // have to use y instead of anchors to avoid animations + y: Math.round((root.height / 2) + (folderBackground.height / 2) + Kirigami.Units.largeSpacing) + anchors.horizontalCenter: parent.horizontalCenter + + currentIndex: Folio.HomeScreenState.currentFolderPage + count: Folio.HomeScreenState.currentFolder ? Folio.HomeScreenState.currentFolder.applications.numberOfPages : 0 + + opacity: (root.opacity === 1) ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.shortDuration } + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/FolderViewTitle.qml b/containments/homescreens/folio/package/contents/ui/FolderViewTitle.qml new file mode 100644 index 00000000..7d3131c1 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/FolderViewTitle.qml @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Controls as QQC2 + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +MobileShell.BaseItem { + id: root + + property Folio.FolioApplicationFolder folder + property bool inFolderTitleEditMode: false + + Connections { + target: Folio.HomeScreenState + + function onLeftCurrentFolder() { + root.inFolderTitleEditMode = false; + } + } + + background: Rectangle { + color: 'transparent' + TapHandler { + onTapped: { + root.close() + } + } + } + + Component { + id: folderTitleEdit + + TextEdit { + text: root.folder ? root.folder.name : "" + color: "white" + selectByMouse: true + wrapMode: TextEdit.Wrap + horizontalAlignment: TextEdit.AlignHCenter + + Component.onCompleted: { + forceActiveFocus(); + cursorPosition = text.length; + } + + font.weight: Font.Bold + font.pointSize: 18 + + layer.enabled: true + layer.effect: MobileShell.TextDropShadow {} + + onTextChanged: { + if (text.includes('\n')) { + // exit text edit mode when new line is entered + root.inFolderTitleEditMode = false; + } else if (root.folder) { + root.folder.name = text; + } + } + onEditingFinished: root.inFolderTitleEditMode = false + } + } + + Component { + id: folderTitleLabel + + QQC2.Label { + text: root.folder ? root.folder.name : "" + color: "white" + style: Text.Normal + styleColor: "transparent" + horizontalAlignment: Text.AlignHCenter + textFormat: Text.MarkdownText + + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + + font.weight: Font.Bold + font.pointSize: 18 + + layer.enabled: true + layer.effect: MobileShell.TextDropShadow {} + + MouseArea { + anchors.fill: parent + onClicked: root.inFolderTitleEditMode = true + } + } + } + + // folder title + contentItem: Loader { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + sourceComponent: root.inFolderTitleEditMode ? folderTitleEdit : folderTitleLabel + } +} diff --git a/containments/homescreens/folio/package/contents/ui/HomeDelegate.qml b/containments/homescreens/folio/package/contents/ui/HomeDelegate.qml deleted file mode 100644 index bb3b2757..00000000 --- a/containments/homescreens/folio/package/contents/ui/HomeDelegate.qml +++ /dev/null @@ -1,233 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.15 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.3 as Controls -import Qt5Compat.GraphicalEffects - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.core as PlasmaCore -import org.kde.plasma.components 3.0 as PC3 -import org.kde.kquickcontrolsaddons 2.0 - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager -import org.kde.plasma.private.mobileshell 1.0 as MobileShell -import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings -import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState -import org.kde.private.mobile.homescreen.folio 1.0 as Folio - -import "private" as Private - -ContainmentLayoutManager.ItemContainer { - id: delegate - required property Folio.DesktopModel desktopModel - - enabled: homeScreenState.currentView === HomeScreenState.PageView || homeScreenState.currentSwipeState === HomeScreenState.SwipingAppDrawerVisibility - - property var homeScreenState - - z: dragActive ? 1 : 0 - - property var modelData: typeof model !== "undefined" ? model : null - - Layout.minimumWidth: appletsLayout.cellWidth - Layout.minimumHeight: appletsLayout.cellHeight - - key: model.applicationUniqueId - property ContainmentLayoutManager.AppletsLayout appletsLayout - property int reservedSpaceForLabel - property real dragCenterX - property real dragCenterY - property alias iconItem: icon - - editModeCondition: ContainmentLayoutManager.ItemContainer.AfterPressAndHold - - signal launch(int x, int y, var source, string title) - - function syncDelegateGeometry() { - if (!applicationRunning) { - return; - } - - desktopModel.setMinimizedDelegate(index, delegate); - } - - function launchApp() { - if (modelData.applicationRunning) { - delegate.launch(0, 0, "", modelData.applicationName); - } else { - delegate.launch(delegate.x + (Kirigami.Units.smallSpacing * 2), delegate.y + (Kirigami.Units.smallSpacing * 2), icon.source, modelData.applicationName); - } - - desktopModel.setMinimizedDelegate(index, delegate); - MobileShell.AppLaunch.launchOrActivateApp(modelData.applicationStorageId); - } - - readonly property bool applicationRunning: model.applicationRunning - onApplicationRunningChanged: { - syncDelegateGeometry(); - } - onDragActiveChanged: { - if (dragActive) { - removeButton.show(); - mouseArea.enabled = true; - } - } - Connections { - target: homeScreenState - function onCancelEditModeForItemsRequested() { - cancelEdit() - } - function onXPositionChanged() { - syncDelegateGeometry() - } - } - Connections { - target: appletsLayout - function onAppletsLayoutInteracted() { - removeButton.hide(); - } - } - - contentItem: MouseArea { - id: mouseArea - - // grow/shrink animation - property real zoomScale: 1 - transform: Scale { - origin.x: mouseArea.width / 2; - origin.y: mouseArea.height / 2; - xScale: mouseArea.zoomScale - yScale: mouseArea.zoomScale - } - - property bool launchAppRequested: false - - NumberAnimation on zoomScale { - id: shrinkAnim - running: false - duration: ShellSettings.Settings.animationsEnabled ? 80 : 1 - to: ShellSettings.Settings.animationsEnabled ? 0.8 : 1 - onFinished: { - if (!mouseArea.pressed) { - growAnim.restart(); - } - } - } - - NumberAnimation on zoomScale { - id: growAnim - running: false - duration: ShellSettings.Settings.animationsEnabled ? 80 : 1 - to: 1 - onFinished: { - if (mouseArea.launchAppRequested) { - delegate.launchApp(); - mouseArea.launchAppRequested = false; - } - } - } - - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onPressedChanged: { - if (pressed) { - growAnim.stop(); - shrinkAnim.restart(); - } else if (!pressed && !shrinkAnim.running) { - growAnim.restart(); - } - } - // launch app handled by press animation - onClicked: launchAppRequested = true; - - ColumnLayout { - anchors { - fill: parent - leftMargin: Kirigami.Units.smallSpacing * 2 - topMargin: Kirigami.Units.smallSpacing * 2 - rightMargin: Kirigami.Units.smallSpacing * 2 - bottomMargin: Kirigami.Units.smallSpacing * 2 - } - spacing: 0 - - Kirigami.Icon { - id: icon - - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.fillWidth: true - Layout.minimumHeight: Math.min(Kirigami.Units.iconSizes.large, parent.height - delegate.reservedSpaceForLabel) - Layout.preferredHeight: Layout.minimumHeight - - source: modelData ? modelData.applicationIcon : "" - - Rectangle { - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - } - visible: model.applicationRunning - radius: width - width: Kirigami.Units.smallSpacing - height: width - color: Kirigami.Theme.highlightColor - } - - // darken effect when hovered/pressed - layer { - enabled: mouseArea.pressed || mouseArea.containsMouse - effect: ColorOverlay { - color: Qt.rgba(0, 0, 0, 0.3) - } - } - - //TODO: in loader? - Private.DelegateRemoveButton { - id: removeButton - desktopModel: delegate.desktopModel - } - } - - PC3.Label { - id: label - visible: text.length > 0 - - Layout.fillWidth: true - Layout.preferredHeight: delegate.reservedSpaceForLabel - Layout.alignment: Qt.AlignTop - Layout.topMargin: Kirigami.Units.smallSpacing - Layout.leftMargin: -parent.anchors.leftMargin + Kirigami.Units.smallSpacing * 2 - Layout.rightMargin: -parent.anchors.rightMargin + Kirigami.Units.smallSpacing * 2 - - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignTop - maximumLineCount: 2 - elide: Text.ElideRight - - text: model.applicationName - - font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 - font.weight: Font.Bold - color: "white" - - layer.enabled: true - layer.effect: DropShadow { - horizontalOffset: 0 - verticalOffset: 2 - radius: 6.0 - samples: 10 - cached: true - color: Qt.rgba(0, 0, 0, 0.3) - } - } - Item { Layout.fillHeight: true } - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/HomeScreen.qml b/containments/homescreens/folio/package/contents/ui/HomeScreen.qml index 77d6854b..ab6b54e5 100644 --- a/containments/homescreens/folio/package/contents/ui/HomeScreen.qml +++ b/containments/homescreens/folio/package/contents/ui/HomeScreen.qml @@ -1,148 +1,407 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.1 +import QtQuick +import QtQuick.Window +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Controls as QQC2 + +import org.kde.kirigami 2.20 as Kirigami import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.draganddrop 2.0 as DragDrop - -import "private" as Private -import "appdrawer" - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager - +import org.kde.plasma.components 3.0 as PC3 +import org.kde.plasma.private.mobileshell 1.0 as MobileShell import org.kde.private.mobile.homescreen.folio 1.0 as Folio +import "./delegate" +import "./settings" + Item { id: root - required property real topMargin - required property real bottomMargin - required property real leftMargin - required property real rightMargin - + property real topMargin: 0 + property real bottomMargin: 0 + property real leftMargin: 0 + property real rightMargin: 0 + property bool interactive: true - property var homeScreenState: HomeScreenState { - interactive: root.interactive + property Folio.HomeScreenState homeScreenState: Folio.HomeScreenState - totalPagesWidth: pages.contentWidth - - appDrawerFlickable: appDrawer.flickable - - availableScreenHeight: height - root.topMargin - root.bottomMargin - availableScreenWidth: width - root.leftMargin - root.rightMargin - - appDrawerBottomOffset: favoriteStrip.height - } - - property alias appDrawer: appDrawerLoader.item - property alias homeScreenContents: contents - - Component.onCompleted: { - // ensure that homescreen is on first page - homeScreenState.goToPageIndex(0); - homeScreenState.resetSwipeState(); + readonly property bool dropAnimationRunning: delegateDragItem.dropAnimationRunning + + readonly property real settingsModeHomeScreenScale: 0.8 + + onTopMarginChanged: Folio.HomeScreenState.viewTopPadding = root.topMargin + onBottomMarginChanged: Folio.HomeScreenState.viewBottomPadding = root.bottomMargin + onLeftMarginChanged: Folio.HomeScreenState.viewLeftPadding = root.leftMargin + onRightMarginChanged: Folio.HomeScreenState.viewRightPadding = root.rightMargin + + // called by any delegates when starting drag + // returns the mapped coordinates to be used in the home screen state + function prepareStartDelegateDrag(delegate, item) { + swipeArea.setSkipSwipeThreshold(true); + + delegateDragItem.delegate = delegate; + return root.mapFromItem(item, 0, 0); } - property var desktopModel: Folio.DesktopModel {} - - // the parent of the homescreen is a flickable that captures all flicks - FlickContainer { - id: flickContainer + function cancelDelegateDrag() { + homeScreenState.cancelDelegateDrag(); + } + + // sets the coordinates for the folder opening/closing animation + function prepareFolderOpen(item) { + return root.mapFromItem(item, 0, 0); + } + + function openConfigure() { + Plasmoid.internalAction("configure").trigger(); + } + + // determine how tall an app label is, for delegate measurements + DelegateLabel { + id: appLabelMetrics + text: "M\nM" + visible: false + + onHeightChanged: Folio.HomeScreenState.pageDelegateLabelHeight = appLabelMetrics.height + + Component.onCompleted: { + Folio.HomeScreenState.pageDelegateLabelWidth = Kirigami.Units.smallSpacing; + } + } + + // determine screen dimensions + Item { + id: screenDimensions anchors.fill: parent - - homeScreenState: root.homeScreenState - - // disable flick tracking when necessary - interactive: root.interactive && homeScreenState.currentView !== HomeScreenState.AppDrawerView && !contents.inAppletEditMode - // item is effectively anchored to root, while allowing flickContainer - // to keep track of flicks + onWidthChanged: Folio.HomeScreenState.viewWidth = width; + onHeightChanged: Folio.HomeScreenState.viewHeight = height; + } + + // area that can be swiped + MobileShell.SwipeArea { + id: swipeArea + anchors.fill: parent + + interactive: root.interactive && + !appDrawer.flickable.moving && + (appDrawer.flickable.atYBeginning || // disable the swipe area when we are swiping in the app drawer, and not in drag-and-drop + Folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate || + Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate || + Folio.HomeScreenState.swipeState === Folio.HomeScreenState.SwipingAppDrawerGrid) + + onSwipeStarted: { + homeScreenState.swipeStarted(); + } + onSwipeEnded: { + homeScreenState.swipeEnded(); + } + onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => { + homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY); + } + + SettingsComponent { + id: settings + anchors.fill: parent + opacity: Folio.HomeScreenState.settingsOpenProgress + visible: opacity > 0 + z: 1 + + settingsModeHomeScreenScale: root.settingsModeHomeScreenScale + homeScreen: root + + onRequestLeaveSettingsMode: root.leaveSettingsMode(); + } + Item { - x: flickContainer.contentX - y: flickContainer.contentY - width: flickContainer.width - height: flickContainer.height - - // horizontal pages - HomeScreenPages { - id: pages - homeScreenState: root.homeScreenState + id: mainHomeScreen + anchors.fill: parent + + // we stop showing halfway through the animation + opacity: 1 - Math.max(homeScreenState.appDrawerOpenProgress, homeScreenState.searchWidgetOpenProgress, homeScreenState.folderOpenProgress) * 2 + visible: opacity > 0 // prevent handlers from picking up events + + transform: [ + Scale { + origin.x: mainHomeScreen.width / 2 + origin.y: mainHomeScreen.height / 2 + yScale: 1 - (homeScreenState.appDrawerOpenProgress * 2) * 0.1 + xScale: 1 - (homeScreenState.appDrawerOpenProgress * 2) * 0.1 + } + ] + + HomeScreenPages { + id: homeScreenPages + homeScreen: root + + anchors.topMargin: root.topMargin + anchors.leftMargin: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? 0 : root.leftMargin + anchors.rightMargin: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? 0 : root.rightMargin + anchors.bottomMargin: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? 0 : root.bottomMargin + + // update the model with page dimensions + onWidthChanged: { + homeScreenState.pageWidth = homeScreenPages.width; + } + onHeightChanged: { + homeScreenState.pageHeight = homeScreenPages.height; + } + + transform: [ + Scale { + // animation when settings opens + property real scaleFactor: 1 - Folio.HomeScreenState.settingsOpenProgress * (1 - settingsModeHomeScreenScale) + origin.x: root.leftMargin + (root.width - root.rightMargin - root.leftMargin) / 2 + origin.y: root.height * settingsModeHomeScreenScale / 2 + xScale: scaleFactor + yScale: scaleFactor + } + ] + + states: [ + State { + name: "bottom" + when: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom + AnchorChanges { + target: homeScreenPages + anchors.top: parent.top + anchors.bottom: favouritesBar.top + anchors.left: parent.left + anchors.right: parent.right + } + }, State { + name: "left" + when: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left + AnchorChanges { + target: homeScreenPages + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: favouritesBar.right + anchors.right: parent.right + } + }, State { + name: "right" + when: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right + AnchorChanges { + target: homeScreenPages + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: favouritesBar.left + } + } + ] + } + + Rectangle { + id: favouritesBarScrim + color: Qt.rgba(255, 255, 255, 0.2) + + // don't show in settings mode + opacity: 1 - Folio.HomeScreenState.settingsOpenProgress + visible: Folio.FolioSettings.showFavouritesBarBackground + + anchors.top: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? favouritesBar.top : parent.top + anchors.bottom: parent.bottom + anchors.left: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? favouritesBar.left : parent.left + anchors.right: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? favouritesBar.right : parent.right + + // because of the scale animation, we need to extend the panel out a bit + anchors.topMargin: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? 0 : -Kirigami.Units.gridUnit * 5 + anchors.bottomMargin: -Kirigami.Units.gridUnit * 5 + anchors.leftMargin: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? 0 : -Kirigami.Units.gridUnit * 5 + anchors.rightMargin: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? 0 : -Kirigami.Units.gridUnit * 5 + } + + FavouritesBar { + id: favouritesBar + homeScreen: root + leftMargin: root.leftMargin + topMargin: root.topMargin + + // don't show in settings mode + opacity: 1 - Folio.HomeScreenState.settingsOpenProgress + visible: opacity > 0 + + // one is ignored as anchors are set + height: Kirigami.Units.gridUnit * 6 + width: Kirigami.Units.gridUnit * 6 - // account for panels - anchors.fill: parent anchors.topMargin: root.topMargin anchors.bottomMargin: root.bottomMargin anchors.leftMargin: root.leftMargin anchors.rightMargin: root.rightMargin - - // animation when app drawer is being shown - opacity: root.appDrawer ? 1 - root.appDrawer.openFactor : 1 - transform: Translate { - y: root.appDrawer ? (-pages.height / 20) * root.appDrawer.openFactor : 0 - } - contentWidth: Math.max(width, width * Math.ceil(contents.itemsBoundingRect.width/width)) + (contents.launcherDragManager.active ? width : 0) - showAddPageIndicator: contents.launcherDragManager.active - - HomeScreenContents { - id: contents - desktopModel: root.desktopModel - homeScreenState: root.homeScreenState - - height: pages.height - width: pages.width * 100 - - favoriteStrip: favoriteStrip - homeScreenPages: pages - } - - footer: FavoriteStrip { - id: favoriteStrip - - appletsLayout: contents.appletsLayout - visible: favoriteStrip.flow.children.length > 0 || contents.launcherDragManager.active || contents.containsDrag - opacity: contents.launcherDragManager.active && root.desktopModel.favoriteCount >= root.desktopModel.maxFavoriteCount ? 0.3 : 1 - - TapHandler { - target: favoriteStrip - enabled: flickContainer.interactive // only interactive when we flick on homescreen - onTapped: { - //Hides icons close button - contents.appletsLayout.appletsLayoutInteracted(); - contents.appletsLayout.editMode = false; + states: [ + State { + name: "bottom" + when: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom + AnchorChanges { + target: favouritesBar + anchors.top: undefined + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right } - onLongPressed: { - if (homeScreenState.currentSwipeState === HomeScreenState.DeterminingType) { - // only go into edit mode when not in a swipe - contents.appletsLayout.editMode = true; - } + PropertyChanges { + target: favouritesBar + height: Kirigami.Units.gridUnit * 6 } - onPressedChanged: root.parent.focus = true; + }, State { + name: "left" + when: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left + AnchorChanges { + target: favouritesBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: undefined + } + PropertyChanges { + target: favouritesBar + width: Kirigami.Units.gridUnit * 6 + } + }, State { + name: "right" + when: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right + AnchorChanges { + target: favouritesBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: undefined + anchors.right: parent.right + } + PropertyChanges { + target: favouritesBar + width: Kirigami.Units.gridUnit * 6 + } + } + ] + } + + Item { + id: pageIndicatorWrapper + property bool favouritesBarAtBottom: Folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom + + // don't show in settings mode + opacity: 1 - Folio.HomeScreenState.settingsOpenProgress + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: favouritesBarAtBottom ? favouritesBar.top : parent.bottom + + anchors.topMargin: root.topMargin + anchors.leftMargin: root.leftMargin + anchors.rightMargin: root.rightMargin + anchors.bottomMargin: favouritesBarAtBottom ? 0 : (root.bottomMargin + Kirigami.Units.largeSpacing) + + QQC2.PageIndicator { + visible: count > 1 + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + + currentIndex: Folio.HomeScreenState.currentPage + count: Folio.PageListModel.length + } + } + } + + // folder view + FolderView { + id: folderView + anchors.fill: parent + anchors.topMargin: root.topMargin + anchors.leftMargin: root.leftMargin + anchors.rightMargin: root.rightMargin + anchors.bottomMargin: root.bottomMargin + + homeScreen: root + opacity: homeScreenState.folderOpenProgress + transform: Translate { y: folderView.opacity > 0 ? 0 : folderView.height } + } + + // drag and drop component + DelegateDragItem { + id: delegateDragItem + } + + // bottom app drawer + AppDrawer { + id: appDrawer + width: parent.width + height: parent.height + + homeScreen: root + + // we only start showing it halfway through + opacity: homeScreenState.appDrawerOpenProgress < 0.5 ? 0 : (homeScreenState.appDrawerOpenProgress - 0.5) * 2 + + // position for animation + property real animationY: (1 - homeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2) + + // move the app drawer out of the way if it is not visible + // NOTE: we do this instead of setting visible to false, because + // it doesn't mess with app drag and drop from the app drawer + y: (opacity > 0) ? animationY : parent.height + + headerHeight: Math.round(Kirigami.Units.gridUnit * 5) + headerItem: AppDrawerHeader {} + + // account for panels + topPadding: root.topMargin + bottomPadding: root.bottomMargin + leftPadding: root.leftMargin + rightPadding: root.rightMargin + + Connections { + target: Folio.HomeScreenState + + function onAppDrawerClosed() { + // reset app drawer position when closed + appDrawer.flickable.contentY = 0; + } + } + } + + // search component + MobileShell.KRunnerScreen { + id: searchWidget + anchors.fill: parent + + opacity: homeScreenState.searchWidgetOpenProgress + visible: opacity > 0 + transform: Translate { y: (1 - homeScreenState.searchWidgetOpenProgress) * (-Kirigami.Units.gridUnit * 2) } + + // focus the search bar if it opens + Connections { + target: Folio.HomeScreenState + + function onSearchWidgetOpenProgressChanged() { + if (homeScreenState.searchWidgetOpenProgress === 1.0) { + searchWidget.requestFocus(); + } else { + // TODO this gets called a lot, can we have a more performant way? + root.forceActiveFocus(); } } } - - // app drawer - AppDrawerLoader { - id: appDrawerLoader - anchors.fill: parent - homeScreenState: root.homeScreenState - - // account for panels - topPadding: root.topMargin - bottomPadding: root.bottomMargin - leftPadding: root.leftMargin - rightPadding: root.rightMargin + + onRequestedClose: { + homeScreenState.closeSearchWidget(); } + + anchors.topMargin: root.topMargin + anchors.bottomMargin: root.bottomMargin + anchors.leftMargin: root.leftMargin + anchors.rightMargin: root.rightMargin } } } diff --git a/containments/homescreens/folio/package/contents/ui/HomeScreenContents.qml b/containments/homescreens/folio/package/contents/ui/HomeScreenContents.qml deleted file mode 100644 index 3be702db..00000000 --- a/containments/homescreens/folio/package/contents/ui/HomeScreenContents.qml +++ /dev/null @@ -1,239 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.1 -import Qt5Compat.GraphicalEffects - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.draganddrop 2.0 as DragDrop - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager - -import org.kde.private.mobile.homescreen.folio 1.0 as Folio - -import "private" as Private - -DragDrop.DropArea { - id: dropArea - - required property var homeScreenState - - required property Folio.DesktopModel desktopModel - property var applicationListModel: Folio.ApplicationListModel - - property alias launcherDelegate: launcherRepeater.delegate - property alias launcherModel: launcherRepeater.model - property alias launcherRepeater: launcherRepeater - property alias itemsBoundingRect: appletsLayout.childrenRect - - property alias appletsLayout: appletsLayout - - property FavoriteStrip favoriteStrip - property HomeScreenPages homeScreenPages - - property LauncherDragManager launcherDragManager: LauncherDragManager { - id: launcherDragManager - parent: { - let candidate = dropArea; - while (candidate.parent) { - candidate = candidate.parent; - } - return candidate; - } - anchors.fill: parent - z: 999999 - appletsLayout: dropArea.appletsLayout - favoriteStrip: dropArea.favoriteStrip - desktopModel: dropArea.desktopModel - } - - property bool inAppletEditMode: false - - - Connections { - target: plasmoid - function onEditModeChanged() { - appletsLayout.editMode = plasmoid.editMode - } - } - - onDragEnter: { - event.accept(event.proposedAction); - launcherDragManager.active = true; - } - - onDragMove: { - let posInFavorites = favoriteStrip.mapFromItem(this, event.x, event.y); - if (posInFavorites.y > 0) { - if (desktopModel.favoriteCount >= desktopModel.maxFavoriteCount) { - launcherDragManager.hideSpacer(); - } else { - launcherDragManager.showSpacerAtPos(event.x, event.y, favoriteStrip); - } - appletsLayout.hidePlaceHolder(); - } else { - appletsLayout.showPlaceHolderAt( - Qt.rect(event.x - appletsLayout.defaultItemWidth / 2, - event.y - appletsLayout.defaultItemHeight / 2, - appletsLayout.defaultItemWidth, - appletsLayout.defaultItemHeight) - ); - launcherDragManager.hideSpacer(); - - let scenePos = mapToItem(null, event.x, event.y); - //SCROLL LEFT - if (scenePos.x < Kirigami.Units.gridUnit) { - homeScreenPages.scrollLeft(); - //SCROLL RIGHT - } else if (scenePos.x > homeScreenPages.width - Kirigami.Units.gridUnit) { - homeScreenPages.scrollRight(); - //DON't SCROLL - } else { - homeScreenPages.stopScroll(); - } - } - } - - onDragLeave: { - appletsLayout.hidePlaceHolder(); - launcherDragManager.active = false; - } - - preventStealing: true - - onDrop: { - launcherDragManager.active = false; - if (event.mimeData.formats[0] === "text/x-plasma-phone-homescreen-launcher") { - let storageId = event.mimeData.getDataAsByteArray("text/x-plasma-phone-homescreen-launcher"); - - let posInFavorites = favoriteStrip.flow.mapFromItem(this, event.x, event.y); - if (posInFavorites.y > 0) { - if (desktopModel.favoriteCount >= desktopModel.maxFavoriteCount ) { - return; - } - - let pos = Math.min(desktopModel.count, Math.floor(posInFavorites.x/favoriteStrip.cellWidth)) - desktopModel.addFavorite(storageId, pos, Folio.ApplicationListModel.Favorites) - let item = launcherRepeater.itemAt(pos); - - if (item) { - item.x = posInFavorites.x; - item.y = 0//posInFavorites.y; - - //launcherDragManager.showSpacer(item, item.width/2, item.height/2); - launcherDragManager.dropItem(item, item.width/2, item.height/2); - } - - return; - } - - let pos = desktopModel.count; - desktopModel.addFavorite(storageId, pos, Folio.ApplicationListModel.Desktop) - let item = launcherRepeater.itemAt(pos); - - event.accept(event.proposedAction); - if (item) { - item.x = appletsLayout.placeHolder.x; - item.y = appletsLayout.placeHolder.y; - appletsLayout.hidePlaceHolder(); - launcherDragManager.dropItem(item, appletsLayout.placeHolder.x + appletsLayout.placeHolder.width/2, appletsLayout.placeHolder.y + appletsLayout.placeHolder.height/2); - } - appletsLayout.hidePlaceHolder(); - } else { - plasmoid.processMimeData(event.mimeData, - event.x - appletsLayout.placeHolder.width / 2, event.y - appletsLayout.placeHolder.height / 2); - event.accept(event.proposedAction); - appletsLayout.hidePlaceHolder(); - } - } - - ContainmentLayoutManager.AppletsLayout { - id: appletsLayout - - anchors { - fill: parent - bottomMargin: dropArea.favoriteStrip ? dropArea.favoriteStrip.height : 0 - } - - signal appletsLayoutInteracted - - TapHandler { - target: homeScreenPages - enabled: homeScreenState.currentView === HomeScreenState.PageView && homeScreenState.interactive - onTapped: { - //Hides icons close button - appletsLayout.appletsLayoutInteracted(); - appletsLayout.editMode = false; - appletsLayout.forceActiveFocus(); - } - onLongPressed: { - if (homeScreenState.currentSwipeState === HomeScreenState.DeterminingType) { - // only go into edit mode when not in a swipe - appletsLayout.editMode = true; - } - } - onPressedChanged: appletsLayout.focus = true; - } - - cellWidth: favoriteStrip.cellWidth - cellHeight: Math.floor(height / Math.floor(height / favoriteStrip.cellHeight)) - - configKey: width > height ? "ItemGeometriesHorizontal" : "ItemGeometriesVertical" - containment: plasmoid - editModeCondition: plasmoid.immutable - ? ContainmentLayoutManager.AppletsLayout.Manual - : ContainmentLayoutManager.AppletsLayout.AfterPressAndHold - - // Sets the containment in edit mode when we go in edit mode as well - onEditModeChanged: plasmoid.editMode = editMode; - - minimumItemWidth: Kirigami.Units.gridUnit * 3 - minimumItemHeight: minimumItemWidth - - defaultItemWidth: Kirigami.Units.gridUnit * 6 - defaultItemHeight: defaultItemWidth - - acceptsAppletCallback: function(applet, x, y) { - print("Applet: "+applet+" "+x+" "+y) - return true; - } - appletContainerComponent: MobileAppletContainer { - homeScreenState: dropArea.homeScreenState - launcherDragManager: dropArea.launcherDragManager - - onEditModeChanged: { - inAppletEditMode = editMode; - } - } - - placeHolder: ContainmentLayoutManager.PlaceHolder {} - //FIXME: move - PlasmaComponents.Label { - id: metrics - text: "M\nM" - visible: false - font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.9 - } - LauncherRepeater { - id: launcherRepeater - desktopModel: dropArea.desktopModel - homeScreenState: dropArea.homeScreenState - cellWidth: appletsLayout.cellWidth - cellHeight: appletsLayout.cellHeight - appletsLayout: appletsLayout - favoriteStrip: dropArea.favoriteStrip - onScrollLeftRequested: homeScreenPages.scrollLeft() - onScrollRightRequested: homeScreenPages.scrollRight() - onStopScrollRequested: homeScreenPages.stopScroll() - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/HomeScreenPage.qml b/containments/homescreens/folio/package/contents/ui/HomeScreenPage.qml new file mode 100644 index 00000000..ee0dd450 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/HomeScreenPage.qml @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Window +import QtQuick.Layouts + +import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState +import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.private.mobile.homescreen.folio 1.0 as Folio +import org.kde.kirigami 2.10 as Kirigami + +import "./delegate" + +Item { + id: root + + property int pageNum + + property var pageModel + property var homeScreen + + // background when in settings view (for rearranging pages) + Rectangle { + id: settingsViewBackground + anchors.fill: parent + color: Qt.rgba(255, 255, 255, 0.2) + opacity: Folio.HomeScreenState.settingsOpenProgress + radius: Kirigami.Units.largeSpacing + } + + // square that shows when hovering over a spot to drop a delegate on + PlaceholderDelegate { + id: dragDropFeedback + width: Folio.HomeScreenState.pageCellWidth + height: Folio.HomeScreenState.pageCellHeight + + property var dropPosition: Folio.HomeScreenState.dragState.candidateDropPosition + + // only show if it is an empty spot on this page + visible: Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && + dropPosition.location === Folio.DelegateDragPosition.Pages && + dropPosition.page === root.pageNum && + Folio.HomeScreenState.getPageDelegateAt(root.pageNum, dropPosition.pageRow, dropPosition.pageColumn) === null + + x: dropPosition.pageColumn * Folio.HomeScreenState.pageCellWidth + y: dropPosition.pageRow * Folio.HomeScreenState.pageCellHeight + } + + Repeater { + model: root.pageModel + + delegate: Item { + id: delegate + + property Folio.FolioPageDelegate pageDelegate: model.delegate + property int row: pageDelegate.row + property int column: pageDelegate.column + + property var dragState: Folio.HomeScreenState.dragState + + property bool isDropPositionThis: dragState.candidateDropPosition.location === Folio.DelegateDragPosition.Pages && + dragState.candidateDropPosition.page === root.pageNum && + dragState.candidateDropPosition.pageRow === delegate.pageDelegate.row && + dragState.candidateDropPosition.pageColumn === delegate.pageDelegate.column + + property bool isAppHoveredOver: Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && + dragState.dropDelegate && + dragState.dropDelegate.type === Folio.FolioDelegate.Application && + isDropPositionThis + + implicitWidth: Folio.HomeScreenState.pageCellWidth + implicitHeight: Folio.HomeScreenState.pageCellHeight + width: Folio.HomeScreenState.pageCellWidth + height: Folio.HomeScreenState.pageCellHeight + + x: column * Folio.HomeScreenState.pageCellWidth + y: row * Folio.HomeScreenState.pageCellHeight + + visible: row >= 0 && row < Folio.HomeScreenState.pageRows && + column >= 0 && column < Folio.HomeScreenState.pageColumns + + Loader { + anchors.fill: parent + + sourceComponent: { + if (delegate.pageDelegate.type === Folio.FolioDelegate.Application) { + return appComponent; + } else if (delegate.pageDelegate.type === Folio.FolioDelegate.Folder) { + return folderComponent; + } else { + return noneComponent; + } + } + } + + Component { + id: noneComponent + + Item {} + } + + Component { + id: appComponent + + AppDelegate { + id: appDelegate + name: Folio.FolioSettings.showPagesAppLabels ? delegate.pageDelegate.application.name : "" + application: delegate.pageDelegate.application + turnToFolder: delegate.isAppHoveredOver + turnToFolderAnimEnabled: Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate + + // do not show if the drop animation is running to this delegate + visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis) + + // don't show label in drag and drop mode + labelOpacity: delegate.opacity + + onPressAndHold: { + let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.pageDelegate, appDelegate.delegateItem); + Folio.HomeScreenState.startDelegatePageDrag( + mappedCoords.x, + mappedCoords.y, + root.pageNum, + delegate.pageDelegate.row, + delegate.pageDelegate.column + ); + + contextMenu.open(); + } + onPressAndHoldReleased: { + // cancel the event if the delegate is not dragged + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) { + homeScreen.cancelDelegateDrag(); + } + } + + onRightMousePress: { + contextMenu.open(); + } + + // TODO don't use loader, and move outside to a page to make it more performant + ContextMenuLoader { + id: contextMenu + + // close menu when drag starts + Connections { + target: Folio.HomeScreenState + + function onSwipeStateChanged() { + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) { + contextMenu.close(); + } + } + } + + actions: [ + Kirigami.Action { + icon.name: "emblem-favorite" + text: i18n("Remove") + onTriggered: root.pageModel.removeDelegate(delegate.row, delegate.column) + } + ] + } + } + } + + Component { + id: folderComponent + + AppFolderDelegate { + id: appFolderDelegate + name: Folio.FolioSettings.showPagesAppLabels ? delegate.pageDelegate.folder.name : "" + folder: delegate.pageDelegate.folder + + // do not show if the drop animation is running to this delegate, and the drop delegate is a folder + visible: !(root.homeScreen.dropAnimationRunning && + delegate.isDropPositionThis && + delegate.dragState.dropDelegate.type === Folio.FolioDelegate.Folder) + + // don't show label in drag and drop mode + labelOpacity: delegate.opacity + + appHoveredOver: delegate.isAppHoveredOver + + onAfterClickAnimation: { + const pos = homeScreen.prepareFolderOpen(appFolderDelegate.contentItem); + Folio.HomeScreenState.openFolder(pos.x, pos.y, folder); + } + + onPressAndHold: { + let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.pageDelegate, appFolderDelegate.delegateItem); + Folio.HomeScreenState.startDelegatePageDrag( + mappedCoords.x, + mappedCoords.y, + root.pageNum, + delegate.pageDelegate.row, + delegate.pageDelegate.column + ); + + contextMenu.open(); + } + + onPressAndHoldReleased: { + // cancel the event if the delegate is not dragged + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) { + homeScreen.cancelDelegateDrag(); + } + } + + onRightMousePress: { + contextMenu.open(); + } + + // TODO don't use loader, and move outside to a page to make it more performant + ContextMenuLoader { + id: contextMenu + + // close menu when drag starts + Connections { + target: Folio.HomeScreenState + + function onSwipeStateChanged() { + if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) { + contextMenu.close(); + } + } + } + + actions: [ + Kirigami.Action { + icon.name: "emblem-favorite" + text: i18n("Remove") + onTriggered: root.pageModel.removeDelegate(delegate.row, delegate.column) + } + ] + } + } + } + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/HomeScreenPages.qml b/containments/homescreens/folio/package/contents/ui/HomeScreenPages.qml index aed157ad..bf276ba3 100644 --- a/containments/homescreens/folio/package/contents/ui/HomeScreenPages.qml +++ b/containments/homescreens/folio/package/contents/ui/HomeScreenPages.qml @@ -1,161 +1,46 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.1 +import QtQuick +import QtQuick.Window +import QtQuick.Layouts -import org.kde.kirigami 2.20 as Kirigami +import org.kde.plasma.components 3.0 as PC3 +import org.kde.kirigami 2.10 as Kirigami +import org.kde.private.mobile.homescreen.folio 1.0 as Folio -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.draganddrop 2.0 as DragDrop +MouseArea { + id: root -import "private" as Private + property var homeScreen -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager + readonly property real verticalMargin: Math.round((Folio.HomeScreenState.pageHeight - Folio.HomeScreenState.pageContentHeight) / 2) + readonly property real horizontalMargin: Math.round((Folio.HomeScreenState.pageWidth - Folio.HomeScreenState.pageContentWidth) / 2) -import org.kde.plasma.private.mobileshell 1.0 as MobileShell + onPressAndHold: Folio.HomeScreenState.openSettingsView() -MobileShell.Flickable { - id: mainFlickable - - required property var homeScreenState - - property Item footer + Repeater { + model: Folio.PageListModel - property bool showAddPageIndicator: false + delegate: HomeScreenPage { + id: homeScreenPage + pageNum: model.index + pageModel: model.delegate + homeScreen: root.homeScreen - contentX: homeScreenState.xPosition - - contentHeight: height - interactive: false + anchors.fill: root + anchors.leftMargin: root.horizontalMargin + anchors.rightMargin: root.horizontalMargin + anchors.topMargin: root.verticalMargin + anchors.bottomMargin: root.verticalMargin - signal cancelEditModeForItemsRequested - onDragStarted: cancelEditModeForItemsRequested() - onDragEnded: cancelEditModeForItemsRequested() - onFlickStarted: cancelEditModeForItemsRequested() - onFlickEnded: cancelEditModeForItemsRequested() + // animation so that full opacity is only when the page is in view + opacity: 1 - Math.min(1, Math.max(0, Math.abs(-Folio.HomeScreenState.pageViewX - root.width * pageNum) / root.width)) - onFooterChanged: { - if (footer) { - footer.parent = mainFlickable; - footer.anchors.left = mainFlickable.left; - footer.anchors.bottom = mainFlickable.bottom; - footer.anchors.right = mainFlickable.right; - } - } - - // autoscroll between pages (when holding a delegate to go to a new page) - function scrollLeft() { - if (mainFlickable.atXBeginning) { - return; - } - autoScrollTimer.scrollRight = false; - autoScrollTimer.running = true; - scrollLeftIndicator.opacity = 1; - scrollRightIndicator.opacity = 0; - } - - function scrollRight() { - if (mainFlickable.atXEnd) { - return; - } - autoScrollTimer.scrollRight = true; - autoScrollTimer.running = true; - scrollLeftIndicator.opacity = 0; - scrollRightIndicator.opacity = 1; - } - - function stopScroll() { - autoScrollTimer.running = false; - scrollLeftIndicator.opacity = 0; - scrollRightIndicator.opacity = 0; - } - - Timer { - id: autoScrollTimer - property bool scrollRight: true - repeat: true - interval: 1500 - onTriggered: { - homeScreenState.animateGoToPageIndex(Math.max(0, homeScreenState.currentPageIndex + (scrollRight ? 1 : -1)), Kirigami.Units.longDuration * 2); - } - } - - PlasmaComponents.PageIndicator { - id: pageIndicator - anchors { - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - bottomMargin: mainFlickable.footer ? mainFlickable.footer.height : 0 - } - - Kirigami.Theme.inherit: false - Kirigami.Theme.colorSet: Kirigami.Theme.Complementary - - parent: mainFlickable - visible: count > 1 - - count: homeScreenState.pagesCount - currentIndex: homeScreenState.currentPageIndex - - delegate: Rectangle { - property bool isAddPageIndicator: index === pageIndicator.count-1 && mainFlickable.showAddPageIndicator - implicitWidth: Kirigami.Units.gridUnit/2 - implicitHeight: implicitWidth - - radius: width - color: isAddPageIndicator ? "transparent" : Kirigami.Theme.textColor - - PlasmaComponents.Label { - anchors.centerIn: parent - visible: parent.isAddPageIndicator - text: "⊕" + // x position of page + transform: Translate { + x: root.width * index + Folio.HomeScreenState.pageViewX } - - opacity: index === pageIndicator.currentIndex ? 0.9 : pressed ? 0.7 : 0.5 - Behavior on opacity { - OpacityAnimator { - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - } - } - } - - Item { - z: 9999999 - anchors.fill: parent - parent: { - let candidate = mainFlickable; - while (candidate.parent) { - candidate = candidate.parent; - } - return candidate; - } - - Private.ScrollIndicator { - id: scrollLeftIndicator - anchors { - left: parent.left - leftMargin: Kirigami.Units.smallSpacing - } - elementId: "left-arrow" - } - Private.ScrollIndicator { - id: scrollRightIndicator - anchors { - right: parent.right - rightMargin: Kirigami.Units.smallSpacing - } - elementId: "right-arrow" } } } - - diff --git a/containments/homescreens/folio/package/contents/ui/HomeScreenState.qml b/containments/homescreens/folio/package/contents/ui/HomeScreenState.qml deleted file mode 100644 index e452e939..00000000 --- a/containments/homescreens/folio/package/contents/ui/HomeScreenState.qml +++ /dev/null @@ -1,434 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.15 - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.private.mobileshell 1.0 as MobileShell - -/** - * State object for the homescreen. - * - * We expose the data necessary to make custom "swipe-down" gestures from the page view. - */ -QtObject { - id: root - - // whether the homescreen elements should be interactive, or disabled - required property bool interactive - - required property real totalPagesWidth - - required property var appDrawerFlickable - - // dimensions of the homescreen area (not including top panel and task panel) - required property real availableScreenHeight - required property real availableScreenWidth - - // offset from the bottom of the screen that the app drawer starts from, - // would be the height favourites strip - required property real appDrawerBottomOffset - - // ~~ positioning ~~ - - // xPosition: - // We start at 0, which is the beginning x position of the row of pages (left-most side). - // Increasing x moves *right* toward the next page. - // - // yPosition: - // Increasing y results in moving *up* in the view. - // appDrawerOpenYPosition - The app drawer is opened (app drawer flickable is active iff it's not at the beginning). - // pagesYPosition - The app drawer is closed. Homescreen pages are visible, can swipe left/right between pages. - property real xPosition: 0 - property real yPosition: pagesYPosition - - // direction of the movement - property bool movingRight: false - property bool movingUp: false - - // used for calculating movement direction - property real oldXPosition: 0 - property real oldYPosition: 0 - onXPositionChanged: { - movingRight = xPosition > oldXPosition; - oldXPosition = xPosition; - } - onYPositionChanged: { - movingUp = yPosition > oldYPosition; - oldYPosition = yPosition; - - // speed up the animation - if (currentSwipeState == HomeScreenState.SwipingAppDrawerVisibility && yPosition <= 0) { - root.currentView = HomeScreenState.AppDrawerBeginningView; - root.resetSwipeState(); - openDrawerAnim.stop(); - } - } - - // yPosition when the homescreen pages are visible - readonly property real pagesYPosition: availableScreenHeight - appDrawerBottomOffset - - // yPosition when drawer is open - readonly property real appDrawerOpenYPosition: 0 - - // ~~ active state ~~ - - enum View { - PageView, // we are viewing the horizontal row of pages - AppDrawerBeginningView, // we are at the top of the app drawer (could either close it or scroll down) - AppDrawerView // we are in the app drawer, and not at the top of it - } - - // the current view of the homescreen - property var currentView: HomeScreenState.PageView - - // number of homescreen pages - readonly property int pagesCount: Math.floor(totalPagesWidth / pageWidth) - - // current homescreen page index - readonly property int currentPageIndex: { - let candidateIndex = Math.round(xPosition / (pageSpacing + pageWidth)); - return Math.max(0, Math.min(pagesCount - 1, candidateIndex)); - } - - enum PageViewSwipeState { - SwipingPages, // horizontal movement between pages - SwipingAppDrawerVisibility, // opening/closing app drawer - SwipingAppDrawerList, // scrolling app drawer - SwipingPagesDown, // custom gesture can be implemented for swiping down on the page view - DeterminingType - } - - // when we are at the PageView view, we need to distinguish horizontal swipes (changing pages) - // and vertical swipes (opening drawer) - property var currentSwipeState: HomeScreenState.DeterminingType - - // threshold of movement in a direction before we count that as the defining SwipeState - readonly property real horizontalSwipeStateDetermineThreshold: 2 - readonly property real verticalSwipeStateDetermineThreshold: 2 - - // we put the offset position here when determining the swipe type, before we - // transfer movement over to xPosition and yPosition - property real xDetermineSwipePosition: 0 - property real yDetermineSwipePosition: 0 - - // whether animations are currently running - property bool animationsRunning: openDrawerAnim.running || closeDrawerAnim.running || xAnim.running - - // whether the app drawer flickable should be interactive - property bool appDrawerInteractive: currentView === HomeScreenState.AppDrawerView - - // ~~ measurement constants ~~ - - // dimensions of a page - readonly property real pageHeight: availableScreenHeight - readonly property real pageWidth: availableScreenWidth - - // spacing between each homescreen page - readonly property real pageSpacing: 0 - - // ~~ signals and functions ~~ - - // cancel edit mode - signal cancelEditModeForItemsRequested - - // cancel all animated moving, as another flick source is taking over - signal cancelAnimations() - onCancelAnimations: { - openDrawerAnim.stop(); - closeDrawerAnim.stop(); - xAnim.stop(); - } - - // expose signals necessary to implement any behaviour for the "swipe-down" action on the page view - signal swipeDownGestureBegin - signal swipeDownGestureEnd - signal swipeDownGestureOffset(real value) - - // be very careful when resetting the swipe state - // ensure that we aren't in the middle of a gesture - function resetSwipeState() { - currentSwipeState = HomeScreenState.DeterminingType; - xDetermineSwipePosition = 0; - yDetermineSwipePosition = 0; - } - - function openAppDrawer() { - openDrawerAnim.restart(); - } - - function openAppDrawerInstantly() { - yPosition = appDrawerOpenYPosition; - currentView = HomeScreenState.AppDrawerBeginningView; - } - - function closeAppDrawer() { - closeDrawerAnim.restart(); - } - - function closeAppDrawerInstantly() { - yPosition = pagesYPosition; - currentView = HomeScreenState.PageView; - } - - // get the xPosition where the page will be centered on the screen - function xPositionFromPageIndex(index) { - return index * (pageWidth + pageSpacing); - } - - // instantly go to the page index - function goToPageIndex(index) { - xPosition = xPositionFromPageIndex(index); - } - - // go to the page index, animated - function animateGoToPageIndex(index, duration) { - xAnim.duration = duration; - xAnim.to = xPositionFromPageIndex(index); - xAnim.restart(); - } - - // update the position using an offset - // called by swipe provider flickable - function updatePositionWithOffset(x, y) { - switch (currentView) { - case HomeScreenState.PageView: { - switch (currentSwipeState) { - case HomeScreenState.DeterminingType: - xDetermineSwipePosition += x; - yDetermineSwipePosition += y; - - // check if a swipetype can be determined and started - if (Math.abs(xDetermineSwipePosition) >= horizontalSwipeStateDetermineThreshold) { - currentSwipeState = HomeScreenState.SwipingPages; - xDetermineSwipePosition = 0; - yDetermineSwipePosition = 0; - } else if (yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { - currentSwipeState = HomeScreenState.SwipingPagesDown; - root.swipeDownGestureBegin(); - xDetermineSwipePosition = 0; - yDetermineSwipePosition = 0; - } else if (-yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { - currentSwipeState = HomeScreenState.SwipingAppDrawerVisibility; - xDetermineSwipePosition = 0; - yDetermineSwipePosition = 0; - - // reset app drawer position to top - appDrawerFlickable.contentY = 0; - } - break; - - case HomeScreenState.SwipingPages: - xPosition += x; - break; - - case HomeScreenState.SwipingPagesDown: - yPosition = pagesYPosition; - if (y !== 0) { - root.swipeDownGestureOffset(y); - } - break; - - case HomeScreenState.SwipingAppDrawerVisibility: - yPosition = Math.max(appDrawerOpenYPosition, Math.min(pagesYPosition, yPosition + y)); - break; - } - break; - } - - case HomeScreenState.AppDrawerBeginningView: { - switch (currentSwipeState) { - case HomeScreenState.DeterminingType: - xDetermineSwipePosition += x; - yDetermineSwipePosition += y; - - // check if a swipetype can be determined and started - if (yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { - currentSwipeState = HomeScreenState.SwipingAppDrawerVisibility; - xDetermineSwipePosition = 0; - yDetermineSwipePosition = 0; - } else if (-yDetermineSwipePosition >= verticalSwipeStateDetermineThreshold) { - currentSwipeState = HomeScreenState.SwipingAppDrawerList; - yVelocityCalculator.startMeasure(appDrawerFlickable.contentY); - xDetermineSwipePosition = 0; - yDetermineSwipePosition = 0; - } - break; - case HomeScreenState.SwipingAppDrawerVisibility: - yPosition = Math.max(appDrawerOpenYPosition, Math.min(pagesYPosition, yPosition + y)); - break; - - case HomeScreenState.SwipingAppDrawerList: - // app drawer scrolling - let candidateNewPos = appDrawerFlickable.contentY - y; - appDrawerFlickable.contentY = candidateNewPos; - // update velocity - yVelocityCalculator.changePosition(appDrawerFlickable.contentY); - break; - } - break; - } - case HomeScreenState.AppDrawerView: { - break; - } - } - } - - // called after a user finishes an interaction (ex. lets go of the screen) - // called by swipe provider flickable - function updateState() { - cancelAnimations(); - - // we need to always call resetSwipeState() after each interaction. - // if we have an animation to run, we rely on the animation to call the function. - // otherwise, we do it directly here. - - switch (currentView) { - case HomeScreenState.PageView: { - - // update vertical position - switch (currentSwipeState) { - case HomeScreenState.DeterminingType: { - movingUp ? closeAppDrawer() : openAppDrawer(); - break; - } - - case HomeScreenState.SwipingPagesDown: { - root.swipeDownGestureEnd(); - root.resetSwipeState(); - break; - } - - case HomeScreenState.SwipingAppDrawerVisibility: { - movingUp ? closeAppDrawer() : openAppDrawer(); - break; - } - - case HomeScreenState.SwipingPages: { - // update pages position - let currentPageIndexPosition = xPositionFromPageIndex(currentPageIndex); - let duration = Kirigami.Units.longDuration * 2; - - if (xPosition < currentPageIndexPosition) { - if (movingRight) { - animateGoToPageIndex(currentPageIndex, duration); - } else { - animateGoToPageIndex(Math.max(0, currentPageIndex - 1), duration); - } - } else { - if (movingRight) { - animateGoToPageIndex(Math.min(pagesCount - 1, currentPageIndex + 1), duration); - } else { - animateGoToPageIndex(currentPageIndex, duration); - } - } - break; - } - - default: { - // this shouldn't occur, but keeps consistent state if it does - root.resetSwipeState(); - break; - } - } - - break; - } - case HomeScreenState.AppDrawerBeginningView: { - - switch (currentSwipeState) { - case HomeScreenState.DeterminingType: - case HomeScreenState.SwipingAppDrawerVisibility: { - movingUp ? closeAppDrawer() : openAppDrawer(); - break; - } - case HomeScreenState.SwipingAppDrawerList: { - currentView = HomeScreenState.AppDrawerView; - appDrawerFlickable.flick(0, -yVelocityCalculator.velocity); - root.resetSwipeState(); - break; - } - default: { - // this shouldn't occur, but keeps consistent state if it does - root.resetSwipeState(); - break; - } - } - - break; - } - case HomeScreenState.AppDrawerView: { - break; - } - } - } - - // measure velocity of our swipe in the app drawer, so that we can flick - property var yVelocityCalculator: MobileShell.VelocityCalculator {} - - // listen to the app drawer's flickable for if it goes to the top of the list - // we then update our view state - property var appDrawerFlickableListener: Connections { - target: appDrawerFlickable - - function onMovementEnded() { - if (root.currentView === HomeScreenState.AppDrawerView) { - if (appDrawerFlickable.contentY <= 0) { - root.currentView = HomeScreenState.AppDrawerBeginningView; - } - } - } - - function onDraggingChanged() { - if (!appDrawerFlickable.dragging) { - if (root.currentView === HomeScreenState.AppDrawerView) { - if (appDrawerFlickable.contentY <= 0) { - root.currentView = HomeScreenState.AppDrawerBeginningView; - } - } - } - } - } - - // ~~ property animators ~~ - - property var xAnim: NumberAnimation { - target: root - property: "xPosition" - easing.type: Easing.OutBack - onFinished: { - root.resetSwipeState(); - } - } - - property var openDrawerAnim: NumberAnimation { - target: root - property: "yPosition" - to: appDrawerOpenYPosition - duration: Kirigami.Units.longDuration * 2 - easing.type: Easing.OutCubic - - onFinished: { - root.currentView = HomeScreenState.AppDrawerBeginningView; - root.resetSwipeState(); - } - } - - property var closeDrawerAnim: NumberAnimation { - target: root - property: "yPosition" - to: pagesYPosition - duration: Kirigami.Units.longDuration * 2 - easing.type: Easing.OutCubic - - onFinished: { - root.currentView = HomeScreenState.PageView; - root.resetSwipeState(); - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/LauncherContainer.qml b/containments/homescreens/folio/package/contents/ui/LauncherContainer.qml deleted file mode 100644 index 619c0279..00000000 --- a/containments/homescreens/folio/package/contents/ui/LauncherContainer.qml +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.4 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.3 as Controls - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.kquickcontrolsaddons 2.0 - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager - -Item { - id: root - - readonly property int reservedSpaceForLabel: metrics.height - readonly property int cellWidth: root.width / Math.floor(root.width / ((availableCellHeight - reservedSpaceForLabel) + Kirigami.Units.smallSpacing*4)) - readonly property int cellHeight: availableCellHeight - property int availableCellHeight: Kirigami.Units.iconSizes.huge + reservedSpaceForLabel - - property ContainmentLayoutManager.AppletsLayout appletsLayout - - property alias frame: frame - property alias flow: applicationsFlow - - implicitWidth: frame.implicitWidth - implicitHeight: Math.max(Kirigami.Units.gridUnit*3, frame.implicitHeight) - - Controls.Label { - id: metrics - text: "M\nM" - visible: false - font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.9 - } - - Item { - id: spacer - width: Kirigami.Units.gridUnit * 4 - height: width - } - - Controls.Control { - id: frame - anchors.centerIn: parent - implicitWidth: contentItem.implicitWidth - implicitHeight: contentItem.implicitHeight - height: parent.height - - leftPadding: 0 - topPadding: 0 - rightPadding: 0 - bottomPadding: 0 - - // With a mousearea, it will be possible to drag with touch also on empty places - contentItem: MouseArea { - implicitWidth: applicationsFlow.implicitWidth - implicitHeight: applicationsFlow.implicitHeight - Flow { - id: applicationsFlow - - spacing: 0 - anchors.fill: parent - - move: Transition { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - properties: "x,y" - } - } - } - } - Behavior on implicitWidth { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/LauncherDragManager.qml b/containments/homescreens/folio/package/contents/ui/LauncherDragManager.qml deleted file mode 100644 index c1c24326..00000000 --- a/containments/homescreens/folio/package/contents/ui/LauncherDragManager.qml +++ /dev/null @@ -1,298 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.4 - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager -import org.kde.plasma.private.mobileshell 1.0 as MobileShell -import org.kde.private.mobile.homescreen.folio 1.0 as Folio - -Item { - id: root - - property ContainmentLayoutManager.AppletsLayout appletsLayout - property FavoriteStrip favoriteStrip - property ContainmentLayoutManager.ItemContainer currentlyDraggedDelegate - property bool active - - required property Folio.DesktopModel desktopModel - - readonly property Item spacer: Item { - width: favoriteStrip.cellWidth - height: favoriteStrip.cellHeight - } - - function startDrag(item) { - showSpacer(item, 0, 0); - } - - function dragItem(delegate, dragCenterX, dragCenterY) { - // newPosition - var newRow = 0; - - var newContainer = internal.containerForItem(delegate, dragCenterX, dragCenterY); - if (!newContainer) { - newContainer = appletsLayout; - } - - // Put it in the favorites strip - if (newContainer == favoriteStrip) { - var pos = favoriteStrip.flow.mapFromItem(delegate, 0, 0); - newRow = Math.floor((pos.x + dragCenterX) / delegate.width); - - showSpacer(delegate, dragCenterX, dragCenterY); - desktopModel.moveItem(delegate.modelData.index, newRow); - - // Put it on desktop - } else { - var pos = appletsLayout.mapFromItem(delegate, 0, 0); - - showSpacer(delegate, dragCenterX, dragCenterY); - return; - - } - } - - function dropItem(item, dragCenterX, dragCenterY) { - internal.positionItem(item, dragCenterX, dragCenterY); - } - - function showSpacer(item, dragCenterX, dragCenterY) { - var container = internal.containerForItem(item, dragCenterX, dragCenterY); - - internal.raiseContainer(container); - - appletsLayout.hidePlaceHolder(); - - if (container == appletsLayout) { - spacer.visible = false; - spacer.parent = root; - appletsLayout.releaseSpace(item); - internal.putItemInDragSpace(item); - var pos = appletsLayout.mapFromItem(item, 0, 0); - appletsLayout.showPlaceHolderAt(Qt.rect(pos.x, pos.y, item.width, item.height)); - return; - } - - var child = internal.nearestChild(item, dragCenterX, dragCenterY, container); - - if (!child) { - spacer.visible = false; - spacer.parent = container.flow - spacer.visible = true; - return; - } - - spacer.visible = false; - spacer.parent = container.flow - - var pos = container.flow.mapFromItem(item, dragCenterX, dragCenterY); - - if (pos.x < child.x + child.width / 2) { - MobileShell.ShellUtil.stackItemBefore(spacer, child); - } else { - MobileShell.ShellUtil.stackItemAfter(spacer, child); - } - - internal.putItemInDragSpace(item); - - spacer.visible = true; - } - - function showSpacerAtPos(x, y, container) { - var pos = container.flow.mapFromGlobal(x, y); - internal.raiseContainer(container); - - appletsLayout.hidePlaceHolder(); - - if (container == appletsLayout) { - spacer.visible = false; - spacer.parent = root; - appletsLayout.showPlaceHolderAt(Qt.rect(pos.x, pos.y, appletsLayout.cellWidth, appletsLayout.cellHeight)); - return; - } - - var child = internal.nearestChildFromPos(x, y, container); - - if (!child) { - spacer.visible = false; - spacer.parent = container.flow - spacer.visible = true; - return; - } - - spacer.visible = false; - spacer.parent = container.flow - - if (pos.x < child.x + child.width / 2) { - MobileShell.ShellUtil.stackItemBefore(spacer, child); - } else { - MobileShell.ShellUtil.stackItemAfter(spacer, child); - } - - spacer.visible = true; - } - - function hideSpacer () { - spacer.visible = false; - spacer.parent = root; - } - - // Those should never be accessed from outside - QtObject { - id: internal - function raiseContainer(container) { - container.z = 1; - - if (container == appletsLayout) { - favoriteStrip.z = 0; - } else if (container == favoriteStrip) { - appletsLayout.z = 0; - } else { - appletsLayout.z = 0; - favoriteStrip.z = 0; - } - } - - function containerForItem(item, dragCenterX, dragCenterY) { - if (!item.modelData) { - return appletsLayout; - } else if (favoriteStrip.contains(Qt.point(0,favoriteStrip.frame.mapFromItem(item, dragCenterX, dragCenterY).y)) - && (item.modelData.applicationLocation == Folio.DesktopModel.Favorites - || desktopModel.favoriteCount < desktopModel.maxFavoriteCount)) { - return favoriteStrip; - } else { - return appletsLayout; - } - } - - function putItemInDragSpace(item) { - var pos = root.mapFromItem(item, 0, 0); - item.parent = root; - - item.x = pos.x; - item.y = pos.y; - } - - function putInContainerLayout(item, container) { - var pos = container.flow.mapFromItem(item, 0, 0); - - if (container == appletsLayout) { - item.parent = container; - } else { - item.parent = container.flow; - } - - item.x = pos.x; - item.y = pos.y; - } - - function nearestChild(item, dragCenterX, dragCenterY, container) { - var distance = Number.POSITIVE_INFINITY; - var child; - var pos = container.flow.mapFromItem(item, dragCenterX, dragCenterY); - - // Search Right - for (var i = 0; i < item.width * 2; i += item.width/2) { - var candidate = container.flow.childAt( - Math.min(container.flow.width, Math.max(0, pos.x + i)), - Math.min(container.flow.height-1, Math.max(0, pos.y))); - - if (candidate && i < distance) { - child = candidate; - break; - } - } - - // Search Left - for (var i = 0; i < item.width * 2; i += item.width/2) { - var candidate = container.flow.childAt(Math.min(container.flow.width, Math.max(0, pos.x - i)), Math.min(container.flow.height-1, Math.max(0, pos.y))); - - if (candidate && i < distance) { - child = candidate; - break; - } - } - - if (!child) { - if (item.y < container.flow.height/2) { - child = container.flow.children[0]; - } else { - child = container.flow.children[container.flow.children.length - 1]; - } - } - - return child; - } - - - function nearestChildFromPos(x, y, container) { - var distance = Number.POSITIVE_INFINITY; - var child; - var pos = container.flow.mapFromGlobal(x, y); - - // Search Right - for (var i = 0; i < appletsLayout.cellWidth * 2; i += appletsLayout.cellWidth/2) { - var candidate = container.flow.childAt( - Math.min(container.flow.width, Math.max(0, pos.x + i)), - Math.min(container.flow.height-1, Math.max(0, pos.y))); - - if (candidate && i < distance) { - child = candidate; - break; - } - } - - // Search Left - for (var i = 0; i < appletsLayout.cellWidth * 2; i += appletsLayout.cellWidth/2) { - var candidate = container.flow.childAt(Math.min(container.flow.width, Math.max(0, pos.x - i)), Math.min(container.flow.height-1, Math.max(0, pos.y))); - - if (candidate && i < distance) { - child = candidate; - break; - } - } - - return child; - } - - - function positionItem(item, dragCenterX, dragCenterY) { - hideSpacer(); - var container = containerForItem(item, dragCenterX, dragCenterY); - - raiseContainer(container); - - if (container == appletsLayout) { - if (item.modelData) { - desktopModel.setLocation(item.modelData.index, Folio.DesktopModel.Desktop); - } - var pos = appletsLayout.mapFromItem(item, 0, 0); - item.parent = appletsLayout; - item.x = pos.x; - item.y = pos.y; - appletsLayout.hidePlaceHolder(); - appletsLayout.positionItem(item); - - return; - } else if (container == favoriteStrip) { - desktopModel.setLocation(item.modelData.index, Folio.DesktopModel.Favorites); - } else { - desktopModel.setLocation(item.modelData.index, Folio.DesktopModel.None); - } - - var child = nearestChild(item, dragCenterX, dragCenterY, container); - - putInContainerLayout(item, container); - MobileShell.ShellUtil.stackItemBefore(item, spacer); - spacer.visible = false; - spacer.parent = root; - } - } -} - - diff --git a/containments/homescreens/folio/package/contents/ui/LauncherRepeater.qml b/containments/homescreens/folio/package/contents/ui/LauncherRepeater.qml deleted file mode 100644 index 049f85de..00000000 --- a/containments/homescreens/folio/package/contents/ui/LauncherRepeater.qml +++ /dev/null @@ -1,128 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.14 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.3 as Controls -import Qt5Compat.GraphicalEffects - -import org.kde.plasma.plasmoid 2.0 -import org.kde.kquickcontrolsaddons 2.0 - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager -import org.kde.plasma.private.mobileshell 1.0 as MobileShell -import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState -import org.kde.private.mobile.homescreen.folio 1.0 as Folio -import org.kde.kirigami 2.14 as Kirigami - -Repeater { - id: launcherRepeater - - required property var homeScreenState - required property Folio.DesktopModel desktopModel - - property ContainmentLayoutManager.AppletsLayout appletsLayout - property FavoriteStrip favoriteStrip - property int cellWidth - property int cellHeight - - signal scrollLeftRequested - signal scrollRightRequested - signal stopScrollRequested - - delegate: HomeDelegate { - id: delegate - desktopModel: launcherRepeater.desktopModel - homeScreenState: launcherRepeater.homeScreenState - - width: launcherRepeater.cellWidth - height: Math.min(parent.height, launcherRepeater.cellHeight) - appletsLayout: launcherRepeater.appletsLayout - - //just the normal inline binding in height: fails as it gets broken, make it explicit - Binding { - target: delegate - property: "height" - value: Math.min(delegate.parent.height, launcherRepeater.cellHeight) - } - parent: parentFromLocation - reservedSpaceForLabel: metrics.height - property Item parentFromLocation: { - switch (model.applicationLocation) { - case Folio.DesktopModel.Favorites: - return favoriteStrip.flow; - case Folio.DesktopModel.Desktop: - default: - return appletsLayout; - } - } - Component.onCompleted: { - if (model.applicationLocation === Folio.DesktopModel.Desktop) { - appletsLayout.restoreItem(delegate); - } - } - - onUserDrag: { - dragCenterX = dragCenter.x; - dragCenterY = dragCenter.y; - launcherDragManager.dragItem(delegate, dragCenter.x, dragCenter.y); - - delegate.width = appletsLayout.cellWidth; - delegate.height = appletsLayout.cellHeight; - - var pos = plasmoid.fullRepresentationItem.mapFromItem(delegate, dragCenter.x, dragCenter.y); - - //SCROLL LEFT - if (pos.x < Kirigami.Units.gridUnit) { - launcherRepeater.scrollLeftRequested(); - //SCROLL RIGHT - } else if (pos.x > homeScreenState.pageWidth - Kirigami.Units.gridUnit) { - launcherRepeater.scrollRightRequested(); - //DON't SCROLL - } else { - launcherRepeater.stopScrollRequested(); - } - } - - onDragActiveChanged: { - launcherDragManager.active = dragActive - if (dragActive) { - // Must be 0, 0 as at this point dragCenterX and dragCenterY are on the drag before" - launcherDragManager.startDrag(delegate); - launcherDragManager.currentlyDraggedDelegate = delegate; - } else { - launcherDragManager.dropItem(delegate, dragCenterX, dragCenterY); - plasmoid.editMode = false; - editMode = false; - launcherRepeater.stopScrollRequested(); - launcherDragManager.currentlyDraggedDelegate = null; - forceActiveFocus(); - } - } - - onLaunch: (x, y, icon, title) => { - if (icon !== "") { - MobileShellState.ShellDBusClient.openAppLaunchAnimation( - icon, - title, - delegate.iconItem.Kirigami.ScenePosition.x + delegate.iconItem.width/2, - delegate.iconItem.Kirigami.ScenePosition.y + delegate.iconItem.height/2, - Math.min(delegate.iconItem.width, delegate.iconItem.height)); - } - } - onParentFromLocationChanged: { - if (!launcherDragManager.active && parent != parentFromLocation) { - parent = parentFromLocation; - if (model.applicationLocation === Folio.DesktopModel.Favorites) { - MobileShell.ShellUtil.stackItemBefore(delegate, parentFromLocation.children[index]); - } else if (model.applicationLocation === Folio.DesktopModel.None) { - MobileShell.ShellUtil.stackItemBefore(delegate, parentFromLocation.children[Math.max(0, index - desktopModel.favoriteCount)]); - } - } - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/MobileAppletContainer.qml b/containments/homescreens/folio/package/contents/ui/MobileAppletContainer.qml deleted file mode 100644 index fa4621ab..00000000 --- a/containments/homescreens/folio/package/contents/ui/MobileAppletContainer.qml +++ /dev/null @@ -1,126 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.1 -import Qt5Compat.GraphicalEffects - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.draganddrop 2.0 as DragDrop - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager - -import "private" as Private - -ContainmentLayoutManager.BasicAppletContainer { - id: appletContainer - - property var homeScreenState - property real screenWidth: homeScreenState.availableScreenWidth - property real screenHeight: homeScreenState.availableScreenHeight - - // HACK: for some reason configOverlayComponent access the appletContainer id - property var container: appletContainer - - configOverlayComponent: Private.ConfigOverlay { - onRequestEditModeClose: container.editMode = false; - onRequestRemoveTrigger: container.applet.action("remove").trigger(); - } - - property LauncherDragManager launcherDragManager - - onEditModeChanged: { - launcherDragManager.active = dragActive || editMode; - } - - property real dragCenterX - property real dragCenterY - - editModeCondition: ContainmentLayoutManager.ItemContainer.AfterPressAndHold - - onDragActiveChanged: { - launcherDragManager.active = dragActive || editMode; - if (dragActive) { - // Must be 0, 0 as at this point dragCenterX and dragCenterY are on the drag before" - launcherDragManager.startDrag(appletContainer); - launcherDragManager.currentlyDraggedDelegate = appletContainer; - // Reparenting removed focus - appletContainer.forceActiveFocus(); - } else { - launcherDragManager.dropItem(appletContainer, dragCenterX, dragCenterY); - plasmoid.editMode = false; - launcherRepeater.stopScrollRequested(); - launcherDragManager.currentlyDraggedDelegate = null; - forceActiveFocus(); - } - } - onUserDrag: { - dragCenterX = dragCenter.x; - dragCenterY = dragCenter.y; - launcherDragManager.dragItem(appletContainer, dragCenter.x, dragCenter.y); - - var pos = plasmoid.fullRepresentationItem.mapFromItem(appletContainer, dragCenter.x, dragCenter.y); - - //SCROLL LEFT - if (pos.x < Kirigami.Units.gridUnit) { - launcherRepeater.scrollLeftRequested(); - //SCROLL RIGHT - } else if (pos.x > screenWidth - Kirigami.Units.gridUnit) { - launcherRepeater.scrollRightRequested(); - //DON't SCROLL - } else { - launcherRepeater.stopScrollRequested(); - } - - appletContainer.x = Math.max(0, Math.min(screenWidth - appletContainer.width, appletContainer.x)); - } - onWidthChanged: { - if (appletContainer.x + appletContainer.width > screenWidth * Math.max(1, Math.ceil(appletContainer.x / screenWidth))) { - appletsLayout.releaseSpace(appletContainer); - appletContainer.width = (screenWidth * Math.max(1, Math.ceil(appletContainer.x / screenWidth)) - appletContainer.x); - appletsLayout.positionItem(appletContainer); - } - } - - Connections { - target: appletsLayout - function onAppletsLayoutInteracted() { - appletContainer.editMode = false; - } - } - Connections { - target: dropArea - function onWidthChanged () { - let spaceReleased = false; - if (appletContainer.width > screenWidth || appletContainer.height > screenHeight) { - appletsLayout.releaseSpace(appletContainer); - appletContainer.width = Math.min(appletContainer.width, screenWidth); - appletContainer.height = Math.min(appletContainer.height, screenHeight); - spaceReleased = true; - } - if (Math.floor((appletContainer.x) / screenWidth) < Math.floor((appletContainer.x + appletContainer.width/2) / screenWidth)) { - appletsLayout.releaseSpace(appletContainer); - appletContainer.x = Math.floor((appletContainer.x + appletContainer.width) / screenWidth) * screenWidth; - appletsLayout.positionItem(appletContainer); - spaceReleased = false; - - } else if (Math.floor((appletContainer.x + appletContainer.width/2) / screenWidth) < Math.floor((appletContainer.x + appletContainer.width) / screenWidth)) { - appletsLayout.releaseSpace(appletContainer); - appletContainer.x = Math.ceil(appletContainer.x / screenWidth) * screenWidth - screenWidth; - appletsLayout.positionItem(appletContainer); - spaceReleased = false; - } - if (spaceReleased) { - appletsLayout.positionItem(appletContainer); - } - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/PlaceholderDelegate.qml b/containments/homescreens/folio/package/contents/ui/PlaceholderDelegate.qml new file mode 100644 index 00000000..1a233b46 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/PlaceholderDelegate.qml @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts + +import org.kde.private.mobile.homescreen.folio 1.0 as Folio +import org.kde.kirigami 2.10 as Kirigami + +import "./delegate" + +Item { + id: root + width: Folio.HomeScreenState.pageCellWidth + height: Folio.HomeScreenState.pageCellHeight + + // we need to simulate the position of the icon if it is placed at this spot + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // icon position placement + Rectangle { + id: loader + color: Qt.rgba(255, 255, 255, 0.3) + radius: Kirigami.Units.largeSpacing + + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + Layout.minimumWidth: Folio.FolioSettings.delegateIconSize + Layout.minimumHeight: Folio.FolioSettings.delegateIconSize + Layout.preferredHeight: Layout.minimumHeight + layer.enabled: true + layer.effect: DelegateShadow {} + } + + // simulate a delegate's label for positioning purposes + DelegateLabel { + id: label + opacity: 0 + Layout.fillWidth: true + Layout.preferredHeight: Folio.HomeScreenState.pageDelegateLabelHeight + Layout.topMargin: Folio.HomeScreenState.pageDelegateLabelSpacing + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/appdrawer/AbstractAppDrawer.qml b/containments/homescreens/folio/package/contents/ui/appdrawer/AbstractAppDrawer.qml deleted file mode 100644 index b96b1e32..00000000 --- a/containments/homescreens/folio/package/contents/ui/appdrawer/AbstractAppDrawer.qml +++ /dev/null @@ -1,175 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.15 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.15 as Controls - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PC3 -import org.kde.kirigami 2.10 as Kirigami - -import org.kde.plasma.private.mobileshell 1.0 as MobileShell - -import "../private" -import "../" - -Item { - id: root - required property var homeScreenState - - property real leftPadding: 0 - property real topPadding: 0 - property real bottomPadding: 100 - property real rightPadding: 0 - - property alias flickable: flickableBody.contentItem - - property Flickable contentItem - property real contentWidth: holdingColumn.width - - required property int headerHeight - required property var headerItem - - signal launched - signal dragStarted - - readonly property int reservedSpaceForLabel: metrics.height - property int availableCellHeight: Kirigami.Units.iconSizes.huge + reservedSpaceForLabel - - readonly property real openFactor: factorNormalize(view.contentY / (Kirigami.Units.gridUnit * 10)) - - // height from top of screen that the drawer starts - readonly property real drawerTopMargin: height - topPadding - bottomPadding - closedPositionOffset - readonly property real closedPositionOffset: homeScreenState.appDrawerBottomOffset - -//BEGIN functions - - function factorNormalize(num) { - return Math.min(1, Math.max(0, num)); - } - -//END functions - - Drag.dragType: Drag.Automatic - - PC3.Label { - id: metrics - text: "M\nM" - visible: false - font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 - font.weight: Font.Bold - } - - // bottom divider - GradientBar { - opacity: (homeScreenState.currentView !== HomeScreenState.PageView || homeScreenState.currentSwipeState === HomeScreenState.SwipingAppDrawerVisibility) ? 0.6 : 0 - visible: root.bottomPadding > 0 - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.bottomMargin: root.bottomPadding - height - } - - // physical position of drawer is handled through this flickable - Flickable { - id: view - anchors.fill: parent - - // scroll events are handled by our flick container, we are only using this for positioning - interactive: false - contentY: Math.max(0, Math.min(root.drawerTopMargin, root.drawerTopMargin - homeScreenState.yPosition)) - - contentHeight: column.implicitHeight - contentWidth: -1 - boundsBehavior: Flickable.StopAtBounds - - ColumnLayout { - id: column - width: view.width - spacing: 0 - - // margin of the drawer from the top - Rectangle { - id: topMargin - color: "transparent" - Layout.fillWidth: true - Layout.preferredHeight: root.drawerTopMargin - - OpenDrawerButton { - id: openDrawerButton - anchors { - leftMargin: root.leftPadding - left: parent.left - rightMargin: root.rightPadding - right: parent.right - bottom: parent.bottom - } - factor: root.openFactor - flickable: view - onOpenRequested: { - contentItem.positionViewAtBeginning(); - homeScreenState.openAppDrawer(); - } - onCloseRequested: homeScreenState.closeAppDrawer(); - } - } - - // actual drawer - MobileShell.BaseItem { - visible: root.openFactor > 0 // prevent handlers from picking up events - - Layout.fillWidth: true - Layout.preferredHeight: root.height - leftPadding: root.leftPadding; topPadding: root.topPadding - rightPadding: root.rightPadding; bottomPadding: root.bottomPadding - - // drawer background - background: Rectangle { - id: scrim - color: "black" - opacity: 0.6 * root.openFactor - - // remove radius - radius: view.contentY > (topMargin.height - Kirigami.Units.gridUnit) ? 0 : Kirigami.Units.gridUnit - Behavior on radius { - NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad } - } - } - - opacity: root.openFactor - - contentItem: ColumnLayout { - id: holdingColumn - width: view.width - spacing: 0 - - // drawer header - MobileShell.BaseItem { - id: flickableHeader - Layout.preferredHeight: root.headerHeight - Layout.fillWidth: true - leftPadding: 0; rightPadding: 0; topPadding: 0; bottomPadding: 0 - - contentItem: root.headerItem - } - - // drawer body - MobileShell.BaseItem { - id: flickableBody - Layout.fillHeight: true - Layout.fillWidth: true - leftPadding: 0; rightPadding: 0; topPadding: 0; bottomPadding: 0 - - contentItem: root.contentItem - } - } - } - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/appdrawer/AppDrawerHeader.qml b/containments/homescreens/folio/package/contents/ui/appdrawer/AppDrawerHeader.qml deleted file mode 100644 index e14ec30d..00000000 --- a/containments/homescreens/folio/package/contents/ui/appdrawer/AppDrawerHeader.qml +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Layouts 1.1 - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.components 3.0 as PlasmaComponents - -Item { - id: root - - signal switchToListRequested() - signal switchToGridRequested() - - Kirigami.Theme.colorSet: Kirigami.Theme.Complementary - Kirigami.Theme.inherit: false - - // HACK: Here only to steal inputs the would normally be delivered to home - MouseArea { - anchors.fill: parent - } - - RowLayout { - anchors.topMargin: Kirigami.Units.smallSpacing - anchors.leftMargin: Kirigami.Units.gridUnit - anchors.rightMargin: Kirigami.Units.gridUnit - anchors.fill: parent - spacing: Kirigami.Units.smallSpacing - - Kirigami.Heading { - color: "white" - level: 1 - text: i18n("Applications") - font.weight: Font.Medium - } - Item { Layout.fillWidth: true } - PlasmaComponents.ToolButton { - icon.name: "view-list-symbolic" - implicitWidth: Math.round(Kirigami.Units.gridUnit * 2.1) - implicitHeight: Math.round(Kirigami.Units.gridUnit * 2.1) - onClicked: root.switchToListRequested() - } - PlasmaComponents.ToolButton { - icon.name: "view-grid-symbolic" - implicitWidth: Math.round(Kirigami.Units.gridUnit * 2.1) - implicitHeight: Math.round(Kirigami.Units.gridUnit * 2.1) - onClicked: root.switchToGridRequested() - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/appdrawer/AppDrawerLoader.qml b/containments/homescreens/folio/package/contents/ui/appdrawer/AppDrawerLoader.qml deleted file mode 100644 index 9c35933b..00000000 --- a/containments/homescreens/folio/package/contents/ui/appdrawer/AppDrawerLoader.qml +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.1 - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PlasmaComponents - -Loader { - id: root - - required property var homeScreenState - - property real topPadding: 0 - property real bottomPadding: 0 - property real leftPadding: 0 - property real rightPadding: 0 - - property string appDrawerType: "gridview" // gridview/listview - - readonly property real headerHeight: Math.round(Kirigami.Units.gridUnit * 3) - - sourceComponent: appDrawerType === "gridview" ? gridViewDrawer : listViewDrawer - - Component { - id: headerComponent - - AppDrawerHeader { - onSwitchToListRequested: { - if (root.appDrawerType !== "listview") { - root.appDrawerType = "listview"; - } - } - - onSwitchToGridRequested: { - if (root.appDrawerType !== "gridview") { - root.appDrawerType = "gridview"; - } - } - } - } - - Component { - id: listViewDrawer - ListViewAppDrawer { - anchors.fill: parent - topPadding: root.topPadding - bottomPadding: root.bottomPadding - leftPadding: root.leftPadding - rightPadding: root.rightPadding - - homeScreenState: root.homeScreenState - headerItem: Loader { sourceComponent: headerComponent } - headerHeight: root.headerHeight - } - } - - Component { - id: gridViewDrawer - GridViewAppDrawer { - anchors.fill: parent - topPadding: root.topPadding - bottomPadding: root.bottomPadding - leftPadding: root.leftPadding - rightPadding: root.rightPadding - - homeScreenState: root.homeScreenState - headerItem: Loader { sourceComponent: headerComponent } - headerHeight: root.headerHeight - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/appdrawer/DrawerGridDelegate.qml b/containments/homescreens/folio/package/contents/ui/appdrawer/DrawerGridDelegate.qml deleted file mode 100644 index 1c2ed76e..00000000 --- a/containments/homescreens/folio/package/contents/ui/appdrawer/DrawerGridDelegate.qml +++ /dev/null @@ -1,166 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.15 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.3 as Controls -import Qt5Compat.GraphicalEffects - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.core as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.kquickcontrolsaddons 2.0 - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager -import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings - -MouseArea { - id: delegate - width: GridView.view.cellWidth - height: GridView.view.cellHeight - - property int reservedSpaceForLabel - property alias iconItem: icon - - readonly property real margins: Math.floor(width * 0.2) - - signal launch(int x, int y, var source, string title, string storageId) - signal dragStarted(string imageSource, int x, int y, string mimeData) - - function launchApp() { - // launch app - if (model.applicationRunning) { - delegate.launch(0, 0, "", model.applicationName, model.applicationStorageId); - } else { - delegate.launch(delegate.x + (Kirigami.Units.smallSpacing * 2), delegate.y + (Kirigami.Units.smallSpacing * 2), icon.source, model.applicationName, model.applicationStorageId); - } - } - - onPressAndHold: { - delegate.grabToImage(function(result) { - delegate.Drag.imageSource = result.url - dragStarted(result.url, width/2, height/2, model.applicationStorageId) - }) - } - - // grow/shrink animation - property real zoomScale: 1 - transform: Scale { - origin.x: delegate.width / 2; - origin.y: delegate.height / 2; - xScale: delegate.zoomScale - yScale: delegate.zoomScale - } - - property bool launchAppRequested: false - - NumberAnimation on zoomScale { - id: shrinkAnim - running: false - duration: ShellSettings.Settings.animationsEnabled ? 80 : 1 - to: ShellSettings.Settings.animationsEnabled ? 0.8 : 1 - onFinished: { - if (!delegate.pressed) { - growAnim.restart(); - } - } - } - - NumberAnimation on zoomScale { - id: growAnim - running: false - duration: ShellSettings.Settings.animationsEnabled ? 80 : 1 - to: 1 - onFinished: { - if (delegate.launchAppRequested) { - delegate.launchApp(); - delegate.launchAppRequested = false; - } - } - } - - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onPressedChanged: { - if (pressed) { - growAnim.stop(); - shrinkAnim.restart(); - } else if (!pressed && !shrinkAnim.running) { - growAnim.restart(); - } - } - // launch app handled by press animation - onClicked: launchAppRequested = true; - - //preventStealing: true - ColumnLayout { - anchors { - fill: parent - leftMargin: margins - topMargin: margins - rightMargin: margins - bottomMargin: margins - } - spacing: 0 - - Kirigami.Icon { - id: icon - - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.fillWidth: true - Layout.minimumHeight: Math.floor(parent.height - delegate.reservedSpaceForLabel) - Layout.preferredHeight: Layout.minimumHeight - - source: model.applicationIcon - - Rectangle { - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - } - visible: model.applicationRunning - radius: width - width: Kirigami.Units.smallSpacing - height: width - color: theme.highlightColor - } - - // darken effect when hovered/pressed - layer { - enabled: delegate.pressed || delegate.containsMouse - effect: ColorOverlay { - color: Qt.rgba(0, 0, 0, 0.3) - } - } - } - - PlasmaComponents.Label { - id: label - visible: text.length > 0 - - Layout.fillWidth: true - Layout.preferredHeight: delegate.reservedSpaceForLabel - Layout.topMargin: Kirigami.Units.smallSpacing - Layout.leftMargin: -parent.anchors.leftMargin + Kirigami.Units.smallSpacing - Layout.rightMargin: -parent.anchors.rightMargin + Kirigami.Units.smallSpacing - - wrapMode: Text.WordWrap - maximumLineCount: 2 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignTop - elide: Text.ElideRight - - text: model.applicationName - - font.pointSize: theme.defaultFont.pointSize * 0.8 - font.weight: Font.Bold - color: "white" - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/appdrawer/DrawerListDelegate.qml b/containments/homescreens/folio/package/contents/ui/appdrawer/DrawerListDelegate.qml deleted file mode 100644 index 882b5773..00000000 --- a/containments/homescreens/folio/package/contents/ui/appdrawer/DrawerListDelegate.qml +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.4 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.3 as Controls -import Qt5Compat.GraphicalEffects - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.core as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.kquickcontrolsaddons 2.0 - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager - -MouseArea { - id: delegate - property int reservedSpaceForLabel - property alias iconItem: icon - - signal launch(int x, int y, var source, string title, string storageId) - signal dragStarted(string imageSource, int x, int y, string mimeData) - - onPressAndHold: { - delegate.grabToImage(function(result) { - delegate.Drag.imageSource = result.url - dragStarted(result.url, width/2, height/2, model.applicationStorageId) - }) - } - - onClicked: { - // launch app - if (model.applicationRunning) { - delegate.launch(0, 0, "", model.applicationName, model.applicationStorageId); - } else { - delegate.launch(delegate.x + (Kirigami.Units.smallSpacing * 2), delegate.y + (Kirigami.Units.smallSpacing * 2), icon.source, model.applicationName, model.applicationStorageId); - } - } - hoverEnabled: true - - Rectangle { - anchors.fill: parent - color: delegate.pressed ? Qt.rgba(255, 255, 255, 0.2) : (delegate.containsMouse ? Qt.rgba(255, 255, 255, 0.05) : "transparent") - Behavior on color { - ColorAnimation { duration: Kirigami.Units.shortDuration } - } - } - - RowLayout { - anchors { - fill: parent - leftMargin: Kirigami.Units.smallSpacing * 2 - topMargin: Kirigami.Units.smallSpacing - rightMargin: Kirigami.Units.smallSpacing * 2 - bottomMargin: Kirigami.Units.smallSpacing - } - spacing: 0 - - Kirigami.Icon { - id: icon - - Layout.alignment: Qt.AlignLeft - Layout.minimumWidth: Layout.minimumHeight - Layout.preferredWidth: Layout.minimumHeight - Layout.minimumHeight: parent.height - Layout.preferredHeight: Layout.minimumHeight - - source: model.applicationIcon - - Rectangle { - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - } - visible: model.applicationRunning - radius: width - width: Kirigami.Units.smallSpacing - height: width - color: theme.highlightColor - } - } - - PlasmaComponents.Label { - id: label - visible: text.length > 0 - - Layout.fillWidth: true - wrapMode: Text.WordWrap - Layout.leftMargin: Kirigami.Units.smallSpacing * 2 - Layout.rightMargin: Kirigami.Units.gridUnit - maximumLineCount: 1 - elide: Text.ElideRight - - text: model.applicationName - - font.pointSize: Math.round(theme.defaultFont.pointSize * 1.1) - color: "white" - } - } -} - - diff --git a/containments/homescreens/folio/package/contents/ui/appdrawer/GridViewAppDrawer.qml b/containments/homescreens/folio/package/contents/ui/appdrawer/GridViewAppDrawer.qml deleted file mode 100644 index e3973ebc..00000000 --- a/containments/homescreens/folio/package/contents/ui/appdrawer/GridViewAppDrawer.qml +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.15 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.15 as Controls - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PC3 -import org.kde.kirigami 2.10 as Kirigami - -import org.kde.plasma.private.mobileshell 1.0 as MobileShell -import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState -import org.kde.private.mobile.homescreen.folio 1.0 as Folio - -import "../private" - -AbstractAppDrawer { - id: root - - contentItem: MobileShell.GridView { - id: gridView - clip: true - - /* - * HACK: When the number of apps is less than the one that would fit in the first shown part of the drawer, make - * this flickable interactive, in order to steal inputs that would normally be delivered to home. - */ - interactive: contentHeight <= height ? true : root.homeScreenState.appDrawerInteractive - - readonly property real effectiveContentWidth: root.contentWidth - 2 * horizontalMargin - readonly property real horizontalMargin: root.width * 0.1 / 2 - leftMargin: horizontalMargin - rightMargin: horizontalMargin - - cellWidth: effectiveContentWidth / Math.min(Math.floor(effectiveContentWidth / (Kirigami.Units.iconSizes.huge + Kirigami.Units.gridUnit * 2)), 8) - cellHeight: cellWidth + root.reservedSpaceForLabel - - readonly property int columns: Math.floor(effectiveContentWidth / cellWidth) - readonly property int rows: Math.ceil(gridView.count / columns) - - cacheBuffer: Math.max(0, rows * cellHeight) - - model: Folio.ApplicationListModel - - delegate: DrawerGridDelegate { - id: delegate - - width: gridView.cellWidth - height: gridView.cellHeight - reservedSpaceForLabel: root.reservedSpaceForLabel - - onDragStarted: (imageSource, x, y, mimeData) => { - root.Drag.imageSource = imageSource; - root.Drag.hotSpot.x = x; - root.Drag.hotSpot.y = y; - root.Drag.mimeData = { "text/x-plasma-phone-homescreen-launcher": mimeData }; - - root.homeScreenState.closeAppDrawer() - - root.dragStarted() - root.Drag.active = true; - } - onLaunch: (x, y, icon, title, storageId) => { - if (icon !== "") { - MobileShellState.ShellDBusClient.openAppLaunchAnimation( - icon, - title, - delegate.iconItem.Kirigami.ScenePosition.x + delegate.iconItem.width/2, - delegate.iconItem.Kirigami.ScenePosition.y + delegate.iconItem.height/2, - Math.min(delegate.iconItem.width, delegate.iconItem.height)); - } - - Folio.ApplicationListModel.setMinimizedDelegate(index, delegate); - MobileShell.AppLaunch.launchOrActivateApp(storageId); - root.launched(); - } - } - - PC3.ScrollBar.vertical: PC3.ScrollBar { - id: scrollBar - interactive: true - enabled: true - Behavior on opacity { - OpacityAnimator { - duration: Kirigami.Units.longDuration * 2 - easing.type: Easing.InOutQuad - } - } - implicitWidth: Kirigami.Units.smallSpacing - contentItem: Rectangle { - radius: width/2 - color: Qt.rgba(1, 1, 1, 0.3) - } - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/appdrawer/ListViewAppDrawer.qml b/containments/homescreens/folio/package/contents/ui/appdrawer/ListViewAppDrawer.qml deleted file mode 100644 index 5c5db8ad..00000000 --- a/containments/homescreens/folio/package/contents/ui/appdrawer/ListViewAppDrawer.qml +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.15 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.15 as Controls - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PC3 -import org.kde.kirigami 2.10 as Kirigami - -import org.kde.plasma.private.mobileshell 1.0 as MobileShell -import org.kde.private.mobile.homescreen.folio 1.0 as Folio - -import "../private" - -AbstractAppDrawer { - id: root - - contentItem: MobileShell.ListView { - id: listView - clip: true - reuseItems: true - cacheBuffer: model.count * delegateHeight // delegate height - - interactive: root.homeScreenState.appDrawerInteractive - - property int delegateHeight: Kirigami.Units.gridUnit * 3 - - model: Folio.ApplicationListModel - - delegate: DrawerListDelegate { - id: delegate - - width: listView.width - height: listView.delegateHeight - reservedSpaceForLabel: root.reservedSpaceForLabel - - onDragStarted: (imageSource, x, y, mimeData) => { - root.Drag.imageSource = imageSource; - root.Drag.hotSpot.x = x; - root.Drag.hotSpot.y = y; - root.Drag.mimeData = { "text/x-plasma-phone-homescreen-launcher": mimeData }; - - root.homeScreenState.closeAppDrawer() - - root.dragStarted() - root.Drag.active = true; - } - onLaunch: (x, y, icon, title, storageId) => { - if (icon !== "") { - MobileShellState.ShellDBusClient.openAppLaunchAnimation( - icon, - title, - delegate.iconItem.Kirigami.ScenePosition.x + delegate.iconItem.width/2, - delegate.iconItem.Kirigami.ScenePosition.y + delegate.iconItem.height/2, - Math.min(delegate.iconItem.width, delegate.iconItem.height)); - } - - Folio.ApplicationListModel.setMinimizedDelegate(index, delegate); - MobileShell.AppLaunch.launchOrActivateApp(storageId); - root.launched(); - } - } - - PC3.ScrollBar.vertical: PC3.ScrollBar { - id: scrollBar - interactive: true - enabled: true - Behavior on opacity { - OpacityAnimator { - duration: Kirigami.Units.longDuration * 2 - easing.type: Easing.InOutQuad - } - } - implicitWidth: Kirigami.Units.smallSpacing - contentItem: Rectangle { - radius: width/2 - color: Qt.rgba(1, 1, 1, 0.3) - } - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml b/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml new file mode 100644 index 00000000..6863139c --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as Controls +import Qt5Compat.GraphicalEffects +import QtQuick.Effects + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.kquickcontrolsaddons 2.0 + +import org.kde.private.mobile.homescreen.folio 1.0 as Folio +import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +Folio.DelegateTouchArea { + id: delegate + + property string name + property bool shadow: false + + property alias contentItem: visualItem.contentItem + property alias delegateItem: delegateWrapper + property alias labelOpacity: label.opacity + + signal afterClickAnimation() + + // grow/shrink animation + property real zoomScale: 1 + property bool clickRequested: false + + NumberAnimation on zoomScale { + id: shrinkAnim + running: false + duration: ShellSettings.Settings.animationsEnabled ? 80 : 1 + to: ShellSettings.Settings.animationsEnabled ? 0.8 : 1 + onFinished: { + if (!delegate.pressed) { + growAnim.restart(); + } + } + } + + NumberAnimation on zoomScale { + id: growAnim + running: false + duration: ShellSettings.Settings.animationsEnabled ? 80 : 1 + to: 1 + onFinished: { + if (delegate.clickRequested) { + delegate.afterClickAnimation(); + delegate.clickRequested = false; + } + } + } + + cursorShape: Qt.PointingHandCursor + onPressedChanged: (pressed) => { + if (pressed) { + growAnim.stop(); + shrinkAnim.restart(); + } else if (!pressed && !shrinkAnim.running) { + growAnim.restart(); + } + } + // trigger handled by press animation + onClicked: clickRequested = true; + + layer.enabled: delegate.shadow + layer.effect: DelegateShadow {} + + Item { + id: delegateWrapper + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // transform is not on delegateWrapper because when it's zoomed in, it apparently + // affects the delegate's x and y position, which messes up the starting drag and drop + // position (for mapFromItem in HomeScreen.qml) + transform: Scale { + origin.x: delegate.width / 2; + origin.y: delegate.height / 2; + xScale: delegate.zoomScale + yScale: delegate.zoomScale + } + + MobileShell.BaseItem { + id: visualItem + + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + Layout.minimumWidth: Folio.FolioSettings.delegateIconSize + Layout.minimumHeight: Folio.FolioSettings.delegateIconSize + Layout.preferredHeight: Layout.minimumHeight + + // darken effect when hovered + // TODO: removed for now, since hovered property seems to overlap with the touch pressed event + // layer { + // enabled: delegate.hovered + // effect: ColorOverlay { + // color: Qt.rgba(0, 0, 0, 0.3) + // } + // } + } + + DelegateLabel { + id: label + opacity: text.length > 0 + + Layout.fillWidth: true + Layout.preferredHeight: Folio.HomeScreenState.pageDelegateLabelHeight + Layout.topMargin: Folio.HomeScreenState.pageDelegateLabelSpacing + Layout.leftMargin: -parent.anchors.leftMargin + Kirigami.Units.smallSpacing + Layout.rightMargin: -parent.anchors.rightMargin + Kirigami.Units.smallSpacing + + text: delegate.name + color: "white" + } + } + } +} + + diff --git a/containments/homescreens/folio/package/contents/ui/delegate/AppDelegate.qml b/containments/homescreens/folio/package/contents/ui/delegate/AppDelegate.qml new file mode 100644 index 00000000..7495b2da --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/AppDelegate.qml @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import QtQuick.Effects + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.private.mobile.homescreen.folio 1.0 as Folio +import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState +import org.kde.plasma.private.mobileshell 1.0 as MobileShell + +AbstractDelegate { + id: root + shadow: true + name: application.name + + property Folio.FolioApplication application + + property alias iconItem: icon + + property bool turnToFolder: false + property bool turnToFolderAnimEnabled: false + + function launchApp() { + if (application.icon !== "") { + MobileShellState.ShellDBusClient.openAppLaunchAnimation( + application.icon, + application.name, + root.iconItem.Kirigami.ScenePosition.x + root.iconItem.width/2, + root.iconItem.Kirigami.ScenePosition.y + root.iconItem.height/2, + Math.min(root.iconItem.width, root.iconItem.height)); + } + + application.setMinimizedDelegate(root); + MobileShell.AppLaunch.launchOrActivateApp(application.storageId); + } + + onAfterClickAnimation: { + launchApp(); + } + + contentItem: Item { + height: Folio.FolioSettings.delegateIconSize + width: Folio.FolioSettings.delegateIconSize + + // background for folder creation animation + Rectangle { + id: rect + radius: Kirigami.Units.largeSpacing + color: Qt.rgba(255, 255, 255, 0.3) + anchors.fill: parent + + opacity: root.turnToFolder ? 1 : 0 + property real scaleAmount: root.turnToFolder ? 1.2 : 1.0 + + Behavior on scaleAmount { + enabled: root.turnToFolderAnimEnabled + NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } + } + Behavior on opacity { + enabled: root.turnToFolderAnimEnabled + NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } + } + + transform: Scale { + origin.x: rect.width / 2 + origin.y: rect.height / 2 + xScale: rect.scaleAmount + yScale: rect.scaleAmount + } + } + + // app icon + DelegateAppIcon { + id: icon + anchors.fill: parent + source: root.application.icon + + property real scaleAmount: root.turnToFolder ? 0.3 : 1.0 + Behavior on scaleAmount { + enabled: root.turnToFolderAnimEnabled + NumberAnimation { duration: root.turnToFolderAnimEnabled ? Kirigami.Units.longDuration : 0; easing.type: Easing.InOutQuad } + } + + transform: Scale { + origin.x: icon.width / 2 + origin.y: icon.height / 2 + xScale: icon.scaleAmount + yScale: icon.scaleAmount + } + + Rectangle { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: Kirigami.Units.smallSpacing + } + visible: root.application.running + radius: width + width: Kirigami.Units.smallSpacing + height: width + color: Kirigami.Theme.highlightColor + } + } + } +} + + diff --git a/containments/homescreens/folio/package/contents/ui/delegate/AppFolderDelegate.qml b/containments/homescreens/folio/package/contents/ui/delegate/AppFolderDelegate.qml new file mode 100644 index 00000000..5b5b67f4 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/AppFolderDelegate.qml @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import QtQuick.Effects + +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +AbstractDelegate { + id: root + name: folder.name + shadow: true + + property Folio.FolioApplicationFolder folder + + property bool appHoveredOver: false + + contentItem: DelegateFolderIcon { + folder: root.folder + expandBackground: root.appHoveredOver + } +} + + diff --git a/containments/homescreens/folio/package/contents/ui/delegate/DelegateAppIcon.qml b/containments/homescreens/folio/package/contents/ui/delegate/DelegateAppIcon.qml new file mode 100644 index 00000000..41b953a0 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/DelegateAppIcon.qml @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import QtQuick.Effects + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +Kirigami.Icon { + id: root + + height: Folio.FolioSettings.delegateIconSize + width: Folio.FolioSettings.delegateIconSize +} diff --git a/containments/homescreens/folio/package/contents/ui/delegate/DelegateFolderIcon.qml b/containments/homescreens/folio/package/contents/ui/delegate/DelegateFolderIcon.qml new file mode 100644 index 00000000..65c87df0 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/DelegateFolderIcon.qml @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import QtQuick.Effects + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +Item { + id: root + + property Folio.FolioApplicationFolder folder + + property bool expandBackground: false + + height: Folio.FolioSettings.delegateIconSize + width: Folio.FolioSettings.delegateIconSize + + Rectangle { + id: rect + radius: Kirigami.Units.largeSpacing + color: Qt.rgba(255, 255, 255, 0.3) + anchors.fill: parent + + property real scaleAmount: root.expandBackground ? 1.2 : 1.0 + + Behavior on scaleAmount { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } } + + transform: Scale { + origin.x: root.width / 2 + origin.y: root.height / 2 + xScale: rect.scaleAmount + yScale: rect.scaleAmount + } + } + + Grid { + id: previewGrid + anchors.fill: parent + anchors.margins: Kirigami.Units.smallSpacing * 2 + columns: 2 + spacing: Kirigami.Units.smallSpacing + + property var previews: root.folder.appPreviews + + Repeater { + model: previewGrid.previews + delegate: Kirigami.Icon { + implicitWidth: Math.round((previewGrid.width - previewGrid.spacing) / 2) + implicitHeight: Math.round((previewGrid.width - previewGrid.spacing) / 2) + source: modelData.icon + } + } + } +} + diff --git a/containments/homescreens/folio/package/contents/ui/delegate/DelegateIconLoader.qml b/containments/homescreens/folio/package/contents/ui/delegate/DelegateIconLoader.qml new file mode 100644 index 00000000..58ae578b --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/DelegateIconLoader.qml @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import QtQuick.Effects + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +Loader { + id: root + + height: Folio.FolioSettings.delegateIconSize + width: Folio.FolioSettings.delegateIconSize + + property Folio.FolioDelegate delegate + + sourceComponent: { + if (!delegate) { + return noIcon; + } else if (delegate.type === Folio.FolioDelegate.Application) { + return appIcon; + } else if (delegate.type === Folio.FolioDelegate.Folder) { + return folderIcon; + } else { + return noIcon; + } + } + + Component { + id: noIcon + Item {} + } + + Component { + id: appIcon + + DelegateAppIcon { + source: delegate.application.icon + } + } + + Component { + id: folderIcon + + DelegateFolderIcon { + folder: delegate.folder + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/delegate/DelegateLabel.qml b/containments/homescreens/folio/package/contents/ui/delegate/DelegateLabel.qml new file mode 100644 index 00000000..ec0a7e3e --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/DelegateLabel.qml @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.plasma.components 3.0 as PC3 +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +PC3.Label { + id: label + wrapMode: Text.WordWrap + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + elide: Text.ElideRight + + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 + font.weight: Font.Bold +} diff --git a/containments/homescreens/folio/package/contents/ui/delegate/DelegateShadow.qml b/containments/homescreens/folio/package/contents/ui/delegate/DelegateShadow.qml new file mode 100644 index 00000000..ac8754f5 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/DelegateShadow.qml @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Effects + +MultiEffect { + shadowEnabled: true + shadowVerticalOffset: 1 + blurMax: 16 + shadowOpacity: 0.5 +} diff --git a/containments/homescreens/folio/package/contents/ui/main.qml b/containments/homescreens/folio/package/contents/ui/main.qml index eff9dff1..46304903 100644 --- a/containments/homescreens/folio/package/contents/ui/main.qml +++ b/containments/homescreens/folio/package/contents/ui/main.qml @@ -1,9 +1,5 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * SPDX-FileCopyrightText: 2021 Devin Lin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later import QtQuick 2.15 import QtQuick.Window 2.15 @@ -19,59 +15,88 @@ import org.kde.plasma.private.mobileshell.state 1.0 as MobileShellState import org.kde.private.mobile.homescreen.folio 1.0 as Folio import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin - ContainmentItem { id: root Component.onCompleted: { + Folio.FolioSettings.load(); + Folio.ApplicationListModel.load(); + Folio.FavouritesModel.load(); + Folio.PageListModel.load(); + // ensure the gestures work immediately on load forceActiveFocus(); } - Plasmoid.onActivated: { - // there's a couple of steps: - // - minimize windows (only if we are in an app) - // - open app drawer - // - close app drawer and, if necessary, restore windows + function homeAction() { + const isInWindow = (!WindowPlugin.WindowUtil.isShowingDesktop && WindowPlugin.WindowMaximizedTracker.showingWindow); - // Always close action drawer - if (MobileShellState.ShellDBusClient.isActionDrawerOpen) { - MobileShellState.ShellDBusClient.closeActionDrawer(); - } - - if (!WindowPlugin.WindowUtil.isShowingDesktop && WindowPlugin.WindowMaximizedTracker.showingWindow - || MobileShellState.ShellDBusClient.isActionDrawerOpen - || searchWidget.isOpen - ) { - - // Always close the search widget as well - if (searchWidget.isOpen) { - searchWidget.close(); - } - - } else if (folioHomeScreen.homeScreenState.currentView === HomeScreenState.PageView) { - folioHomeScreen.homeScreenState.openAppDrawer(); + if (isInWindow) { + Folio.HomeScreenState.closeFolder(); + Folio.HomeScreenState.closeSearchWidget(); + Folio.HomeScreenState.closeAppDrawer(); + Folio.HomeScreenState.goToPage(0); } else { - folioHomeScreen.homeScreenState.closeAppDrawer(); + switch (Folio.HomeScreenState.viewState) { + case Folio.HomeScreenState.PageView: + if (Folio.HomeScreenState.currentPage === 0) { + Folio.HomeScreenState.openAppDrawer(); + } else { + Folio.HomeScreenState.goToPage(0); + } + break; + case Folio.HomeScreenState.AppDrawerView: + Folio.HomeScreenState.closeAppDrawer(); + break; + case Folio.HomeScreenState.SearchWidgetView: + Folio.HomeScreenState.closeSearchWidget(); + break; + case Folio.HomeScreenState.FolderView: + Folio.HomeScreenState.closeFolder(); + break; + } } } + Plasmoid.onActivated: homeAction() + + Rectangle { + id: appDrawerBackground + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.6) + + opacity: Folio.HomeScreenState.appDrawerOpenProgress + } + + Rectangle { + id: searchWidgetBackground + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.3) + + opacity: Folio.HomeScreenState.searchWidgetOpenProgress + } + + Rectangle { + id: settingsViewBackground + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.3) + + opacity: Folio.HomeScreenState.settingsOpenProgress + } + MobileShell.HomeScreen { id: homeScreen + anchors.fill: parent plasmoidItem: root onResetHomeScreenPosition: { - folioHomeScreen.homeScreenState.animateGoToPageIndex(0, Kirigami.Units.longDuration); - folioHomeScreen.homeScreenState.closeAppDrawer(); + // NOTE: empty, because this is handled by homeAction() } - onHomeTriggered: { - searchWidget.close(); - } - - property bool componentComplete: false + onHomeTriggered: root.homeAction() contentItem: Item { + // homescreen component HomeScreen { id: folioHomeScreen @@ -82,38 +107,9 @@ ContainmentItem { leftMargin: homeScreen.leftMargin rightMargin: homeScreen.rightMargin - opacity: (1 - searchWidget.openFactor) - // make the homescreen not interactable when task switcher or startup feedback is on interactive: !homeScreen.overlayShown } - - // search component - MobileShell.KRunnerWidget { - id: searchWidget - anchors.fill: parent - - visible: openFactor > 0 - - topMargin: homeScreen.topMargin - bottomMargin: homeScreen.bottomMargin - leftMargin: homeScreen.leftMargin - rightMargin: homeScreen.rightMargin - } - } - - Connections { - target: folioHomeScreen.homeScreenState - - function onSwipeDownGestureBegin() { - searchWidget.startGesture(); - } - function onSwipeDownGestureEnd() { - searchWidget.endGesture(); - } - function onSwipeDownGestureOffset(offset) { - searchWidget.updateGestureOffset(-offset); - } } // listen to app launch errors @@ -125,3 +121,4 @@ ContainmentItem { } } } + diff --git a/containments/homescreens/folio/package/contents/ui/private/ActionButton.qml b/containments/homescreens/folio/package/contents/ui/private/ActionButton.qml deleted file mode 100644 index dec9b617..00000000 --- a/containments/homescreens/folio/package/contents/ui/private/ActionButton.qml +++ /dev/null @@ -1,138 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2011 Marco Martin - * SPDX-FileCopyrightText: 2013 Sebastian Kügler - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.0 -import org.kde.plasma.core as PlasmaCore -import org.kde.ksvg 1.0 as KSvg -import org.kde.kirigami 2.20 as Kirigami - -PlasmaCore.ToolTipArea { - id: button - - location: PlasmaCore.Types.LeftEdge - mainText: action !== undefined ? action.text : "" - mainItem: toolTipDelegate - - //API - property KSvg.Svg svg - property alias elementId: icon.elementId - property QtObject action - property bool backgroundVisible: false - property int iconSize: Kirigami.Units.iconSizes.large - property int pressedOffset: 1 - property bool checked: false - property bool toggle: false - property string text - - signal clicked - signal requestEditModeClose - - implicitWidth: buttonRow.implicitWidth - implicitHeight: buttonRow.implicitHeight - - opacity: action==undefined||action.enabled?1:0.6 - - Behavior on opacity { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - } - - onCheckedChanged: { - if (checked) { - buttonItem.elementId = "pressed" - shadowItem.opacity = 0 - } else { - buttonItem.elementId = "normal" - shadowItem.opacity = 1 - } - } - - KSvg.Svg { - id: buttonSvg - imagePath: "widgets/actionbutton" - } - - KSvg.SvgItem { - id: shadowItem - svg: buttonSvg - elementId: "shadow" - width: iconSize+13//button.backgroundVisible?iconSize+8:iconSize - height: width - visible: button.backgroundVisible - } - - Row { - id: buttonRow - - Item { - width: buttonItem.visible?buttonItem.width:iconSize - height: buttonItem.visible?buttonItem.height:iconSize - - KSvg.SvgItem { - id: buttonItem - svg: buttonSvg - elementId: "normal" - width: shadowItem.width - height: shadowItem.height - visible: backgroundVisible - } - - KSvg.SvgItem { - id: icon - width: iconSize - height: iconSize - svg: button.svg - anchors.centerIn: parent - } - } - - Text { - id: actionText - text: button.text - style: Text.Outline - color: Kirigami.Theme.textColor - styleColor: Qt.rgba(1,1,1,0.4) - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - anchors.fill: parent - anchors.leftMargin: -10 - anchors.topMargin: -10 - anchors.rightMargin: -10 - anchors.bottomMargin: -10 - preventStealing: true - onPressed: { - buttonItem.elementId = "pressed" - shadowItem.opacity = 0; - button.x = button.x + button.pressedOffset; - button.y = button.y + button.pressedOffset; - } - onReleased: { - if (button.checked || !button.toggle) { - buttonItem.elementId = "normal" - shadowItem.opacity = 1 - button.checked = false - } else { - button.checked = true - } - button.x = button.x - button.pressedOffset; - button.y = button.y - button.pressedOffset; - } - onClicked: { - if (action) { - action.trigger() - } else { - button.clicked() - } - requestEditModeClose(); - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/private/ConfigOverlay.qml b/containments/homescreens/folio/package/contents/ui/private/ConfigOverlay.qml deleted file mode 100644 index e1eb2bc8..00000000 --- a/containments/homescreens/folio/package/contents/ui/private/ConfigOverlay.qml +++ /dev/null @@ -1,132 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Layouts 1.1 - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.ksvg 1.0 as KSvg - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager - -ContainmentLayoutManager.ConfigOverlayWithHandles { - id: overlay - property var appletContainer - - signal requestRemoveTrigger - signal requestEditModeClose - - readonly property int iconSize: Kirigami.Units.iconSizes.medium - KSvg.Svg { - id: configIconsSvg - imagePath: "widgets/configuration-icons" - } - - PlasmaComponents.Label { - id: toolTipDelegate - - width: contentWidth - height: undefined - - property Item toolTip - - text: (toolTip != null) ? toolTip.mainText : "" - } - - SequentialAnimation { - id: removeAnim - NumberAnimation { - target: overlay.itemContainer - property: "scale" - from: 1 - to: 0 - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - ScriptAction { - script: { - overlay.requestRemoveTrigger(); - overlay.requestEditModeClose(); - } - } - } - KSvg.FrameSvgItem { - id: frame - anchors.centerIn: parent - - width: layout.implicitWidth + margins.left + margins.right - height: layout.implicitHeight + margins.top + margins.bottom - imagePath: "widgets/background" - - RowLayout { - id: layout - spacing: Kirigami.Units.gridUnit - anchors { - fill: parent - topMargin: parent.margins.top - leftMargin: parent.margins.left - bottomMargin: parent.margins.bottom - rightMargin: parent.margins.right - } - - ActionButton { - svg: configIconsSvg - elementId: "configure" - iconSize: overlay.iconSize - visible: (action && typeof(action) != "undefined") ? action.enabled : false - action: (applet) ? applet.action("configure") : null - - onRequestEditModeClose: { - overlay.requestEditModeClose(); - } - - Component.onCompleted: { - if (action && typeof(action) != "undefined") { - action.enabled = true - } - } - } - - ActionButton { - id: closeButton - svg: configIconsSvg - elementId: "delete" - mainText: i18n("Remove") - iconSize: overlay.iconSize - visible: { - if (!applet) { - return false; - } - var a = applet.action("remove"); - return (a && typeof(a) != "undefined") ? a.enabled : false; - } - // we don't set action, since we want to catch the button click, - // animate, and then trigger the "remove" action - // Triggering the action is handled in the overlay.itemContainer, we just - // emit a signal here to avoid the applet-gets-removed-before-we- - // can-animate it race condition. - onClicked: { - removeAnim.restart(); - } - - onRequestEditModeClose: { - overlay.requestEditModeClose(); - } - - Component.onCompleted: { - var a = applet.action("remove"); - if (a && typeof(a) != "undefined") { - a.enabled = true - } - } - } - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/private/DelegateRemoveButton.qml b/containments/homescreens/folio/package/contents/ui/private/DelegateRemoveButton.qml deleted file mode 100644 index b6e5f2db..00000000 --- a/containments/homescreens/folio/package/contents/ui/private/DelegateRemoveButton.qml +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.4 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 2.3 as Controls -import Qt5Compat.GraphicalEffects - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PC3 -import org.kde.kquickcontrolsaddons 2.0 - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager -import org.kde.private.mobile.homescreen.folio 1.0 as Folio - -PC3.RoundButton { - id: removeButton - - required property Folio.DesktopModel desktopModel - - anchors { - right: parent.right - top: parent.top - } - visible: false - icon.name: "delete" - onClicked: delegateDestructionAnim.restart() - - function show() { - scale = 0; - visible = true; - removeButtonScaleAnim.from = 0; - removeButtonScaleAnim.to = 1; - removeButtonAnim.running = true; - } - - function hide() { - if (!visible) { - return; - } - removeButtonScaleAnim.from = 1; - removeButtonScaleAnim.to = 0; - removeButtonAnim.running = true; - } - - SequentialAnimation { - id: delegateDestructionAnim - NumberAnimation { - target: delegate - property: "scale" - from: 1 - to: 0 - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - ScriptAction { - script: { - appletsLayout.releaseSpace(delegate); - desktopModel.removeFavorite(index); - } - } - } - SequentialAnimation { - id: removeButtonAnim - NumberAnimation { - id: removeButtonScaleAnim - target: removeButton - property: "scale" - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - ScriptAction { - script: { - if (removeButton.scale === 0) { - removeButton.visible = false; - } - } - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/private/GradientBar.qml b/containments/homescreens/folio/package/contents/ui/private/GradientBar.qml deleted file mode 100644 index ea3a7823..00000000 --- a/containments/homescreens/folio/package/contents/ui/private/GradientBar.qml +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.15 -import org.kde.kirigami 2.20 as Kirigami - -Item { - Behavior on opacity { - OpacityAnimator { - duration: Kirigami.Units.longDuration * 2 - easing.type: Easing.InOutQuad - } - } - - Rectangle { - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - leftMargin: Kirigami.Units.gridUnit + root.leftPadding - rightMargin: Kirigami.Units.gridUnit + root.rightPadding - } - height: 1 - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0.0; color: Qt.rgba(1, 1, 1, 0) } - GradientStop { position: 0.15; color: Qt.rgba(1, 1, 1, 0.5) } - GradientStop { position: 0.5; color: Qt.rgba(1, 1, 1, 1) } - GradientStop { position: 0.85; color: Qt.rgba(1, 1, 1, 0.5) } - GradientStop { position: 1.0; color: Qt.rgba(1, 1, 1, 0) } - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/private/OpenDrawerButton.qml b/containments/homescreens/folio/package/contents/ui/private/OpenDrawerButton.qml deleted file mode 100644 index e917f31b..00000000 --- a/containments/homescreens/folio/package/contents/ui/private/OpenDrawerButton.qml +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: LGPL-2.0-or-later - */ - -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.1 -import Qt5Compat.GraphicalEffects - -import org.kde.kirigami 2.20 as Kirigami - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.draganddrop 2.0 as DragDrop - -import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager - -MouseArea { - id: arrowUpIcon - z: 9 - - property Flickable flickable - property real factor: 0 - - cursorShape: Qt.PointingHandCursor - height: Kirigami.Units.iconSizes.smallMedium - signal openRequested - signal closeRequested - - onClicked: { - openRequested(); - } - - Item { - anchors.centerIn: parent - - width: Kirigami.Units.iconSizes.smallMedium - height: width - - Rectangle { - anchors { - verticalCenter: parent.verticalCenter - right: parent.horizontalCenter - left: parent.left - verticalCenterOffset: -arrowUpIcon.height/4 + (arrowUpIcon.height/4) * arrowUpIcon.factor - } - color: "white" - transformOrigin: Item.Right - rotation: -45 + 90 * arrowUpIcon.factor - antialiasing: true - height: 1 - } - Rectangle { - anchors { - verticalCenter: parent.verticalCenter - left: parent.horizontalCenter - right: parent.right - verticalCenterOffset: -arrowUpIcon.height/4 + (arrowUpIcon.height/4) * arrowUpIcon.factor - } - color: "white" - transformOrigin: Item.Left - rotation: 45 - 90 * arrowUpIcon.factor - antialiasing: true - height: 1 - } - } -} - diff --git a/containments/homescreens/folio/package/contents/ui/private/ScrollIndicator.qml b/containments/homescreens/folio/package/contents/ui/private/ScrollIndicator.qml deleted file mode 100644 index 594ba3fa..00000000 --- a/containments/homescreens/folio/package/contents/ui/private/ScrollIndicator.qml +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2015 Marco Martin - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick 2.4 -import Qt5Compat.GraphicalEffects -import org.kde.ksvg 1.0 as KSvg -import org.kde.kirigami 2.20 as Kirigami - -KSvg.SvgItem { - id: scrollIndicator - - anchors.verticalCenter: parent.verticalCenter - - z: 2 - opacity: 0 - - Kirigami.Theme.colorSet: Kirigami.Theme.Complementary - Kirigami.Theme.inherit: false - - imagePath: "widgets/arrows" - elementId: "left-arrow" - width: Kirigami.Units.iconSizes.large - height: width - layer.enabled: true - layer.effect: DropShadow { - cached: true - horizontalOffset: 0 - verticalOffset: 2 - radius: 8.0 - samples: 16 - color: Qt.rgba(0, 0, 0, 0.8) - } - Behavior on opacity { - OpacityAnimator { - duration: Kirigami.Units.longDuration * 2 - easing.type: Easing.InOutQuad - } - } -} diff --git a/containments/homescreens/folio/package/contents/ui/settings/SettingsComponent.qml b/containments/homescreens/folio/package/contents/ui/settings/SettingsComponent.qml new file mode 100644 index 00000000..297a6e44 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/settings/SettingsComponent.qml @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Window +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.plasma.components 3.0 as PC3 +import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.private.mobile.homescreen.folio 1.0 as Folio + +import '../delegate' + +Item { + id: root + + property var homeScreen + property real settingsModeHomeScreenScale + + signal requestLeaveSettingsMode() + + MouseArea { + id: closeSettings + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: settingsBar.top + + onClicked: { + Folio.HomeScreenState.closeSettingsView(); + } + } + + Item { + id: settingsBar + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: root.height * (1 - settingsModeHomeScreenScale) + + RowLayout { + id: settingsOptions + anchors.centerIn: parent + spacing: Kirigami.Units.largeSpacing + + QQC2.ToolButton { + icon.source: 'edit-image' + text: i18n('Wallpapers') + enabled: false + display: QQC2.ToolButton.TextUnderIcon + + implicitHeight: Kirigami.Units.gridUnit * 4 + implicitWidth: Kirigami.Units.gridUnit * 5 + } + + QQC2.ToolButton { + icon.source: 'settings-configure' + text: ('Settings') + display: QQC2.ToolButton.TextUnderIcon + + implicitHeight: Kirigami.Units.gridUnit * 4 + implicitWidth: Kirigami.Units.gridUnit * 5 + + onClicked: settingsWindow.showMaximized() + } + + QQC2.ToolButton { + icon.source: 'widget-alternatives' + text: 'Widgets' + enabled: false + display: QQC2.ToolButton.TextUnderIcon + + implicitHeight: Kirigami.Units.gridUnit * 4 + implicitWidth: Kirigami.Units.gridUnit * 5 + } + } + } + + SettingsWindow { + id: settingsWindow + visible: false + + onRequestConfigureMenu: { + homeScreen.openConfigure() + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/settings/SettingsWindow.qml b/containments/homescreens/folio/package/contents/ui/settings/SettingsWindow.qml new file mode 100644 index 00000000..01b37e92 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/settings/SettingsWindow.qml @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Window +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.private.mobile.homescreen.folio 1.0 as Folio +import org.kde.kirigamiaddons.formcard 1.0 as FormCard + +import '../delegate' + +Kirigami.ApplicationWindow { + id: root + flags: Qt.FramelessWindowHint + + pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar + pageStack.globalToolBar.showNavigationButtons: Kirigami.ApplicationHeaderStyle.NoNavigationButtons; + + signal requestConfigureMenu() + + pageStack.initialPage: Kirigami.ScrollablePage { + id: page + opacity: root.opacity + + titleDelegate: RowLayout { + QQC2.ToolButton { + Layout.leftMargin: -Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing + icon.name: "arrow-left" + onClicked: root.close() + } + + Kirigami.Heading { + level: 1 + text: page.title + } + } + + title: i18n("Homescreen Settings") + + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + + ColumnLayout { + FormCard.FormHeader { + title: i18n("Icons") + } + + FormCard.FormCard { + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + + Item { + Layout.preferredHeight: Folio.HomeScreenState.pageCellHeight + Layout.fillWidth: true + + AbstractDelegate { + anchors.centerIn: parent + implicitHeight: Folio.HomeScreenState.pageCellHeight + implicitWidth: Folio.HomeScreenState.pageCellWidth + name: i18n('Application') + + contentItem: DelegateAppIcon { + height: Folio.FolioSettings.delegateIconSize + width: Folio.FolioSettings.delegateIconSize + source: 'applications-system' + } + } + } + } + + FormCard.FormCard { + id: iconsCard + readonly property bool isVerticalOrientation: Folio.HomeScreenState.pageOrientation === Folio.HomeScreenState.RegularPosition || + Folio.HomeScreenState.pageOrientation === Folio.HomeScreenState.RotateUpsideDown + + readonly property string numOfRowsText: i18n("Number of rows") + readonly property string numOfColumnsText: i18n("Number of columns") + + FormCard.FormSpinBoxDelegate { + id: iconSizeSpinBox + label: i18n("Size of icons on homescreen") + from: 16 + to: 128 + value: Folio.FolioSettings.delegateIconSize + onValueChanged: { + if (value !== Folio.FolioSettings.delegateIconSize) { + Folio.FolioSettings.delegateIconSize = value; + } + } + } + + FormCard.FormSpinBoxDelegate { + id: rowsSpinBox + label: iconsCard.isVerticalOrientation ? iconsCard.numOfRowsText : iconsCard.numOfColumnsText + from: 3 + to: 10 + value: Folio.FolioSettings.homeScreenRows + onValueChanged: { + if (value !== Folio.FolioSettings.homeScreenRows) { + Folio.FolioSettings.homeScreenRows = value; + } + } + } + + FormCard.FormSpinBoxDelegate { + id: columnsSpinBox + label: iconsCard.isVerticalOrientation ? iconsCard.numOfColumnsText : iconsCard.numOfRowsText + from: 3 + to: 10 + value: Folio.FolioSettings.homeScreenColumns + onValueChanged: { + if (value !== Folio.FolioSettings.homeScreenColumns) { + Folio.FolioSettings.homeScreenColumns = value; + } + } + } + } + + FormCard.FormSectionText { + text: i18n("The rows and columns will swap depending on the screen rotation.") + } + + FormCard.FormHeader { + title: i18n("Labels") + } + + FormCard.FormCard { + FormCard.FormSwitchDelegate { + id: showLabelsOnHomeScreen + text: i18n("Show labels on homescreen") + checked: Folio.FolioSettings.showPagesAppLabels + onCheckedChanged: { + if (checked != Folio.FolioSettings.showPagesAppLabels) { + Folio.FolioSettings.showPagesAppLabels = checked; + } + } + } + + FormCard.FormDelegateSeparator { above: showLabelsOnHomeScreen; below: showLabelsInFavourites } + + FormCard.FormSwitchDelegate { + id: showLabelsInFavourites + text: i18n("Show labels in favorites bar") + checked: Folio.FolioSettings.showFavouritesAppLabels + onCheckedChanged: { + if (checked != Folio.FolioSettings.showFavouritesAppLabels) { + Folio.FolioSettings.showFavouritesAppLabels = checked; + } + } + } + } + + FormCard.FormHeader { + title: i18n("Favorites Bar") + } + + FormCard.FormCard { + FormCard.FormSwitchDelegate { + text: i18n('Show background') + icon.name: 'draw-rectangle' + checked: Folio.FolioSettings.showFavouritesBarBackground + onCheckedChanged: { + if (checked !== Folio.FolioSettings.showFavouritesBarBackground) { + Folio.FolioSettings.showFavouritesBarBackground = checked; + } + } + } + } + + FormCard.FormHeader { + title: i18n("General") + } + + FormCard.FormCard { + FormCard.FormButtonDelegate { + id: containmentSettings + text: i18n('Switch Homescreen') + icon.name: 'settings-configure' + onClicked: root.requestConfigureMenu() + } + + FormCard.FormDelegateSeparator { above: containmentSettings; below: exportSettings } + + FormCard.FormButtonDelegate { + id: exportSettings + enabled: false + text: 'Export layout (in development)' + icon.name: 'document-export' + } + + FormCard.FormDelegateSeparator { above: exportSettings; below: importSettings } + + FormCard.FormButtonDelegate { + id: importSettings + enabled: false + text: 'Import layout (in development)' + icon.name: 'document-import' + } + } + } + } +} diff --git a/containments/homescreens/folio/pagelistmodel.cpp b/containments/homescreens/folio/pagelistmodel.cpp new file mode 100644 index 00000000..9b3ae2d5 --- /dev/null +++ b/containments/homescreens/folio/pagelistmodel.cpp @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "pagelistmodel.h" +#include "homescreenstate.h" + +#include +#include + +PageListModel *PageListModel::self() +{ + static PageListModel *model = new PageListModel; + return model; +} + +PageListModel::PageListModel(QObject *parent) + : QAbstractListModel{parent} +{ +} + +int PageListModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_pages.size(); +} + +QVariant PageListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + switch (role) { + case PageRole: + return QVariant::fromValue(m_pages.at(index.row())); + } + + return QVariant(); +} + +QHash PageListModel::roleNames() const +{ + return {{PageRole, "delegate"}}; +} + +int PageListModel::length() +{ + return m_pages.size(); +} + +PageModel *PageListModel::getPage(int index) +{ + if (index < 0 || index >= m_pages.size()) { + return nullptr; + } + + return m_pages[index]; +} + +void PageListModel::removePage(int index) +{ + if (index < 0 || index >= m_pages.size()) { + return; + } + + beginRemoveRows(QModelIndex(), index, index); + m_pages[index]->deleteLater(); + m_pages.removeAt(index); + endRemoveRows(); + + Q_EMIT lengthChanged(); + + save(); +} + +Q_INVOKABLE void PageListModel::addPageAtEnd() +{ + beginInsertRows(QModelIndex(), m_pages.size(), m_pages.size()); + + PageModel *page = new PageModel{{}, this}; + connect(page, &PageModel::saveRequested, this, &PageListModel::save); + + m_pages.append(page); + + endInsertRows(); + + Q_EMIT lengthChanged(); + + save(); +} + +bool PageListModel::isLastPageEmpty() +{ + return m_pages.size() == 0 ? true : m_pages[m_pages.size() - 1]->isPageEmpty(); +} + +void PageListModel::save() +{ + if (!m_applet) { + return; + } + + QJsonArray arr; + for (auto &page : m_pages) { + arr.push_back(page->toJson()); + } + QByteArray data = QJsonDocument(arr).toJson(QJsonDocument::Compact); + + m_applet->config().writeEntry("Pages", QString::fromStdString(data.toStdString())); + Q_EMIT m_applet->configNeedsSaving(); +} + +void PageListModel::load() +{ + if (!m_applet) { + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(m_applet->config().readEntry("Pages", "{}").toUtf8()); + + beginResetModel(); + + m_pages.clear(); + + for (QJsonValueRef r : doc.array()) { + QJsonArray obj = r.toArray(); + + PageModel *page = PageModel::fromJson(obj, this); + if (page) { + connect(page, &PageModel::saveRequested, this, &PageListModel::save); + m_pages.append(page); + } + } + + endResetModel(); + + Q_EMIT lengthChanged(); + + // add page if there are no pages + if (m_pages.size() == 0) { + addPageAtEnd(); + } +} + +void PageListModel::setApplet(Plasma::Applet *applet) +{ + m_applet = applet; +} diff --git a/containments/homescreens/folio/pagelistmodel.h b/containments/homescreens/folio/pagelistmodel.h new file mode 100644 index 00000000..867ed314 --- /dev/null +++ b/containments/homescreens/folio/pagelistmodel.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "pagemodel.h" + +#include +#include + +#include + +class PageListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int length READ length NOTIFY lengthChanged) + +public: + enum Roles { PageRole = Qt::UserRole + 1 }; + + PageListModel(QObject *parent = nullptr); + + static PageListModel *self(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + int length(); + + PageModel *getPage(int index); + void removePage(int index); + Q_INVOKABLE void addPageAtEnd(); + bool isLastPageEmpty(); + + void save(); + Q_INVOKABLE void load(); + + void setApplet(Plasma::Applet *applet); + +Q_SIGNALS: + void lengthChanged(); + +private: + QList m_pages; + + Plasma::Applet *m_applet{nullptr}; +}; diff --git a/containments/homescreens/folio/pagemodel.cpp b/containments/homescreens/folio/pagemodel.cpp new file mode 100644 index 00000000..3269805f --- /dev/null +++ b/containments/homescreens/folio/pagemodel.cpp @@ -0,0 +1,291 @@ +// SPDX-FileCopyrightText: 2022-2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "pagemodel.h" +#include "foliosettings.h" +#include "homescreenstate.h" + +FolioPageDelegate::FolioPageDelegate(int row, int column, QObject *parent) + : FolioDelegate{parent} + , m_row{row} + , m_column{column} +{ + init(); +} + +FolioPageDelegate::FolioPageDelegate(int row, int column, FolioApplication *application, QObject *parent) + : FolioDelegate{application, parent} + , m_row{row} + , m_column{column} +{ + init(); +} + +FolioPageDelegate::FolioPageDelegate(int row, int column, FolioApplicationFolder *folder, QObject *parent) + : FolioDelegate{folder, parent} + , m_row{row} + , m_column{column} +{ + init(); +} + +FolioPageDelegate::FolioPageDelegate(int row, int column, FolioDelegate *delegate, QObject *parent) + : FolioDelegate{parent} + , m_row{row} + , m_column{column} +{ + m_type = delegate->type(); + m_application = delegate->application(); + m_folder = delegate->folder(); + + init(); +} + +void FolioPageDelegate::init() +{ + // we have to use the "real" rows and columns, so fetch them from FolioSettings instead of HomeScreenState + switch (HomeScreenState::self()->pageOrientation()) { + case HomeScreenState::RegularPosition: + m_realRow = m_row; + m_realColumn = m_column; + break; + case HomeScreenState::RotateClockwise: + m_realRow = HomeScreenState::self()->pageColumns() - m_column - 1; + m_realColumn = m_row; + break; + case HomeScreenState::RotateCounterClockwise: // (0, 4) -> (4, 3) + m_realRow = m_column; + m_realColumn = HomeScreenState::self()->pageRows() - m_row - 1; + break; + case HomeScreenState::RotateUpsideDown: + m_realRow = HomeScreenState::self()->pageRows() - m_row - 1; + m_realColumn = HomeScreenState::self()->pageColumns() - m_column - 1; + break; + } + + connect(HomeScreenState::self(), &HomeScreenState::pageOrientationChanged, this, [this]() { + setRow(getTranslatedRow(m_realRow, m_realColumn)); + setColumn(getTranslatedColumn(m_realRow, m_realColumn)); + }); +} + +FolioPageDelegate *FolioPageDelegate::fromJson(QJsonObject &obj, QObject *parent) +{ + FolioDelegate *fd = FolioDelegate::fromJson(obj, parent); + + if (!fd) { + return nullptr; + } + + int realRow = obj[QStringLiteral("row")].toInt(); + int realColumn = obj[QStringLiteral("column")].toInt(); + + int row = getTranslatedRow(realRow, realColumn); + int column = getTranslatedColumn(realRow, realColumn); + + FolioPageDelegate *delegate = new FolioPageDelegate{row, column, fd, parent}; + fd->deleteLater(); + + return delegate; +} + +int FolioPageDelegate::getTranslatedRow(int realRow, int realColumn) +{ + // we have to use the "real" rows and columns, so fetch them from FolioSettings instead of HomeScreenState + switch (HomeScreenState::self()->pageOrientation()) { + case HomeScreenState::RegularPosition: + return realRow; + case HomeScreenState::RotateClockwise: + return realColumn; + case HomeScreenState::RotateCounterClockwise: + return FolioSettings::self()->homeScreenColumns() - realColumn - 1; + case HomeScreenState::RotateUpsideDown: + return FolioSettings::self()->homeScreenRows() - realRow - 1; + } + return realRow; +} + +int FolioPageDelegate::getTranslatedColumn(int realRow, int realColumn) +{ + // we have to use the "real" rows and columns, so fetch them from FolioSettings instead of HomeScreenState + switch (HomeScreenState::self()->pageOrientation()) { + case HomeScreenState::RegularPosition: + return realColumn; + case HomeScreenState::RotateClockwise: + return FolioSettings::self()->homeScreenRows() - realRow - 1; + case HomeScreenState::RotateCounterClockwise: + return realRow; + case HomeScreenState::RotateUpsideDown: + return FolioSettings::self()->homeScreenColumns() - realColumn - 1; + } + return realRow; +} + +QJsonObject FolioPageDelegate::toJson() const +{ + QJsonObject o = FolioDelegate::toJson(); + o[QStringLiteral("row")] = m_realRow; + o[QStringLiteral("column")] = m_realColumn; + return o; +} + +int FolioPageDelegate::row() +{ + return m_row; +} + +void FolioPageDelegate::setRow(int row) +{ + m_row = row; + Q_EMIT rowChanged(); +} + +int FolioPageDelegate::column() +{ + return m_column; +} + +void FolioPageDelegate::setColumn(int column) +{ + m_column = column; + Q_EMIT columnChanged(); +} + +PageModel::PageModel(QList delegates, QObject *parent) + : QAbstractListModel{parent} + , m_delegates{delegates} +{ +} + +PageModel::~PageModel() = default; + +PageModel *PageModel::fromJson(QJsonArray &arr, QObject *parent) +{ + QList delegates; + QList folderDelegates; + + for (QJsonValueRef r : arr) { + QJsonObject obj = r.toObject(); + + FolioPageDelegate *delegate = FolioPageDelegate::fromJson(obj, parent); + if (delegate) { + delegates.append(delegate); + + if (delegate->type() == FolioDelegate::Folder) { + folderDelegates.append(delegate); + } + } + } + + PageModel *model = new PageModel{delegates, parent}; + + // ensure folders request saves + for (auto *delegate : folderDelegates) { + connect(delegate->folder(), &FolioApplicationFolder::saveRequested, model, &PageModel::save); + } + + return model; +} + +QJsonArray PageModel::toJson() const +{ + QJsonArray arr; + + for (FolioPageDelegate *delegate : m_delegates) { + if (!delegate) { + continue; + } + + arr.append(delegate->toJson()); + } + + return arr; +} + +int PageModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_delegates.size(); +} + +QVariant PageModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + switch (role) { + case DelegateRole: + return QVariant::fromValue(m_delegates.at(index.row())); + } + + return QVariant(); +} + +QHash PageModel::roleNames() const +{ + return {{DelegateRole, "delegate"}}; +} + +void PageModel::removeDelegate(int row, int col) +{ + bool removed = false; + + for (int i = 0; i < m_delegates.size(); ++i) { + if (m_delegates[i]->row() == row && m_delegates[i]->column() == col) { + beginRemoveRows(QModelIndex(), i, i); + // HACK: do not deleteLater(), because the delegate might still be used somewhere else + m_delegates.removeAt(i); + endRemoveRows(); + + removed = true; + } + } + + if (removed) { + save(); + } +} + +bool PageModel::addDelegate(FolioPageDelegate *delegate) +{ + if (delegate->row() < 0 || delegate->row() >= HomeScreenState::self()->pageRows() || delegate->column() < 0 + || delegate->column() >= HomeScreenState::self()->pageColumns()) { + return false; + } + + // check if there already exists a delegate in this space + for (FolioPageDelegate *d : m_delegates) { + if (d->row() == delegate->row() && d->column() == delegate->column()) { + return false; + } + } + + beginInsertRows(QModelIndex(), m_delegates.size(), m_delegates.size()); + m_delegates.append(delegate); + endInsertRows(); + + save(); + + return true; +} + +FolioPageDelegate *PageModel::getDelegate(int row, int col) +{ + for (FolioPageDelegate *d : m_delegates) { + if (d->row() == row && d->column() == col) { + return d; + } + } + return nullptr; +} + +bool PageModel::isPageEmpty() +{ + return m_delegates.size() == 0; +} + +void PageModel::save() +{ + Q_EMIT saveRequested(); +} diff --git a/containments/homescreens/folio/pagemodel.h b/containments/homescreens/folio/pagemodel.h new file mode 100644 index 00000000..b9eda204 --- /dev/null +++ b/containments/homescreens/folio/pagemodel.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2022-2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "folioapplication.h" +#include "folioapplicationfolder.h" +#include "foliodelegate.h" + +#include +#include +#include + +#include + +class FolioPageDelegate : public FolioDelegate +{ + Q_OBJECT + Q_PROPERTY(int row READ row NOTIFY rowChanged) + Q_PROPERTY(int column READ column NOTIFY columnChanged) + +public: + FolioPageDelegate(int row = 0, int column = 0, QObject *parent = nullptr); + FolioPageDelegate(int row, int column, FolioApplication *application, QObject *parent); + FolioPageDelegate(int row, int column, FolioApplicationFolder *folder, QObject *parent); + FolioPageDelegate(int row, int column, FolioDelegate *delegate, QObject *parent); + + static FolioPageDelegate *fromJson(QJsonObject &obj, QObject *parent); + static int getTranslatedRow(int realRow, int realColumn); + static int getTranslatedColumn(int realRow, int realColumn); + + virtual QJsonObject toJson() const override; + + int row(); + void setRow(int row); + + int column(); + void setColumn(int column); + +Q_SIGNALS: + void rowChanged(); + void columnChanged(); + +private: + void init(); + + int m_realRow; + int m_realColumn; + int m_row; + int m_column; +}; + +class PageModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles { + DelegateRole = Qt::UserRole + 1, + XPositionRole, + YPositionRole, + ShownRole, + }; + + PageModel(QList delegates = QList{}, QObject *parent = nullptr); + ~PageModel(); + + static PageModel *fromJson(QJsonArray &arr, QObject *parent); + + QJsonArray toJson() const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + Q_INVOKABLE void removeDelegate(int row, int col); + bool addDelegate(FolioPageDelegate *delegate); + FolioPageDelegate *getDelegate(int row, int col); + + bool isPageEmpty(); + +public Q_SLOTS: + void save(); + +Q_SIGNALS: + void saveRequested(); + +private: + QList m_delegates; +}; diff --git a/containments/homescreens/folio/plugin/CMakeLists.txt b/containments/homescreens/folio/plugin/CMakeLists.txt index a174305b..f2ed62c2 100644 --- a/containments/homescreens/folio/plugin/CMakeLists.txt +++ b/containments/homescreens/folio/plugin/CMakeLists.txt @@ -4,7 +4,17 @@ set(folioplugin_SRCS folioplugin.cpp applicationlistmodel.cpp - desktopmodel.cpp + homescreenstate.cpp + windowlistener.cpp + favouritesmodel.cpp + folioapplication.cpp + folioapplicationfolder.cpp + foliodelegate.cpp + foliosettings.cpp + pagemodel.cpp + pagelistmodel.cpp + delegatetoucharea.cpp + dragstate.cpp ) install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/private/mobile/homescreen/folio) @@ -26,4 +36,3 @@ target_link_libraries(folioplugin set_property(TARGET folioplugin PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/org/kde/private/mobile/homescreen/folio) install(TARGETS folioplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/private/mobile/homescreen/folio) - diff --git a/containments/homescreens/folio/plugin/applicationlistmodel.cpp b/containments/homescreens/folio/plugin/applicationlistmodel.cpp deleted file mode 100644 index c66b1882..00000000 --- a/containments/homescreens/folio/plugin/applicationlistmodel.cpp +++ /dev/null @@ -1,236 +0,0 @@ -// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas -// SPDX-FileCopyrightText: 2022 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "applicationlistmodel.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -ApplicationListModel::ApplicationListModel(QObject *parent) - : QAbstractListModel(parent) -{ - connect(KSycoca::self(), &KSycoca::databaseChanged, this, &ApplicationListModel::sycocaDbChanged); - - // initialize wayland window checking - KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(this); - if (!connection) { - return; - } - - auto *registry = new KWayland::Client::Registry(this); - registry->create(connection); - - connect(registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, this, [this, registry](quint32 name, quint32 version) { - m_windowManagement = registry->createPlasmaWindowManagement(name, version, this); - qRegisterMetaType>("QVector"); - connect(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated, this, &ApplicationListModel::windowCreated); - }); - - registry->setup(); - connection->roundtrip(); - - load(); -} - -ApplicationListModel::~ApplicationListModel() = default; - -ApplicationListModel *ApplicationListModel::self() -{ - static ApplicationListModel *inst = new ApplicationListModel(nullptr); - return inst; -} - -QHash ApplicationListModel::roleNames() const -{ - return {{ApplicationNameRole, QByteArrayLiteral("applicationName")}, - {ApplicationIconRole, QByteArrayLiteral("applicationIcon")}, - {ApplicationStorageIdRole, QByteArrayLiteral("applicationStorageId")}, - {ApplicationEntryPathRole, QByteArrayLiteral("applicationEntryPath")}, - {ApplicationStartupNotifyRole, QByteArrayLiteral("applicationStartupNotify")}, - {ApplicationRunningRole, QByteArrayLiteral("applicationRunning")}, - {ApplicationUniqueIdRole, QByteArrayLiteral("applicationUniqueId")}, - {ApplicationLocationRole, QByteArrayLiteral("applicationLocation")}}; -} - -void ApplicationListModel::sycocaDbChanged() -{ - load(); -} - -void ApplicationListModel::windowCreated(KWayland::Client::PlasmaWindow *window) -{ - if (window->appId() == QStringLiteral("org.kde.plasmashell")) { - return; - } - int idx = 0; - for (auto i = m_applicationList.begin(); i != m_applicationList.end(); i++) { - if ((*i).storageId == window->appId() + QStringLiteral(".desktop")) { - (*i).window = window; - Q_EMIT dataChanged(index(idx, 0), index(idx, 0)); - connect(window, &KWayland::Client::PlasmaWindow::unmapped, this, [this, window]() { - int idx = 0; - for (auto i = m_applicationList.begin(); i != m_applicationList.end(); i++) { - if ((*i).storageId == window->appId() + QStringLiteral(".desktop")) { - (*i).window = nullptr; - Q_EMIT dataChanged(index(idx, 0), index(idx, 0)); - break; - } - idx++; - } - }); - break; - } - idx++; - } -} - -void ApplicationListModel::load() -{ - auto cfg = KSharedConfig::openConfig(QStringLiteral("applications-blacklistrc")); - auto blgroup = KConfigGroup(cfg, QStringLiteral("Applications")); - - const QStringList blacklist = blgroup.readEntry("blacklist", QStringList()); - - beginResetModel(); - - m_applicationList.clear(); - - QList unorderedList; - - auto filter = [blacklist](const KService::Ptr &service) -> bool { - if (service->noDisplay()) { - return false; - } - - if (!service->showOnCurrentPlatform()) { - return false; - } - - if (blacklist.contains(service->desktopEntryName())) { - return false; - } - - return true; - }; - - const KService::List apps = KApplicationTrader::query(filter); - - for (const KService::Ptr &service : apps) { - ApplicationData data; - data.name = service->name(); - data.icon = service->icon(); - data.storageId = service->storageId(); - data.uniqueId = service->storageId(); - data.entryPath = service->exec(); - data.startupNotify = service->startupNotify().value_or(false); - unorderedList << data; - } - - std::sort(unorderedList.begin(), unorderedList.end(), [](const ApplicationListModel::ApplicationData &a1, const ApplicationListModel::ApplicationData &a2) { - return a1.name.compare(a2.name, Qt::CaseInsensitive) < 0; - }); - - m_applicationList << unorderedList; - - endResetModel(); -} - -QVariant ApplicationListModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) { - return QVariant(); - } - - switch (role) { - case Qt::DisplayRole: - case ApplicationNameRole: - return m_applicationList.at(index.row()).name; - case ApplicationIconRole: - return m_applicationList.at(index.row()).icon; - case ApplicationStorageIdRole: - return m_applicationList.at(index.row()).storageId; - case ApplicationEntryPathRole: - return m_applicationList.at(index.row()).entryPath; - case ApplicationStartupNotifyRole: - return m_applicationList.at(index.row()).startupNotify; - case ApplicationRunningRole: - return m_applicationList.at(index.row()).window != nullptr; - case ApplicationUniqueIdRole: - return m_applicationList.at(index.row()).uniqueId; - case ApplicationLocationRole: - return m_applicationList.at(index.row()).location; - default: - return QVariant(); - } -} - -int ApplicationListModel::rowCount(const QModelIndex &parent) const -{ - if (parent.isValid()) { - return 0; - } - - return m_applicationList.count(); -} - -void ApplicationListModel::setMinimizedDelegate(int row, QQuickItem *delegate) -{ - if (row < 0 || row >= m_applicationList.count()) { - return; - } - - QWindow *delegateWindow = delegate->window(); - if (!delegateWindow) { - return; - } - - KWayland::Client::PlasmaWindow *window = m_applicationList[row].window; - if (!window) { - return; - } - - KWayland::Client::Surface *surface = KWayland::Client::Surface::fromWindow(delegateWindow); - if (!surface) { - return; - } - - QRect rect = delegate->mapRectToScene(QRectF(0, 0, delegate->width(), delegate->height())).toRect(); - - window->setMinimizedGeometry(surface, rect); -} - -void ApplicationListModel::unsetMinimizedDelegate(int row, QQuickItem *delegate) -{ - if (row < 0 || row >= m_applicationList.count()) { - return; - } - - QWindow *delegateWindow = delegate->window(); - if (!delegateWindow) { - return; - } - - KWayland::Client::PlasmaWindow *window = m_applicationList[row].window; - if (!window) { - return; - } - - KWayland::Client::Surface *surface = KWayland::Client::Surface::fromWindow(delegateWindow); - if (!surface) { - return; - } - - window->unsetMinimizedGeometry(surface); -} diff --git a/containments/homescreens/folio/plugin/applicationlistmodel.h b/containments/homescreens/folio/plugin/applicationlistmodel.h deleted file mode 100644 index 12965e23..00000000 --- a/containments/homescreens/folio/plugin/applicationlistmodel.h +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas -// SPDX-FileCopyrightText: 2022 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -/** - * @short The base application list, used directly by the app drawer. - * - * Items that are displayed on the desktop/pinned are done by DesktopModel, which is a subclass. - */ -class ApplicationListModel : public QAbstractListModel -{ - Q_OBJECT - -public: - // this enum is solely used by DesktopModel - enum LauncherLocation { None = 0, Favorites, Desktop }; - Q_ENUM(LauncherLocation) - - struct ApplicationData { - QString uniqueId; - QString name; - QString icon; - QString storageId; - QString entryPath; - bool startupNotify = true; - KWayland::Client::PlasmaWindow *window = nullptr; - LauncherLocation location = LauncherLocation::None; // only for DesktopModel - }; - - enum Roles { - ApplicationNameRole = Qt::UserRole + 1, - ApplicationIconRole, - ApplicationStorageIdRole, - ApplicationEntryPathRole, - ApplicationStartupNotifyRole, - ApplicationRunningRole, - ApplicationUniqueIdRole, - ApplicationLocationRole // only valid for DesktopModel - }; - - ApplicationListModel(QObject *parent = nullptr); - ~ApplicationListModel() override; - static ApplicationListModel *self(); - - int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; - QHash roleNames() const Q_DECL_OVERRIDE; - - Q_INVOKABLE void setMinimizedDelegate(int row, QQuickItem *delegate); - Q_INVOKABLE void unsetMinimizedDelegate(int row, QQuickItem *delegate); - -public Q_SLOTS: - void sycocaDbChanged(); - void windowCreated(KWayland::Client::PlasmaWindow *window); - -Q_SIGNALS: - void launchError(const QString &msg); - -protected: - virtual void load(); - - QList m_applicationList; - - KWayland::Client::PlasmaWindowManagement *m_windowManagement = nullptr; -}; diff --git a/containments/homescreens/folio/plugin/desktopmodel.cpp b/containments/homescreens/folio/plugin/desktopmodel.cpp deleted file mode 100644 index a1f1c0c4..00000000 --- a/containments/homescreens/folio/plugin/desktopmodel.cpp +++ /dev/null @@ -1,309 +0,0 @@ -// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas -// SPDX-FileCopyrightText: 2022 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -// Self -#include "desktopmodel.h" - -// Qt -#include -#include -#include - -// KDE -#include -#include - -#include - -const int MAX_FAVORITES = 5; - -DesktopModel::DesktopModel(QObject *parent, Plasma::Applet *applet) - : ApplicationListModel(parent) // constructor calls load() - , m_applet{applet} -{ -} - -DesktopModel::~DesktopModel() = default; - -QString DesktopModel::storageToUniqueId(const QString &storageId) const -{ - if (storageId.isEmpty()) { - return storageId; - } - - int id = 0; - QString uniqueId = storageId + QStringLiteral("-") + QString::number(id); - - while (m_appOrder.contains(uniqueId)) { - uniqueId = storageId + QStringLiteral("-") + QString::number(++id); - } - - return uniqueId; -} - -QString DesktopModel::uniqueToStorageId(const QString &uniqueId) const -{ - if (uniqueId.isEmpty()) { - return uniqueId; - } - - return uniqueId.split(QLatin1Char('-')).first(); -} - -void DesktopModel::loadSettings() -{ - if (!m_applet) { - return; - } - m_favorites = m_applet->config().readEntry("Favorites", QStringList()); - const auto di = m_applet->config().readEntry("DesktopItems", QStringList()); - m_desktopItems = QSet(di.begin(), di.end()); - m_appOrder = m_applet->config().readEntry("AppOrder", QStringList()); - - int i = 0; - for (const QString &app : std::as_const(m_appOrder)) { - m_appPositions[app] = i; - ++i; - } -} - -void DesktopModel::load() -{ - loadSettings(); - - // load applications - beginResetModel(); - - m_applicationList.clear(); - - QSet appsToRemove; - - for (const auto &uniqueId : m_appOrder) { - const QString storageId = uniqueToStorageId(uniqueId); - if (KService::Ptr service = KService::serviceByStorageId(storageId)) { - ApplicationData data; - data.name = service->name(); - data.icon = service->icon(); - data.storageId = service->storageId(); - data.uniqueId = uniqueId; - data.entryPath = service->exec(); - data.startupNotify = service->startupNotify().value_or(false); - - if (m_favorites.contains(uniqueId)) { - data.location = Favorites; - } else if (m_desktopItems.contains(uniqueId)) { - data.location = Desktop; - } - - m_applicationList << data; - } else { - appsToRemove.insert(uniqueId); - } - } - - bool favChanged = false; - - for (const auto &uniqueId : appsToRemove) { - m_appOrder.removeAll(uniqueId); - if (m_favorites.contains(uniqueId)) { - favChanged = true; - m_favorites.removeAll(uniqueId); - } - m_desktopItems.remove(uniqueId); - } - - endResetModel(); - - Q_EMIT countChanged(); - - if (m_applet) { - m_applet->config().writeEntry("Favorites", m_favorites); - m_applet->config().writeEntry("AppOrder", m_appOrder); - m_applet->config().writeEntry("DesktopItems", m_desktopItems.values()); - Q_EMIT m_applet->configNeedsSaving(); - } - - if (favChanged) { - Q_EMIT favoriteCountChanged(); - } -} - -int DesktopModel::count() -{ - return m_applicationList.count(); -} - -int DesktopModel::favoriteCount() -{ - return m_favorites.count(); -} - -int DesktopModel::maxFavoriteCount() -{ - return MAX_FAVORITES; -} - -void DesktopModel::setLocation(int row, LauncherLocation location) -{ - if (row < 0 || row >= m_applicationList.length()) { - return; - } - - ApplicationData data = m_applicationList.at(row); - if (data.location == location) { - return; - } - - if (location == Favorites) { - qWarning() << "favoriting" << row << data.name; - // Deny favorites when full - if (row >= maxFavoriteCount() || m_favorites.count() >= maxFavoriteCount() || m_favorites.contains(data.uniqueId)) { - return; - } - - m_favorites.insert(row, data.uniqueId); - - if (m_applet) { - m_applet->config().writeEntry("Favorites", m_favorites); - } - Q_EMIT favoriteCountChanged(); - - // Out of favorites - } else if (data.location == Favorites) { - m_favorites.removeAll(data.uniqueId); - if (m_applet) { - m_applet->config().writeEntry("Favorites", m_favorites); - } - Q_EMIT favoriteCountChanged(); - } - - // In Desktop - if (location == Desktop) { - m_desktopItems.insert(data.uniqueId); - if (m_applet) { - m_applet->config().writeEntry("DesktopItems", m_desktopItems.values()); - } - - // Out of Desktop - } else if (data.location == Desktop) { - m_desktopItems.remove(data.uniqueId); - if (m_applet) { - m_applet->config().writeEntry(QStringLiteral("DesktopItems"), m_desktopItems.values()); - } - } - - data.location = location; - if (m_applet) { - Q_EMIT m_applet->configNeedsSaving(); - } - Q_EMIT dataChanged(index(row, 0), index(row, 0)); -} - -void DesktopModel::moveItem(int row, int destination) -{ - if (row < 0 || destination < 0 || row >= m_applicationList.length() || destination >= m_applicationList.length() || row == destination) { - return; - } - if (destination > row) { - ++destination; - } - - beginMoveRows(QModelIndex(), row, row, QModelIndex(), destination); - if (destination > row) { - ApplicationData data = m_applicationList.at(row); - m_applicationList.insert(destination, data); - m_applicationList.takeAt(row); - - } else { - ApplicationData data = m_applicationList.takeAt(row); - m_applicationList.insert(destination, data); - } - - m_appOrder.clear(); - m_appPositions.clear(); - int i = 0; - for (const ApplicationData &app : std::as_const(m_applicationList)) { - m_appOrder << app.uniqueId; - m_appPositions[app.uniqueId] = i; - ++i; - } - - if (m_applet) { - m_applet->config().writeEntry("AppOrder", m_appOrder); - } - - endMoveRows(); -} - -void DesktopModel::addFavorite(const QString &storageId, int row, LauncherLocation location) -{ - if (row < 0 || row > m_applicationList.count()) { - return; - } - - if (KService::Ptr service = KService::serviceByStorageId(storageId)) { - const QString uniqueId = storageToUniqueId(service->storageId()); - ApplicationData data; - data.name = service->name(); - data.icon = service->icon(); - data.storageId = service->storageId(); - data.uniqueId = uniqueId; - data.entryPath = service->exec(); - data.startupNotify = service->startupNotify().value_or(false); - - bool favChanged = false; - if (location == Favorites) { - data.location = Favorites; - m_favorites.insert(qMin(row, m_favorites.count()), uniqueId); - favChanged = true; - } else { - data.location = location; - m_desktopItems.insert(data.uniqueId); - } - - beginInsertRows(QModelIndex(), row, row); - m_applicationList.insert(row, data); - m_appOrder.insert(row, uniqueId); - endInsertRows(); - if (favChanged) { - Q_EMIT favoriteCountChanged(); - } - - if (m_applet) { - m_applet->config().writeEntry("Favorites", m_favorites); - m_applet->config().writeEntry("AppOrder", m_appOrder); - m_applet->config().writeEntry("DesktopItems", m_desktopItems.values()); - Q_EMIT m_applet->configNeedsSaving(); - } - } -} - -void DesktopModel::removeFavorite(int row) -{ - if (row < 0 || row >= m_applicationList.count()) { - return; - } - - beginRemoveRows(QModelIndex(), row, row); - const QString uniqueId = m_applicationList[row].uniqueId; - m_appOrder.removeAll(uniqueId); - - const bool favChanged = m_favorites.contains(uniqueId); - m_favorites.removeAll(uniqueId); - m_desktopItems.remove(uniqueId); - m_appPositions.remove(uniqueId); - m_applicationList.removeAt(row); - endRemoveRows(); - - if (favChanged) { - Q_EMIT favoriteCountChanged(); - } - - if (m_applet) { - m_applet->config().writeEntry("Favorites", m_favorites); - m_applet->config().writeEntry("AppOrder", m_appOrder); - m_applet->config().writeEntry("DesktopItems", m_desktopItems.values()); - Q_EMIT m_applet->configNeedsSaving(); - } -} diff --git a/containments/homescreens/folio/plugin/desktopmodel.h b/containments/homescreens/folio/plugin/desktopmodel.h deleted file mode 100644 index 69d2c7d1..00000000 --- a/containments/homescreens/folio/plugin/desktopmodel.h +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Marco Martin -// SPDX-FileCopyrightText: 2022 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -// Qt -#include -#include -#include -#include - -// KDE -#include - -#include "applicationlistmodel.h" - -/** - * @short Filtered application list for applications on the desktop and pinned bar. - */ -class DesktopModel : public ApplicationListModel -{ - Q_OBJECT - Q_PROPERTY(int count READ count NOTIFY countChanged) - Q_PROPERTY(int favoriteCount READ favoriteCount NOTIFY favoriteCountChanged) - Q_PROPERTY(int maxFavoriteCount READ maxFavoriteCount CONSTANT) - -public: - DesktopModel(QObject *parent = nullptr, Plasma::Applet *applet = nullptr); - ~DesktopModel() override; - - QString storageToUniqueId(const QString &storageId) const; - QString uniqueToStorageId(const QString &uniqueId) const; - - void loadSettings(); - - int count(); - int favoriteCount(); - int maxFavoriteCount(); - - Q_INVOKABLE void setLocation(int row, LauncherLocation location); - Q_INVOKABLE void moveItem(int row, int destination); - - Q_INVOKABLE void addFavorite(const QString &storageId, int row, LauncherLocation location); - Q_INVOKABLE void removeFavorite(int row); - -Q_SIGNALS: - void countChanged(); - void favoriteCountChanged(); - -private: - void load() override; - - QStringList m_appOrder; - QStringList m_favorites; - QSet m_desktopItems; - QHash m_appPositions; - - Plasma::Applet *m_applet = nullptr; -}; diff --git a/containments/homescreens/folio/plugin/folioplugin.cpp b/containments/homescreens/folio/plugin/folioplugin.cpp deleted file mode 100644 index 5feb6361..00000000 --- a/containments/homescreens/folio/plugin/folioplugin.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "folioplugin.h" -#include "applicationlistmodel.h" -#include "desktopmodel.h" - -void HalcyonPlugin::registerTypes(const char *uri) -{ - Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.private.mobile.homescreen.folio")); - - qmlRegisterSingletonType(uri, 1, 0, "ApplicationListModel", [](QQmlEngine *, QJSEngine *) -> QObject * { - return ApplicationListModel::self(); - }); - - qmlRegisterType(uri, 1, 0, "DesktopModel"); -} diff --git a/containments/homescreens/folio/plugin/folioplugin.h b/containments/homescreens/folio/plugin/folioplugin.h deleted file mode 100644 index eae08dc1..00000000 --- a/containments/homescreens/folio/plugin/folioplugin.h +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -class HalcyonPlugin : public QQmlExtensionPlugin -{ - Q_OBJECT - Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") - -public: - void registerTypes(const char *uri) override; -}; diff --git a/containments/homescreens/folio/windowlistener.cpp b/containments/homescreens/folio/windowlistener.cpp new file mode 100644 index 00000000..318e6e64 --- /dev/null +++ b/containments/homescreens/folio/windowlistener.cpp @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "windowlistener.h" + +WindowListener::WindowListener(QObject *parent) + : QObject{parent} +{ + // initialize wayland window checking + KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(this); + if (!connection) { + return; + } + + auto *registry = new KWayland::Client::Registry(this); + registry->create(connection); + + connect(registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_windowManagement = registry->createPlasmaWindowManagement(name, version, this); + connect(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated, this, &WindowListener::windowCreated); + }); + + registry->setup(); + connection->roundtrip(); +} + +WindowListener *WindowListener::instance() +{ + static WindowListener *listener = new WindowListener(); + return listener; +} + +QList WindowListener::windowsFromStorageId(QString &storageId) const +{ + if (!m_windows.contains(storageId)) { + return {}; + } + return m_windows[storageId]; +} + +void WindowListener::windowCreated(KWayland::Client::PlasmaWindow *window) +{ + QString storageId = window->appId() + QStringLiteral(".desktop"); + + // ignore empty windows + if (storageId == ".desktop" || storageId == "org.kde.plasmashell.desktop") { + return; + } + + if (!m_windows.contains(storageId)) { + m_windows[storageId] = {}; + } + m_windows[storageId].push_back(window); + + // listen for window close + connect(window, &KWayland::Client::PlasmaWindow::unmapped, this, [this, storageId]() { + m_windows.remove(storageId); + Q_EMIT windowChanged(storageId); + }); + + Q_EMIT windowChanged(storageId); +} diff --git a/containments/homescreens/folio/windowlistener.h b/containments/homescreens/folio/windowlistener.h new file mode 100644 index 00000000..f773c0ec --- /dev/null +++ b/containments/homescreens/folio/windowlistener.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include +#include +#include +#include + +class WindowListener : public QObject +{ + Q_OBJECT + +public: + WindowListener(QObject *parent = nullptr); + + static WindowListener *instance(); + + QList windowsFromStorageId(QString &storageId) const; + +public Q_SLOTS: + void windowCreated(KWayland::Client::PlasmaWindow *window); + +Q_SIGNALS: + void windowChanged(QString storageId); + +private: + KWayland::Client::PlasmaWindowManagement *m_windowManagement{nullptr}; + QHash> m_windows; // +}; diff --git a/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml b/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml index 15b31490..0ddeea89 100644 --- a/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml +++ b/containments/homescreens/halcyon/package/contents/ui/HomeScreen.qml @@ -110,7 +110,7 @@ Item { anchors.fill: parent - property int horizontalMargin: Math.round(swipeView.width * 0.05) + property int horizontalMargin: Math.round(swipeView.width * 0.05) interactive: root.interactive leftMargin: horizontalMargin rightMargin: horizontalMargin diff --git a/lookandfeel/contents/lockscreen/FlickContainer.qml b/lookandfeel/contents/lockscreen/FlickContainer.qml index a844dc19..af827b09 100644 --- a/lookandfeel/contents/lockscreen/FlickContainer.qml +++ b/lookandfeel/contents/lockscreen/FlickContainer.qml @@ -70,7 +70,7 @@ MobileShell.SwipeArea { } onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => { - position = Math.max(0, Math.min(keypadHeight, position + deltaY)); + position = Math.max(0, Math.min(keypadHeight, position - deltaY)); } } diff --git a/lookandfeel/contents/plasmoidsetupscripts/org.kde.phone.homescreen.js b/lookandfeel/contents/plasmoidsetupscripts/org.kde.phone.homescreen.js deleted file mode 100644 index 92f857d4..00000000 --- a/lookandfeel/contents/plasmoidsetupscripts/org.kde.phone.homescreen.js +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2019-2020 Marco Martin -// SPDX-FileCopyrightText: 2021 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -applet.wallpaperPlugin = 'org.kde.image' -applet.writeConfig("AppOrder", ["org.kde.phone.dialer.desktop", "org.kde.spacebar.desktop", "org.kde.angelfish.desktop"]) -applet.writeConfig("Favorites", ["org.kde.phone.dialer.desktop", "org.kde.spacebar.desktop", "org.kde.angelfish.desktop"]) -applet.reloadConfig() - diff --git a/shell/contents/configuration/WallpaperSelector.qml b/shell/contents/configuration/WallpaperSelector.qml index e3c54905..ed0d67ca 100644 --- a/shell/contents/configuration/WallpaperSelector.qml +++ b/shell/contents/configuration/WallpaperSelector.qml @@ -46,9 +46,10 @@ Controls.Drawer { keyNavigationEnabled: true highlightFollowsCurrentItem: true snapMode: ListView.SnapToItem - model: imageWallpaper.wallpaperModel - onCountChanged: currentIndex = Math.min(model.indexOf(configDialog.wallpaperConfiguration["Image"]), model.rowCount()-1) + model: imageWallpaper.slidePaths + // onCountChanged: currentIndex = Math.min(model.indexOf(configDialog.wallpaperConfiguration["Image"]), model.rowCount()-1) headerPositioning: ListView.PullBackHeader + delegate: Controls.ItemDelegate { width: imageWallpaperDrawer.horizontal ? parent.width : height * (imageWallpaperDrawer.width / imageWallpaperDrawer.Screen.height) height: imageWallpaperDrawer.horizontal ? width / (imageWallpaperDrawer.Screen.width / imageWallpaperDrawer.Screen.height) : parent.height diff --git a/shell/contents/defaults b/shell/contents/defaults index 02563417..d328403f 100644 --- a/shell/contents/defaults +++ b/shell/contents/defaults @@ -7,7 +7,7 @@ LookAndFeelPackage=org.kde.breeze.mobile [Desktop] -Containment=org.kde.plasma.mobile.homescreen.halcyon +Containment=org.kde.plasma.mobile.homescreen.folio ToolBox=org.kde.plasma.nano.desktoptoolbox [Desktop][ContainmentActions]