shift-shell/containments/homescreens/folio/folioapplicationfolder.cpp
Devin Lin 3550caa580 folio: Use smart pointers to manage delegate lifetimes
Currently delegates do not get deleted when removed from models. Due to
the complexity of drag state and other objects, we can't simply delete
the delegate when removed from the model because they might still be
involved in animations or other state at the time of removal. Use smart
pointers to have the delegate objects deleted instead.

QML unfortunately doesn't support QSharedPointer, so we need to expose raw pointers to it.
2025-02-21 18:06:24 +00:00

484 lines
15 KiB
C++

// 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(HomeScreen *parent, QString name)
: QObject{parent}
, m_homeScreen{parent}
, m_name{name}
, m_applicationFolderModel{new ApplicationFolderModel{this}}
{
}
FolioApplicationFolder::Ptr FolioApplicationFolder::fromJson(QJsonObject &obj, HomeScreen *parent)
{
QString name = obj[QStringLiteral("name")].toString();
QList<FolioApplication::Ptr> apps;
for (auto storageId : obj[QStringLiteral("apps")].toArray()) {
if (KService::Ptr service = KService::serviceByStorageId(storageId.toString())) {
apps.append(std::make_shared<FolioApplication>(parent, service));
}
}
FolioApplicationFolder::Ptr folder = std::make_shared<FolioApplicationFolder>(parent, name);
folder->setApplications(apps);
return folder;
}
QJsonObject FolioApplicationFolder::toJson() const
{
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().get());
}
return previews;
}
ApplicationFolderModel *FolioApplicationFolder::applications()
{
return m_applicationFolderModel;
}
void FolioApplicationFolder::setApplications(QList<FolioApplication::Ptr> applications)
{
if (m_applicationFolderModel) {
m_applicationFolderModel->deleteLater();
}
m_delegates.clear();
for (FolioApplication::Ptr app : applications) {
m_delegates.append({FolioDelegate::Ptr{new FolioDelegate{app, m_homeScreen}}, 0, 0, 0});
}
m_applicationFolderModel = new ApplicationFolderModel{this};
m_applicationFolderModel->evaluateDelegateIndexes();
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::Ptr 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 *parent)
: QAbstractListModel{parent}
, m_folder{parent}
{
HomeScreenState *homeScreenState = m_folder->m_homeScreen->homeScreenState();
connect(homeScreenState, &HomeScreenState::folderPageWidthChanged, this, [this]() {
evaluateDelegateIndexes();
});
connect(homeScreenState, &HomeScreenState::folderPageHeightChanged, this, [this]() {
evaluateDelegateIndexes();
});
connect(homeScreenState, &HomeScreenState::folderPageContentWidthChanged, this, [this]() {
evaluateDelegateIndexes();
});
connect(homeScreenState, &HomeScreenState::folderPageContentHeightChanged, this, [this]() {
evaluateDelegateIndexes();
});
connect(homeScreenState, &HomeScreenState::viewWidthChanged, this, [this]() {
evaluateDelegateIndexes();
});
connect(homeScreenState, &HomeScreenState::viewHeightChanged, this, [this]() {
evaluateDelegateIndexes();
});
connect(homeScreenState, &HomeScreenState::pageCellWidthChanged, this, [this]() {
evaluateDelegateIndexes();
});
connect(homeScreenState, &HomeScreenState::pageCellHeightChanged, this, [this]() {
evaluateDelegateIndexes();
});
}
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.get());
case columnIndexRole:
return QVariant::fromValue(m_folder->m_delegates.at(index.row()).columnIndex);
case rowIndexRole:
return QVariant::fromValue(m_folder->m_delegates.at(index.row()).rowIndex);
case pageIndexRole:
return QVariant::fromValue(m_folder->m_delegates.at(index.row()).pageIndex);
}
return QVariant();
}
QHash<int, QByteArray> ApplicationFolderModel::roleNames() const
{
return {{DelegateRole, "delegate"}, {columnIndexRole, "columnIndex"}, {rowIndexRole, "rowIndex"}, {pageIndexRole, "pageIndex"}};
}
FolioDelegate::Ptr 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();
evaluateDelegateIndexes();
Q_EMIT m_folder->applicationsChanged();
Q_EMIT m_folder->saveRequested();
}
bool ApplicationFolderModel::canAddDelegate(FolioDelegate::Ptr delegate, int index)
{
if (index < 0 || index > m_folder->m_delegates.size()) {
return false;
}
if (!delegate) {
return false;
}
return true;
}
bool ApplicationFolderModel::addDelegate(FolioDelegate::Ptr delegate, int index)
{
if (!canAddDelegate(delegate, index)) {
return false;
}
if (index == m_folder->m_delegates.size()) {
beginInsertRows(QModelIndex(), index, index);
m_folder->m_delegates.append({delegate, 0, 0, 0});
evaluateDelegateIndexes(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, 0});
evaluateDelegateIndexes(false);
endInsertRows();
}
evaluateDelegateIndexes();
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();
evaluateDelegateIndexes();
Q_EMIT m_folder->applicationsChanged();
Q_EMIT m_folder->saveRequested();
}
QPointF ApplicationFolderModel::getDelegatePosition(int index)
{
if (index < 0 || index >= m_folder->m_delegates.size()) {
return {0, 0};
}
auto delegate = m_folder->m_delegates[index];
qreal pageContentSize = m_folder->m_homeScreen->homeScreenState()->folderPageContentWidth();
qreal topMargin = verticalPageMargin();
qreal leftMargin = horizontalPageMargin();
int cellSize = pageContentSize / numGridLengthOnPage();
qreal cellWitdhRecenter = (cellSize - m_folder->m_homeScreen->homeScreenState()->pageCellWidth()) / 2;
qreal cellHeightRecenter = (cellSize - m_folder->m_homeScreen->homeScreenState()->pageCellHeight()) / 2;
qreal xPosition = cellWitdhRecenter + leftMargin + delegate.columnIndex * cellSize;
qreal yPosition = cellHeightRecenter + topMargin + delegate.rowIndex * cellSize;
return {xPosition, 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::Ptr 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 = std::make_shared<FolioDelegate>(m_folder->m_homeScreen);
}
// add empty delegate at new position
addDelegate(ghost, index);
}
void ApplicationFolderModel::replaceGhostEntry(FolioDelegate::Ptr 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 pageContentWidth = m_folder->m_homeScreen->homeScreenState()->folderPageContentWidth();
qreal cellSize = (pageContentWidth) / numGridLengthOnPage();
int row = (y - topMarginFromScreenEdge()) / cellSize;
row = std::max(0, std::min(numGridLengthOnPage(), row));
// the index that the position is over
int leftColumn = std::max(0.0, x - leftMarginFromScreenEdge()) / cellSize;
leftColumn = std::min(numGridLengthOnPage() - 1, leftColumn);
qreal leftColumnPosition = leftColumn * cellSize + 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 + cellSize * 0.5) || (leftColumn == numGridLengthOnPage() - 1)) {
column = leftColumn;
}
// calculate the position based on the page, row and column it is at
int pos = (page * numGridLengthOnPage() * numGridLengthOnPage()) + (row * numGridLengthOnPage()) + 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 > (m_folder->m_homeScreen->homeScreenState()->viewWidth() - leftMarginFromScreenEdge()))
|| (y < topMarginFromScreenEdge()) || (y > m_folder->m_homeScreen->homeScreenState()->viewHeight() - topMarginFromScreenEdge());
}
void ApplicationFolderModel::evaluateDelegateIndexes(bool emitSignal)
{
int rows = numGridLengthOnPage();
int columns = numGridLengthOnPage();
int numOfDelegates = m_folder->m_delegates.size();
int index = 0;
int page = 0;
while (index < m_folder->m_delegates.size()) {
int prevIndex = index;
// determine the row and column index 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].columnIndex = column;
m_folder->m_delegates[index].rowIndex = row;
m_folder->m_delegates[index].pageIndex = page;
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), {columnIndexRole});
Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_folder->m_delegates.size() - 1, 0), {rowIndexRole});
Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_folder->m_delegates.size() - 1, 0), {pageIndexRole});
}
Q_EMIT numberOfPagesChanged();
}
QPointF ApplicationFolderModel::getDelegateStartPosition(int page)
{
qreal pageWidth = m_folder->m_homeScreen->homeScreenState()->folderPageWidth();
qreal x = pageWidth * page + leftMarginFromScreenEdge();
qreal y = topMarginFromScreenEdge();
return QPointF{x, y};
}
int ApplicationFolderModel::numTotalPages()
{
int numOfDelegatesOnPage = numGridLengthOnPage() * numGridLengthOnPage();
return std::ceil(((qreal)m_folder->m_delegates.size()) / numOfDelegatesOnPage);
}
int ApplicationFolderModel::numGridLengthOnPage()
{
return m_folder->m_homeScreen->homeScreenState()->folderGridLength();
}
qreal ApplicationFolderModel::leftMarginFromScreenEdge()
{
HomeScreenState *homeScreenState = m_folder->m_homeScreen->homeScreenState();
qreal viewWidth = homeScreenState->viewWidth();
qreal folderPageWidth = homeScreenState->folderPageWidth();
return (viewWidth - folderPageWidth) / 2 + horizontalPageMargin();
}
qreal ApplicationFolderModel::topMarginFromScreenEdge()
{
HomeScreenState *homeScreenState = m_folder->m_homeScreen->homeScreenState();
qreal viewHeight = homeScreenState->viewHeight();
qreal folderPageHeight = homeScreenState->folderPageHeight();
return (viewHeight - folderPageHeight) / 2 + verticalPageMargin();
}
qreal ApplicationFolderModel::horizontalPageMargin()
{
HomeScreenState *homeScreenState = m_folder->m_homeScreen->homeScreenState();
qreal pageWidth = homeScreenState->folderPageWidth();
qreal pageContentWidth = homeScreenState->folderPageContentWidth();
return (pageWidth - pageContentWidth) / 2;
}
qreal ApplicationFolderModel::verticalPageMargin()
{
HomeScreenState *homeScreenState = m_folder->m_homeScreen->homeScreenState();
qreal pageHeight = homeScreenState->folderPageHeight();
qreal pageContentHeight = homeScreenState->folderPageContentHeight();
return (pageHeight - pageContentHeight) / 2;
}