folio: Rewrite

fix landscape favourites bar drag and drop, and cleanup folder

fix drawer scrolling

add settings
This commit is contained in:
Devin Lin 2023-10-21 20:59:27 -07:00
parent 0d6e17e247
commit 580afdfc9c
91 changed files with 8247 additions and 4052 deletions

View file

@ -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());
}

View file

@ -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)

View file

@ -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");

View file

@ -0,0 +1,221 @@
/*
* SPDX-FileCopyrightText: 2014 Aaron Seigo <aseigo@kde.org>
* SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
* SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
*
* 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()
}
}
}

View file

@ -202,7 +202,7 @@ Item {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
highlight: activeFocus ? highlightComponent : null
Component{
Component {
id: highlightComponent
PlasmaExtras.Highlight {}

View file

@ -67,6 +67,7 @@
<file>qml/statusbar/StatusBar.qml</file>
<file>qml/statusbar/TaskWidget.qml</file>
<file>qml/widgets/krunner/KRunnerScreen.qml</file>
<file>qml/widgets/krunner/KRunnerWidget.qml</file>
<file>qml/widgets/mediacontrols/BlurredBackground.qml</file>

View file

@ -1,8 +1,20 @@
# SPDX-FileCopyrightText: 2015-2021 Marco Martin <mart@kde.org>
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# 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)

View file

@ -0,0 +1,26 @@
<!--
- SPDX-FileCopyrightText: None
- SPDX-License-Identifier: CC0-1.0
-->
# 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

View file

@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas <antonis.tsiapaliokas@kde.org>
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "applicationlistmodel.h"
#include <QByteArray>
#include <QDebug>
#include <QModelIndex>
#include <QProcess>
#include <QQuickWindow>
#include <KApplicationTrader>
#include <KConfigGroup>
#include <KIO/ApplicationLauncherJob>
#include <KNotificationJobUiDelegate>
#include <KService>
#include <KSharedConfig>
#include <KSycoca>
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<int, QByteArray> 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<FolioDelegate *> 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();
}

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas <antonis.tsiapaliokas@kde.org>
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QList>
#include <QObject>
#include <QQuickItem>
#include <QSet>
#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<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
Q_INVOKABLE void load();
public Q_SLOTS:
void sycocaDbChanged();
Q_SIGNALS:
void launchError(const QString &msg);
protected:
QList<FolioDelegate *> m_delegates;
};

View file

@ -0,0 +1,302 @@
// SPDX-FileCopyrightText: 2016 The Qt Company Ltd.
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "delegatetoucharea.h"
#include <QCursor>
// 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<QPointerEvent *>(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<QMouseEvent *>(event) && !dynamic_cast<QTabletEvent *>(event) && !dynamic_cast<QTouchEvent *>(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();
}

View file

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QQuickItem>
#include <QTimer>
#include <Qt>
/**
* @short A component that is similar to MouseArea but allows for a
* simpler tracking of dragging movements after pressing and holding.
*
* @author Devin Lin <devin@kde.org>
*/
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)

View file

@ -0,0 +1,883 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "dragstate.h"
#include "favouritesmodel.h"
#include "pagelistmodel.h"
#include <KLocalizedString>
#include <algorithm>
// 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;
}

View file

@ -0,0 +1,156 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QTimer>
#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};
};

View file

@ -0,0 +1,403 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "favouritesmodel.h"
#include "homescreenstate.h"
#include <QByteArray>
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QModelIndex>
#include <QProcess>
#include <QQuickWindow>
#include <KApplicationTrader>
#include <KConfigGroup>
#include <KIO/ApplicationLauncherJob>
#include <KNotificationJobUiDelegate>
#include <KService>
#include <KSharedConfig>
#include <KSycoca>
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<int, QByteArray> 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);
}
}

View file

@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QAbstractListModel>
#include <QList>
#include <QObject>
#include <QQuickItem>
#include <QSet>
#include <Plasma/Applet>
#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<int, QByteArray> 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<FavouritesDelegate> m_delegates;
Plasma::Applet *m_applet{nullptr};
};

View file

@ -0,0 +1,139 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "folioapplication.h"
#include "windowlistener.h"
#include <QQuickWindow>
#include <KNotificationJobUiDelegate>
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);
}

