homescreens/halcyon: Add initial folders implementation

This commit is contained in:
Devin Lin 2022-06-30 00:40:22 -04:00
parent 1629733d7e
commit 564c3f9e81
7 changed files with 516 additions and 78 deletions

View file

@ -54,6 +54,16 @@ void ApplicationFolder::setName(QString &name)
Q_EMIT saveRequested(); Q_EMIT saveRequested();
} }
QList<Application *> ApplicationFolder::appPreviews()
{
QList<Application *> previews;
// we give a maximum of 4 icons
for (int i = 0; i < std::min(m_applications.length(), 4); ++i) {
previews.push_back(m_applications[i]);
}
return previews;
}
QList<Application *> ApplicationFolder::applications() QList<Application *> ApplicationFolder::applications()
{ {
return m_applications; return m_applications;
@ -66,6 +76,28 @@ void ApplicationFolder::setApplications(QList<Application *> applications)
Q_EMIT saveRequested(); Q_EMIT saveRequested();
} }
void ApplicationFolder::moveEntry(int fromRow, int toRow)
{
if (fromRow < 0 || toRow < 0 || fromRow >= m_applications.length() || toRow >= m_applications.length() || fromRow == toRow) {
return;
}
if (toRow > fromRow) {
++toRow;
}
if (toRow > fromRow) {
Application *app = m_applications.at(fromRow);
m_applications.insert(toRow, app);
m_applications.takeAt(fromRow);
} else {
Application *app = m_applications.takeAt(fromRow);
m_applications.insert(toRow, app);
}
Q_EMIT applicationsChanged();
Q_EMIT saveRequested();
}
void ApplicationFolder::addApp(const QString &storageId, int row) void ApplicationFolder::addApp(const QString &storageId, int row)
{ {
if (row < 0 || row > m_applications.size()) { if (row < 0 || row > m_applications.size()) {

View file

@ -22,6 +22,7 @@ class ApplicationFolder : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QList<Application *> appPreviews READ appPreviews NOTIFY applicationsChanged)
Q_PROPERTY(QList<Application *> applications READ applications NOTIFY applicationsChanged) Q_PROPERTY(QList<Application *> applications READ applications NOTIFY applicationsChanged)
public: public:
@ -33,9 +34,12 @@ public:
QString name() const; QString name() const;
void setName(QString &name); void setName(QString &name);
QList<Application *> appPreviews();
QList<Application *> applications(); QList<Application *> applications();
void setApplications(QList<Application *> applications); void setApplications(QList<Application *> applications);
Q_INVOKABLE void moveEntry(int fromRow, int toRow);
Q_INVOKABLE void addApp(const QString &storageId, int row); Q_INVOKABLE void addApp(const QString &storageId, int row);
Q_INVOKABLE void removeApp(int row); Q_INVOKABLE void removeApp(int row);

View file

@ -19,58 +19,61 @@ import org.kde.kirigami 2.19 as Kirigami
MouseArea { MouseArea {
id: delegate id: delegate
property int visualIndex: 0 property int visualIndex: 0
property real leftPadding property real leftPadding
property real rightPadding property real rightPadding
property alias iconItem: icon // whether this delegate is a folder
property var application property bool isFolder
// folder object
property var folder
readonly property string folderName: folder ? folder.name : ""
// app object
property var application
readonly property string applicationName: application ? application.name : "" readonly property string applicationName: application ? application.name : ""
readonly property string applicationStorageId: application ? application.storageId : "" readonly property string applicationStorageId: application ? application.storageId : ""
readonly property string applicationIcon: application ? application.icon : "" readonly property string applicationIcon: application ? application.icon : ""
signal launch(int x, int y, var source, string title, string storageId) signal folderOpenRequested()
signal dragStarted(string imageSource, int x, int y, string mimeData)
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
onLaunch: {
if (icon !== "") {
MobileShell.HomeScreenControls.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));
}
application.setMinimizedDelegate(delegate);
application.runApplication();
}
function openContextMenu() { function openContextMenu() {
dialogLoader.active = true; dialogLoader.active = true;
dialogLoader.item.open(); dialogLoader.item.open();
} }
function launchApp() { function launch() {
if (application.running) { if (isFolder) {
delegate.launch(0, 0, "", applicationName, applicationStorageId); folderOpenRequested();
} else { } else {
delegate.launch(delegate.x + (PlasmaCore.Units.smallSpacing * 2), delegate.y + (PlasmaCore.Units.smallSpacing * 2), icon.source, applicationName, applicationStorageId); if (application.running) {
launchAppWithAnim(0, 0, "", applicationName, applicationStorageId);
} else {
launchAppWithAnim(delegate.x + (PlasmaCore.Units.smallSpacing * 2), delegate.y + (PlasmaCore.Units.smallSpacing * 2), iconLoader.source, applicationName, applicationStorageId);
} }
} }
}
function launchAppWithAnim(x: int, y: int, source, title: string, storageId: string) {
if (source !== "") {
MobileShell.HomeScreenControls.openAppLaunchAnimation(
source,
title,
iconLoader.Kirigami.ScenePosition.x + iconLoader.width/2,
iconLoader.Kirigami.ScenePosition.y + iconLoader.height/2,
Math.min(iconLoader.width, iconLoader.height));
}
application.setMinimizedDelegate(delegate);
application.runApplication();
}
property bool inDrag: false property bool inDrag: false
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse.button === Qt.RightButton) ? openContextMenu() : launchApp(); onClicked: (mouse.button === Qt.RightButton) ? openContextMenu() : launch();
onReleased: { onReleased: {
parent.Drag.drop(); parent.Drag.drop();
inDrag = false; inDrag = false;
@ -131,17 +134,72 @@ MouseArea {
} }
spacing: 0 spacing: 0
PlasmaCore.IconItem { Loader {
id: icon id: iconLoader
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.minimumWidth: Layout.minimumHeight Layout.minimumWidth: Layout.minimumHeight
Layout.preferredWidth: Layout.minimumHeight Layout.preferredWidth: Layout.minimumHeight
Layout.minimumHeight: parent.height Layout.minimumHeight: parent.height
Layout.preferredHeight: Layout.minimumHeight Layout.preferredHeight: Layout.minimumHeight
sourceComponent: delegate.isFolder ? folderIconComponent : appIconComponent
}
PlasmaComponents.Label {
id: label
visible: text.length > 0
Layout.fillWidth: true
Layout.leftMargin: PlasmaCore.Units.smallSpacing * 2
Layout.rightMargin: PlasmaCore.Units.largeSpacing
wrapMode: Text.WordWrap
maximumLineCount: 1
elide: Text.ElideRight
text: delegate.isFolder ? delegate.folderName : delegate.applicationName
font.pointSize: PlasmaCore.Theme.defaultFont.pointSize
font.weight: Font.Bold
color: "white"
layer.enabled: true
layer.effect: DropShadow {
verticalOffset: 1
radius: 4
samples: 6
color: Qt.rgba(0, 0, 0, 0.5)
}
}
Kirigami.Icon {
Layout.alignment: Qt.AlignRight
Layout.minimumWidth: Layout.minimumHeight
Layout.preferredWidth: Layout.minimumHeight
Layout.minimumHeight: Math.round(parent.height * 0.5)
Layout.preferredHeight: Layout.minimumHeight
isMask: true
color: 'white'
source: 'arrow-right'
visible: delegate.isFolder
layer.enabled: true
layer.effect: DropShadow {
verticalOffset: 1
radius: 4
samples: 6
color: Qt.rgba(0, 0, 0, 0.5)
}
}
}
}
Component {
id: appIconComponent
PlasmaCore.IconItem {
usesPlasmaTheme: false usesPlasmaTheme: false
source: delegate.applicationIcon source: delegate.isFolder ? 'document-open-folder' : delegate.applicationIcon
Rectangle { Rectangle {
anchors { anchors {
@ -163,34 +221,47 @@ MouseArea {
color: Qt.rgba(0, 0, 0, 0.5) color: Qt.rgba(0, 0, 0, 0.5)
} }
} }
}
PlasmaComponents.Label { Component {
id: label id: folderIconComponent
visible: text.length > 0
Layout.fillWidth: true Item {
Layout.leftMargin: PlasmaCore.Units.smallSpacing * 2 Rectangle {
Layout.rightMargin: PlasmaCore.Units.largeSpacing anchors.fill: parent
wrapMode: Text.WordWrap anchors.margins: PlasmaCore.Units.smallSpacing
maximumLineCount: 1 color: Qt.rgba(255, 255, 255, 0.2)
elide: Text.ElideRight radius: PlasmaCore.Units.smallSpacing
text: delegate.applicationName Grid {
id: grid
anchors.fill: parent
anchors.margins: PlasmaCore.Units.smallSpacing
columns: 2
spacing: PlasmaCore.Units.smallSpacing
font.pointSize: PlasmaCore.Theme.defaultFont.pointSize property var previews: model.folder.appPreviews
font.weight: Font.Bold
color: "white" Repeater {
model: grid.previews
delegate: Kirigami.Icon {
implicitWidth: (grid.width - PlasmaCore.Units.smallSpacing) / 2
implicitHeight: (grid.width - PlasmaCore.Units.smallSpacing) / 2
source: modelData.icon
layer.enabled: true layer.enabled: true
layer.effect: DropShadow { layer.effect: DropShadow {
verticalOffset: 1 verticalOffset: 1
radius: 4 radius: 4
samples: 6 samples: 3
color: Qt.rgba(0, 0, 0, 0.5) color: Qt.rgba(0, 0, 0, 0.5)
} }
} }
} }
} }
}
}
}
} }

