shift-shell/containments/homescreens/folio/folioapplicationfolder.cpp
Devin Lin ecefc446a6 homescreens/folio: Do not initialize shared_ptr with a QObject parent
This MR ensures that delegates created with make_shared do not also get
initialized with a QObject parent. Some of the classes used the same
constructor parameter for HomeScreen and used it as a parent simultaneously. This was refactored
so that they are separated.
2025-07-15 18:10:03 -04:00

495 lines
16 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 *homeScreen, QString name, QObject *parent)
: QObject{parent}
, m_homeScreen{homeScreen}
, m_name{name}
, m_applicationFolderModel{new ApplicationFolderModel{this}}
{
}
FolioApplicationFolder::Ptr FolioApplicationFolder::fromJson(QJsonObject &obj, HomeScreen *homeScreen)
{
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>(service));
}
}
FolioApplicationFolder::Ptr folder = std::make_shared<FolioApplicationFolder>(homeScreen, 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();
});
// Listen to application removal events and delete delegates
connect(m_folder->m_homeScreen->applicationListModel(), &ApplicationListModel::applicationRemoved, this, [this](const QString &storageId) {
for (int i = 0; i < m_folder->m_delegates.size(); i++) {
auto delegate = m_folder->m_delegates[i].delegate;
if (delegate->type() == FolioDelegate::Application && delegate->application()->storageId() == storageId) {
removeDelegate(i);
}
}
});
}
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>();
}
// 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;
}