View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QJsonObject>
#include <QObject>
#include <QQuickItem>
#include <QString>
#include <KIO/ApplicationLauncherJob>
#include <KService>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/plasmawindowmanagement.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/surface.h>
/**
* @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<KService>{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};
};

View file

@ -0,0 +1,468 @@
// SPDX-FileCopyrightText: 2022-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "folioapplicationfolder.h"
#include "homescreenstate.h"
#include <QJsonArray>
#include <algorithm>
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<FolioApplication *> 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<FolioApplication *> FolioApplicationFolder::appPreviews()
{
QList<FolioApplication *> previews;
// we give a maximum of 4 icons
for (int i = 0; i < std::min<int>(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<FolioApplication *> 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<int, QByteArray> 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;
}

View file

@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: 2022-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "folioapplication.h"
#include "foliodelegate.h"
#include <QAbstractListModel>
#include <QObject>
#include <QString>
#include <KService>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/plasmawindowmanagement.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/surface.h>
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<FolioApplication *> 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<FolioApplication *> appPreviews();
ApplicationFolderModel *applications();
void setApplications(QList<FolioApplication *> 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<ApplicationDelegate> 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<int, QByteArray> 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;
};

View file

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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;
}

View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#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};
};

View file

@ -0,0 +1,141 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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();
}

View file

@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <Plasma/Applet>
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};
};

View file

@ -1,12 +1,25 @@
// SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-FileCopyrightText: 2022-2023 Devin Lin <devin@kde.org>
// 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 <KWindowSystem>
#include <QDebug>
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
#include <QQuickItem>
#include <QtQml>
@ -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<ApplicationListModel>(uri, 1, 0, "ApplicationListModel", [](QQmlEngine *, QJSEngine *) -> QObject * {
return ApplicationListModel::self();
});
qmlRegisterSingletonType<FavouritesModel>(uri, 1, 0, "FavouritesModel", [](QQmlEngine *, QJSEngine *) -> QObject * {
return FavouritesModel::self();
});
qmlRegisterSingletonType<PageListModel>(uri, 1, 0, "PageListModel", [](QQmlEngine *, QJSEngine *) -> QObject * {
return PageListModel::self();
});
qmlRegisterSingletonType<FolioSettings>(uri, 1, 0, "FolioSettings", [](QQmlEngine *, QJSEngine *) -> QObject * {
return FolioSettings::self();
});
qmlRegisterSingletonType<HomeScreenState>(uri, 1, 0, "HomeScreenState", [](QQmlEngine *, QJSEngine *) -> QObject * {
return HomeScreenState::self();
});
qmlRegisterType<FolioApplication>(uri, 1, 0, "FolioApplication");
qmlRegisterType<FolioApplicationFolder>(uri, 1, 0, "FolioApplicationFolder");
qmlRegisterType<FolioDelegate>(uri, 1, 0, "FolioDelegate");
qmlRegisterType<PageModel>(uri, 1, 0, "PageModel");
qmlRegisterType<FolioPageDelegate>(uri, 1, 0, "FolioPageDelegate");
qmlRegisterType<DelegateTouchArea>(uri, 1, 0, "DelegateTouchArea");
qmlRegisterType<DelegateDragPosition>(uri, 1, 0, "DelegateDragPosition");
connect(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged, this, &HomeScreen::showingDesktopChanged);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,400 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "qqml.h"
#include <QObject>
#include <QPropertyAnimation>
#include <Plasma/Applet>
#include "dragstate.h"
class DragState;
/**
* @short The homescreen state, containing information on positioning panels as well as any swipe events.
*
* @author Devin Lin <devin@kde.org>
*/
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};
};

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- SPDX-FileCopyrightText: 2015-2019 Marco Martin <mart@kde.org>
- SPDX-License-Identifier: GPL-2.0-or-later
-->
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name=""/>
<group name="General">
<entry name="AppOrder" type="StringList">
<label>order of apps</label>
<default>org.kde.phone.dialer.desktop</default>
</entry>
</group>
</kcfg>

View file

@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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' }
}
}
}
}
}

View file

@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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)
}
}
}

View file

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// 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
}
}
}

View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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<Kirigami.Action> 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
}
}

View file

@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}
}

View file

@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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
}
}
}

View file

@ -0,0 +1,223 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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)
}
]
}
}
}
}
}
}

View file

@ -1,77 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* 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;
}
}

View file

@ -0,0 +1,297 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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 }
}
}
}

View file

@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}

View file

@ -1,233 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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 }
}
}
}

View file

@ -1,148 +1,407 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}
}

View file

@ -1,239 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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()
}
}
}

View file

@ -0,0 +1,242 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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)
}
]
}
}
}
}
}
}

View file

@ -1,161 +1,46 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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"
}
}
}

View file

@ -1,434 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* 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();
}
}
}

View file

@ -1,85 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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
}
}
}
}

View file

@ -1,298 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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;
}
}
}

View file

@ -1,128 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Marco Martin <mart@kde.org>
*
* 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)]);
}
}
}
}
}

View file

@ -1,126 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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);
}
}
}
}

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}
}

View file

@ -1,175 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
*
* 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
}
}
}
}
}
}

View file

@ -1,55 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* 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()
}
}
}

View file

@ -1,80 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* 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
}
}
}

View file

@ -1,166 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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"
}
}
}

View file

@ -1,108 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
*
* 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"
}
}
}

View file

@ -1,102 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
*
* 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)
}
}
}
}

View file

@ -1,87 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
*
* 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)
}
}
}
}

View file

@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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"
}
}
}
}

View file

@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}
}
}

View file

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}

View file

@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}
}
}

View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}
}
}

View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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
}