View file

@ -18,20 +18,15 @@ import org.kde.phone.homescreen.halcyon 1.0 as Halcyon
GridView { GridView {
id: root id: root
required property var searchWidget required property var searchWidget
signal openConfigureRequested() signal openConfigureRequested()
signal requestOpenFolder(Halcyon.ApplicationFolder folder)
readonly property real twoColumnThreshold: PlasmaCore.Units.gridUnit * 10
readonly property bool twoColumn: root.width / 2 > twoColumnThreshold
cellWidth: twoColumn ? root.width / 2 : root.width
cellHeight: delegateHeight
// don't set anchors.margins since we want everywhere to be draggable // don't set anchors.margins since we want everywhere to be draggable
readonly property real leftMargin: Math.round(parent.width * 0.1) required property real leftMargin
readonly property real rightMargin: Math.round(parent.width * 0.1) required property real rightMargin
readonly property real delegateHeight: PlasmaCore.Units.gridUnit * 3 required property bool twoColumn
// search widget open gesture // search widget open gesture
property bool openingSearchWidget: false property bool openingSearchWidget: false
@ -63,7 +58,7 @@ GridView {
} }
header: MobileShell.BaseItem { header: MobileShell.BaseItem {
topPadding: Math.round(swipeView.height * 0.2) topPadding: Math.round(root.height * 0.2)
bottomPadding: PlasmaCore.Units.largeSpacing bottomPadding: PlasmaCore.Units.largeSpacing
leftPadding: root.leftMargin leftPadding: root.leftMargin
rightPadding: root.rightMargin rightPadding: root.rightMargin
@ -82,8 +77,6 @@ GridView {
delegate: DropArea { delegate: DropArea {
id: delegateRoot id: delegateRoot
property var application: model.application
property int modelIndex property int modelIndex
property int visualIndex: DelegateModel.itemsIndex property int visualIndex: DelegateModel.itemsIndex
@ -106,12 +99,12 @@ GridView {
FavoritesAppDelegate { FavoritesAppDelegate {
id: appDelegate id: appDelegate
visualIndex: delegateRoot.visualIndex visualIndex: delegateRoot.visualIndex
application: delegateRoot.application
anchors { isFolder: model.isFolder
horizontalCenter: parent.horizontalCenter folder: model.folder
verticalCenter: parent.verticalCenter application: model.application
}
onFolderOpenRequested: root.requestOpenFolder(model.folder)
readonly property bool isLeftColumn: !root.twoColumn || ((visualIndex % 2) === 0) readonly property bool isLeftColumn: !root.twoColumn || ((visualIndex % 2) === 0)
readonly property bool isRightColumn: !root.twoColumn || ((visualIndex % 2) !== 0) readonly property bool isRightColumn: !root.twoColumn || ((visualIndex % 2) !== 0)
@ -121,6 +114,9 @@ GridView {
implicitWidth: root.cellWidth implicitWidth: root.cellWidth
implicitHeight: visible ? root.cellHeight : 0 implicitHeight: visible ? root.cellHeight : 0
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
states: [ states: [
State { State {
when: appDelegate.drag.active when: appDelegate.drag.active

View file

@ -0,0 +1,170 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.1
import QtQml.Models 2.15
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.components 3.0 as PC3
import org.kde.draganddrop 2.0 as DragDrop
import org.kde.kirigami 2.19 as Kirigami
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
import org.kde.phone.homescreen.halcyon 1.0 as Halcyon
Item {
id: root
required property bool interactive
required property var searchWidget
readonly property real twoColumnThreshold: PlasmaCore.Units.gridUnit * 10
readonly property bool twoColumn: root.width / 2 > twoColumnThreshold
readonly property real cellWidth: twoColumn ? root.width / 2 : root.width
readonly property real cellHeight: delegateHeight
readonly property real leftMargin: Math.round(parent.width * 0.1)
readonly property real rightMargin: Math.round(parent.width * 0.1)
readonly property real delegateHeight: PlasmaCore.Units.gridUnit * 3
property bool folderShown: false
onFolderShownChanged: folderShown ? openFolderAnim.restart() : closeFolderAnim.restart()
signal openConfigureRequested()
FavoritesGrid {
id: favoritesGrid
anchors.fill: parent
interactive: root.interactive
searchWidget: root.searchWidget
cellWidth: root.cellWidth
cellHeight: root.cellHeight
leftMargin: root.leftMargin
rightMargin: root.rightMargin
twoColumn: root.twoColumn
onOpenConfigureRequested: root.openConfigureRequested()
onRequestOpenFolder: (folder) => {
folderGrid.folder = folder;
root.folderShown = true;
}
property real translateX: 0
transform: Translate { x: favoritesGrid.translateX }
visible: opacity !== 0
}
FolderGrid {
id: folderGrid
anchors.fill: parent
folder: null
interactive: root.interactive
cellWidth: root.cellWidth
cellHeight: root.cellHeight
leftMargin: root.leftMargin
rightMargin: root.rightMargin
twoColumn: root.twoColumn
onOpenConfigureRequested: root.openConfigureRequested()
onCloseRequested: root.folderShown = false
property real translateX: 0
transform: Translate { x: folderGrid.translateX }
opacity: 0
visible: opacity !== 0
}
SequentialAnimation {
id: openFolderAnim
ParallelAnimation {
NumberAnimation {
target: favoritesGrid
properties: 'translateX'
duration: 200
from: 0
to: -PlasmaCore.Units.gridUnit
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: favoritesGrid
properties: 'opacity'
duration: 200
from: 1
to: 0
easing.type: Easing.InOutQuad
}
}
ParallelAnimation {
NumberAnimation {
target: folderGrid
properties: 'translateX'
duration: 200
from: PlasmaCore.Units.gridUnit
to: 0
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: folderGrid
properties: 'opacity'
duration: 200
from: 0
to: 1
easing.type: Easing.InOutQuad
}
}
}
SequentialAnimation {
id: closeFolderAnim
ParallelAnimation {
NumberAnimation {
target: folderGrid
properties: 'translateX'
duration: 200
from: 0
to: PlasmaCore.Units.gridUnit
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: folderGrid
properties: 'opacity'
duration: 200
from: 1
to: 0
easing.type: Easing.InOutQuad
}
}
ParallelAnimation {
NumberAnimation {
target: favoritesGrid
properties: 'translateX'
duration: 200
from: -PlasmaCore.Units.gridUnit
to: 0
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: favoritesGrid
properties: 'opacity'
duration: 200
from: 0
to: 1
easing.type: Easing.InOutQuad
}
}
}
}

View file

@ -0,0 +1,168 @@
// SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.1
import QtQml.Models 2.15
import QtGraphicalEffects 1.12
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.components 3.0 as PC3
import org.kde.draganddrop 2.0 as DragDrop
import org.kde.kirigami 2.19 as Kirigami
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
import org.kde.phone.homescreen.halcyon 1.0 as Halcyon
GridView {
id: root
property Halcyon.ApplicationFolder folder: null
property string folderName: folder ? folder.name : ""
property var folderModel: folder ? folder.applications : []
// don't set anchors.margins since we want everywhere to be draggable
required property real leftMargin
required property real rightMargin
required property bool twoColumn
signal openConfigureRequested()
signal closeRequested()
TapHandler {
onLongPressed: root.openConfigureRequested()
onTapped: root.closeRequested()
}
header: MobileShell.BaseItem {
topPadding: Math.round(root.height * 0.2)
bottomPadding: PlasmaCore.Units.largeSpacing
leftPadding: root.leftMargin
rightPadding: root.rightMargin
implicitWidth: root.width
background: Rectangle {
color: 'transparent'
TapHandler {
onLongPressed: root.openConfigureRequested()
onTapped: root.closeRequested()
}
}
contentItem: RowLayout {
spacing: PlasmaCore.Units.gridUnit
Kirigami.Icon {
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: PlasmaCore.Units.iconSizes.small
Layout.preferredWidth: PlasmaCore.Units.iconSizes.small
isMask: true
color: 'white'
source: 'arrow-left'
layer.enabled: true
layer.effect: DropShadow {
verticalOffset: 1
radius: 4
samples: 6
color: Qt.rgba(0, 0, 0, 0.5)
}
}
QQC2.Label {
Layout.fillWidth: true
text: root.folderName
color: "white"
style: Text.Normal
styleColor: "transparent"
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
font.weight: Font.Bold
font.pointSize: 18
layer.enabled: true
layer.effect: DropShadow {
verticalOffset: 1
radius: 4
samples: 6
color: Qt.rgba(0, 0, 0, 0.5)
}
}
}
}
model: DelegateModel {
id: visualModel
model: root.folderModel
delegate: DropArea {
id: delegateRoot
property var application: model.application
property int modelIndex
property int visualIndex: DelegateModel.itemsIndex
width: root.cellWidth
height: root.cellHeight
onEntered: (drag) => {
let from = (drag.source as MobileShell.BaseItem).visualIndex;
let to = appDelegate.visualIndex;
visualModel.items.move(from, to);
root.folder.moveEntry(from, to);
}
//onDropped: (drag) => {
//let from = modelIndex;
//let to = (drag.source as MobileShell.BaseItem).visualIndex
//Halcyon.PinnedModel.moveEntry(from, to);
//}
FavoritesAppDelegate {
id: appDelegate
visualIndex: delegateRoot.visualIndex
isFolder: false
application: modelData
readonly property bool isLeftColumn: !root.twoColumn || ((visualIndex % 2) === 0)
readonly property bool isRightColumn: !root.twoColumn || ((visualIndex % 2) !== 0)
leftPadding: isLeftColumn ? root.leftMargin : 0
rightPadding: isRightColumn ? root.rightMargin : 0
implicitWidth: root.cellWidth
implicitHeight: visible ? root.cellHeight : 0
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
states: [
State {
when: appDelegate.drag.active
ParentChange {
target: appDelegate
parent: root
}
AnchorChanges {
target: appDelegate
anchors.horizontalCenter: undefined
anchors.verticalCenter: undefined
}
}
]
}
}
}
// animations
displaced: Transition {
NumberAnimation {
properties: "x,y"
easing.type: Easing.OutQuad
}
}
}

View file

@ -25,11 +25,9 @@ Item {
function triggerHomescreen() { function triggerHomescreen() {
swipeView.setCurrentIndex(0); swipeView.setCurrentIndex(0);
favouritesList.contentY = favouritesList.originY;
} }
function openConfigure() { function openConfigure() {
console.log('triggered')
plasmoid.action("configure").trigger(); plasmoid.action("configure").trigger();
plasmoid.editMode = false; plasmoid.editMode = false;
} }
@ -54,11 +52,10 @@ Item {
onLongPressed: root.openConfigure() onLongPressed: root.openConfigure()
} }
FavoritesGrid { FavoritesView {
anchors.fill: parent anchors.fill: parent
searchWidget: root.searchWidget searchWidget: root.searchWidget
interactive: root.interactive interactive: root.interactive
onOpenConfigureRequested: root.openConfigure() onOpenConfigureRequested: root.openConfigure()
} }
} }