shift-shell/containments/homescreens/folio/pagemodel.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

253 lines
7.5 KiB
C++

// 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"
#include "widgetsmanager.h"
PageModel::PageModel(QList<FolioPageDelegate::Ptr> delegates, QObject *parent, HomeScreen *homeScreen)
: QAbstractListModel{parent}
, m_homeScreen{homeScreen}
, m_delegates{delegates}
{
connect(homeScreen->widgetsManager(), &WidgetsManager::widgetRemoved, this, [this](Plasma::Applet *applet) {
if (applet) {
// delete any instance of this widget
for (int i = 0; i < m_delegates.size(); i++) {
FolioPageDelegate::Ptr delegate = m_delegates[i];
if (delegate->type() == FolioDelegate::Widget && delegate->widget()->applet() == applet) {
removeDelegate(i);
break;
}
}
}
});
}
PageModel::~PageModel() = default;
PageModel *PageModel::fromJson(QJsonArray &arr, QObject *parent, HomeScreen *homeScreen)
{
QList<FolioPageDelegate::Ptr> delegates;
for (QJsonValueRef r : arr) {
QJsonObject obj = r.toObject();
FolioPageDelegate::Ptr delegate = FolioPageDelegate::fromJson(obj, homeScreen);
if (delegate) {
delegates.append(delegate);
}
}
PageModel *model = new PageModel{delegates, parent, homeScreen};
// ensure delegates can request saves
for (FolioPageDelegate::Ptr delegate : delegates) {
model->connectSaveRequests(delegate);
}
return model;
}
QJsonArray PageModel::toJson() const
{
QJsonArray arr;
for (FolioPageDelegate::Ptr 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()).get());
}
return QVariant();
}
QHash<int, QByteArray> PageModel::roleNames() const
{
return {{DelegateRole, "delegate"}};
}
void PageModel::removeDelegate(int row, int col)
{
for (int i = 0; i < m_delegates.size(); ++i) {
if (m_delegates[i]->row() == row && m_delegates[i]->column() == col) {
removeDelegate(i);
break;
}
}
}
void PageModel::removeDelegate(int index)
{
if (index < 0 || index >= m_delegates.size()) {
return;
}
beginRemoveRows(QModelIndex(), index, index);
// HACK: do not deleteLater(), because the delegate might still be used somewhere else
m_delegates.removeAt(index);
endRemoveRows();
save();
}
bool PageModel::canAddDelegate(int row, int column, FolioDelegate *delegate)
{
HomeScreenState *homeScreenState = m_homeScreen->homeScreenState();
if (row < 0 || row >= homeScreenState->pageRows() || column < 0 || column >= homeScreenState->pageColumns()) {
return false;
}
if (delegate->type() == FolioDelegate::Widget) {
// inserting a widget...
// bounds of widget
int maxRow = row + delegate->widget()->gridHeight() - 1;
int maxColumn = column + delegate->widget()->gridWidth() - 1;
// check bounds
if ((row < 0 || row >= homeScreenState->pageRows()) || (maxRow < 0 || maxRow >= homeScreenState->pageRows())
|| (column < 0 || column >= homeScreenState->pageColumns()) || (maxColumn < 0 || maxColumn >= homeScreenState->pageColumns())) {
return false;
}
// check if any delegate exists at any of the spots where the widget is being added
for (FolioPageDelegate::Ptr d : m_delegates) {
if (delegate->widget()->isInBounds(row, column, d->row(), d->column())) {
return false;
} else if (d->type() == FolioDelegate::Widget) {
// 2 widgets overlapping scenario
if (d->widget()->overlapsWidget(d->row(), d->column(), delegate->widget(), row, column)) {
return false;
}
}
}
} else {
// inserting app or folder...
// check if there already exists a delegate in this space
for (FolioPageDelegate::Ptr d : m_delegates) {
if (d->row() == row && d->column() == column) {
return false;
} else if (d->type() == FolioDelegate::Widget && d->widget()->isInBounds(d->row(), d->column(), row, column)) {
return false;
}
}
}
return true;
}
bool PageModel::addDelegate(FolioPageDelegate::Ptr delegate)
{
if (!canAddDelegate(delegate->row(), delegate->column(), delegate.get())) {
return false;
}
beginInsertRows(QModelIndex(), m_delegates.size(), m_delegates.size());
m_delegates.append(delegate);
endInsertRows();
// ensure the delegate requests saves
connectSaveRequests(delegate);
save();
return true;
}
FolioPageDelegate::Ptr PageModel::getDelegate(int row, int col)
{
for (FolioPageDelegate::Ptr d : m_delegates) {
if (d->row() == row && d->column() == col) {
return d;
}
// check if this is in a widget's space
if (d->type() == FolioDelegate::Widget) {
if (d->widget()->isInBounds(d->row(), d->column(), row, col)) {
return d;
}
}
}
return nullptr;
}
void PageModel::moveAndResizeWidgetDelegate(FolioPageDelegate *delegate, int newRow, int newColumn, int newGridWidth, int newGridHeight)
{
if (delegate->type() != FolioDelegate::Widget) {
return;
}
if (newGridWidth < 1 || newGridHeight < 1) {
return;
}
// Test if we can add the delegate with new size and position
FolioWidget::Ptr testWidget = std::make_shared<FolioWidget>(m_homeScreen, 0, 0, 0);
// We have to use setGridWidth and setGridHeight since it takes into account the page orientation
testWidget->setGridWidth(newGridWidth);
testWidget->setGridHeight(newGridHeight);
FolioDelegate::Ptr testDelegate = std::make_shared<FolioDelegate>(testWidget, m_homeScreen);
// testWidget and testDelegate will get cleaned up automatically since are smart pointers
// NOT THREAD SAFE!
// which is fine, because the GUI isn't multithreaded
int index = m_delegates.indexOf(delegate->sharedPageDelegate());
m_delegates.remove(index); // remove the delegate temporarily, since we don't want it to check overlapping of itself
bool canAdd = canAddDelegate(newRow, newColumn, testDelegate.get());
m_delegates.insert(index, delegate->sharedPageDelegate()); // add it back
if (!canAdd) {
return;
}
delegate->setRow(newRow);
delegate->setColumn(newColumn);
delegate->widget()->setGridWidth(newGridWidth);
delegate->widget()->setGridHeight(newGridHeight);
}
bool PageModel::isPageEmpty()
{
return m_delegates.size() == 0;
}
void PageModel::connectSaveRequests(FolioDelegate::Ptr delegate)
{
if (delegate->type() == FolioDelegate::Folder && delegate->folder()) {
connect(delegate->folder().get(), &FolioApplicationFolder::saveRequested, this, &PageModel::save);
} else if (delegate->type() == FolioDelegate::Widget && delegate->widget()) {
connect(delegate->widget().get(), &FolioWidget::saveRequested, this, &PageModel::save);
}
}
void PageModel::save()
{
Q_EMIT saveRequested();
}