View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Effects
MultiEffect {
shadowEnabled: true
shadowVerticalOffset: 1
blurMax: 16
shadowOpacity: 0.5
}

View file

@ -1,9 +1,5 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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 {
}
}
}

View file

@ -1,138 +0,0 @@
/*
* SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
*
* 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();
}
}
}

View file

@ -1,132 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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
}
}
}
}
}
}

View file

@ -1,85 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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;
}
}
}
}
}

View file

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Marco Martin <mart@kde.org>
*
* 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) }
}
}
}

View file

@ -1,70 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* 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
}
}
}

View file

@ -1,42 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
*
* 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
}
}
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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()
}
}
}

View file

@ -0,0 +1,208 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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'
}
}
}
}
}

View file

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "pagelistmodel.h"
#include "homescreenstate.h"
#include <QJsonArray>
#include <QJsonDocument>
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<int, QByteArray> 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;
}

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "pagemodel.h"
#include <QAbstractListModel>
#include <QList>
#include <Plasma/Applet>
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<int, QByteArray> 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<PageModel *> m_pages;
Plasma::Applet *m_applet{nullptr};
};

View file

@ -0,0 +1,291 @@
// SPDX-FileCopyrightText: 2022-2023 Devin Lin <devin@kde.org>
// 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<FolioPageDelegate *> delegates, QObject *parent)
: QAbstractListModel{parent}
, m_delegates{delegates}
{
}
PageModel::~PageModel() = default;
PageModel *PageModel::fromJson(QJsonArray &arr, QObject *parent)
{
QList<FolioPageDelegate *> delegates;
QList<FolioPageDelegate *> 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<int, QByteArray> 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();
}

View file

@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2022-2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "folioapplication.h"
#include "folioapplicationfolder.h"
#include "foliodelegate.h"
#include <QAbstractListModel>
#include <QJsonArray>
#include <QList>
#include <Plasma/Applet>
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<FolioPageDelegate *> delegates = QList<FolioPageDelegate *>{}, 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<int, QByteArray> 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<FolioPageDelegate *> m_delegates;
};

View file

@ -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)

View file

@ -1,236 +0,0 @@
// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas <antonis.tsiapaliokas@kde.org>
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "applicationlistmodel.h"
#include <QByteArray>
#include <QDebug>
#include <QModelIndex>
#include <QProcess>
#include <QQuickWindow>
#include <KApplicationTrader>
#include <KConfigGroup>
#include <KIO/ApplicationLauncherJob>
#include <KNotificationJobUiDelegate>
#include <KService>
#include <KSharedConfig>
#include <KSycoca>
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<int>>("QVector<int>");
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<int, QByteArray> 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<ApplicationData> 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);
}

View file

@ -1,78 +0,0 @@
// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas <antonis.tsiapaliokas@kde.org>
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QList>
#include <QObject>
#include <QQuickItem>
#include <QSet>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/plasmawindowmanagement.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/surface.h>
/**
* @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<int, QByteArray> 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<ApplicationData> m_applicationList;
KWayland::Client::PlasmaWindowManagement *m_windowManagement = nullptr;
};

View file

@ -1,309 +0,0 @@
// SPDX-FileCopyrightText: 2014 Antonis Tsiapaliokas <antonis.tsiapaliokas@kde.org>
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
// Self
#include "desktopmodel.h"
// Qt
#include <QByteArray>
#include <QDebug>
#include <QModelIndex>
// KDE
#include <KService>
#include <KSharedConfig>
#include <PlasmaQuick/AppletQuickItem>
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<QString>(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<QString> 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();
}
}

View file

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2021 Marco Martin <mart@kde.org>
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// Qt
#include <QAbstractListModel>
#include <QList>
#include <QObject>
#include <QSet>
// KDE
#include <Plasma/Applet>
#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<QString> m_desktopItems;
QHash<QString, int> m_appPositions;
Plasma::Applet *m_applet = nullptr;
};

View file

@ -1,17 +0,0 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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<ApplicationListModel>(uri, 1, 0, "ApplicationListModel", [](QQmlEngine *, QJSEngine *) -> QObject * {
return ApplicationListModel::self();
});
qmlRegisterType<DesktopModel>(uri, 1, 0, "DesktopModel");
}

View file

@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
class HalcyonPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
public:
void registerTypes(const char *uri) override;
};

View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// 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<KWayland::Client::PlasmaWindow *> 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);
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QList>
#include <QObject>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/plasmawindowmanagement.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/surface.h>
class WindowListener : public QObject
{
Q_OBJECT
public:
WindowListener(QObject *parent = nullptr);
static WindowListener *instance();
QList<KWayland::Client::PlasmaWindow *> 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<QString, QList<KWayland::Client::PlasmaWindow *>> m_windows; // <storageId, window>
};

View file

@ -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

View file

@ -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));
}
}

View file

@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: 2019-2020 Marco Martin <mart@kde.org>
// SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
// 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()

View file

@ -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

View file

@ -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]