File indexing completed on 2024-12-29 05:06:05

0001 // SPDX-FileCopyrightText: 2022-2023 Devin Lin <devin@kde.org>
0002 // SPDX-License-Identifier: GPL-2.0-or-later
0003 
0004 #include "pagemodel.h"
0005 #include "foliosettings.h"
0006 #include "homescreenstate.h"
0007 #include "widgetsmanager.h"
0008 
0009 FolioPageDelegate::FolioPageDelegate(int row, int column, QObject *parent)
0010     : FolioDelegate{parent}
0011     , m_row{row}
0012     , m_column{column}
0013 {
0014     init();
0015 }
0016 
0017 FolioPageDelegate::FolioPageDelegate(int row, int column, FolioApplication *application, QObject *parent)
0018     : FolioDelegate{application, parent}
0019     , m_row{row}
0020     , m_column{column}
0021 {
0022     init();
0023 }
0024 
0025 FolioPageDelegate::FolioPageDelegate(int row, int column, FolioApplicationFolder *folder, QObject *parent)
0026     : FolioDelegate{folder, parent}
0027     , m_row{row}
0028     , m_column{column}
0029 {
0030     init();
0031 }
0032 
0033 FolioPageDelegate::FolioPageDelegate(int row, int column, FolioWidget *widget, QObject *parent)
0034     : FolioDelegate{widget, parent}
0035     , m_row{row}
0036     , m_column{column}
0037 {
0038     init();
0039 }
0040 
0041 FolioPageDelegate::FolioPageDelegate(int row, int column, FolioDelegate *delegate, QObject *parent)
0042     : FolioDelegate{parent}
0043     , m_row{row}
0044     , m_column{column}
0045 {
0046     m_type = delegate->type();
0047     m_application = delegate->application();
0048     m_folder = delegate->folder();
0049     m_widget = delegate->widget();
0050 
0051     init();
0052 }
0053 
0054 void FolioPageDelegate::init()
0055 {
0056     // we have to use the "real" rows and columns, so fetch them from FolioSettings instead of HomeScreenState
0057     switch (HomeScreenState::self()->pageOrientation()) {
0058     case HomeScreenState::RegularPosition:
0059         m_realRow = m_row;
0060         m_realColumn = m_column;
0061         break;
0062     case HomeScreenState::RotateClockwise:
0063         m_realRow = HomeScreenState::self()->pageColumns() - m_column - 1;
0064         m_realColumn = m_row;
0065 
0066         if (m_widget) {
0067             // since top-left in cw is bottom-left in portrait
0068             m_realRow -= m_widget->realGridHeight() - 1;
0069         }
0070 
0071         break;
0072     case HomeScreenState::RotateCounterClockwise:
0073         m_realRow = m_column;
0074         m_realColumn = HomeScreenState::self()->pageRows() - m_row - 1;
0075 
0076         if (m_widget) {
0077             // since top-left in ccw is top-right in portrait
0078             m_realColumn -= m_widget->realGridWidth() - 1;
0079         }
0080 
0081         break;
0082     case HomeScreenState::RotateUpsideDown:
0083         m_realRow = HomeScreenState::self()->pageRows() - m_row - 1;
0084         m_realColumn = HomeScreenState::self()->pageColumns() - m_column - 1;
0085 
0086         if (m_widget) {
0087             // since top-left in upside-down is bottom-right in portrait
0088             m_realRow -= m_widget->realGridHeight() - 1;
0089             m_realColumn -= m_widget->realGridWidth() - 1;
0090         }
0091 
0092         break;
0093     }
0094 
0095     if (m_widget) {
0096         connect(m_widget, &FolioWidget::realTopLeftPositionChanged, this, [this](int rowOffset, int columnOffset) {
0097             m_realRow += rowOffset;
0098             m_realColumn += columnOffset;
0099         });
0100     }
0101 
0102     connect(HomeScreenState::self(), &HomeScreenState::pageOrientationChanged, this, [this]() {
0103         setRowOnly(getTranslatedTopLeftRow(m_realRow, m_realColumn, this));
0104         setColumnOnly(getTranslatedTopLeftColumn(m_realRow, m_realColumn, this));
0105     });
0106 }
0107 
0108 FolioPageDelegate *FolioPageDelegate::fromJson(QJsonObject &obj, QObject *parent)
0109 {
0110     FolioDelegate *fd = FolioDelegate::fromJson(obj, parent);
0111 
0112     if (!fd) {
0113         return nullptr;
0114     }
0115 
0116     int realRow = obj[QStringLiteral("row")].toInt();
0117     int realColumn = obj[QStringLiteral("column")].toInt();
0118 
0119     int row = getTranslatedTopLeftRow(realRow, realColumn, fd);
0120     int column = getTranslatedTopLeftColumn(realRow, realColumn, fd);
0121 
0122     FolioPageDelegate *delegate = new FolioPageDelegate{row, column, fd, parent};
0123     fd->deleteLater();
0124 
0125     return delegate;
0126 }
0127 
0128 int FolioPageDelegate::getTranslatedTopLeftRow(int realRow, int realColumn, FolioDelegate *fd)
0129 {
0130     int row = getTranslatedRow(realRow, realColumn);
0131     int column = getTranslatedColumn(realRow, realColumn);
0132 
0133     // special logic to return "top left" for widgets, since they take more than one tile
0134     if (fd->type() == FolioDelegate::Widget) {
0135         return fd->widget()->topLeftCorner(row, column).row;
0136     } else {
0137         return row;
0138     }
0139 }
0140 
0141 int FolioPageDelegate::getTranslatedTopLeftColumn(int realRow, int realColumn, FolioDelegate *fd)
0142 {
0143     int row = getTranslatedRow(realRow, realColumn);
0144     int column = getTranslatedColumn(realRow, realColumn);
0145 
0146     // special logic to return "top left" for widgets, since they take more than one tile
0147     if (fd->type() == FolioDelegate::Widget) {
0148         return fd->widget()->topLeftCorner(row, column).column;
0149     } else {
0150         return column;
0151     }
0152 }
0153 
0154 int FolioPageDelegate::getTranslatedRow(int realRow, int realColumn)
0155 {
0156     // we have to use the "real" rows and columns, so fetch them from FolioSettings instead of HomeScreenState
0157     switch (HomeScreenState::self()->pageOrientation()) {
0158     case HomeScreenState::RegularPosition:
0159         return realRow;
0160     case HomeScreenState::RotateClockwise:
0161         return realColumn;
0162     case HomeScreenState::RotateCounterClockwise:
0163         return FolioSettings::self()->homeScreenColumns() - realColumn - 1;
0164     case HomeScreenState::RotateUpsideDown:
0165         return FolioSettings::self()->homeScreenRows() - realRow - 1;
0166     }
0167     return realRow;
0168 }
0169 
0170 int FolioPageDelegate::getTranslatedColumn(int realRow, int realColumn)
0171 {
0172     // we have to use the "real" rows and columns, so fetch them from FolioSettings instead of HomeScreenState
0173     switch (HomeScreenState::self()->pageOrientation()) {
0174     case HomeScreenState::RegularPosition:
0175         return realColumn;
0176     case HomeScreenState::RotateClockwise:
0177         return FolioSettings::self()->homeScreenRows() - realRow - 1;
0178     case HomeScreenState::RotateCounterClockwise:
0179         return realRow;
0180     case HomeScreenState::RotateUpsideDown:
0181         return FolioSettings::self()->homeScreenColumns() - realColumn - 1;
0182     }
0183     return realRow;
0184 }
0185 
0186 QJsonObject FolioPageDelegate::toJson() const
0187 {
0188     QJsonObject o = FolioDelegate::toJson();
0189     o[QStringLiteral("row")] = m_realRow;
0190     o[QStringLiteral("column")] = m_realColumn;
0191     return o;
0192 }
0193 
0194 int FolioPageDelegate::row()
0195 {
0196     return m_row;
0197 }
0198 
0199 void FolioPageDelegate::setRow(int row)
0200 {
0201     if (m_row != row) {
0202         // adjust stored data too
0203         switch (HomeScreenState::self()->pageOrientation()) {
0204         case HomeScreenState::RegularPosition:
0205             m_realRow = row;
0206             break;
0207         case HomeScreenState::RotateClockwise:
0208             m_realColumn += row - m_row;
0209             break;
0210         case HomeScreenState::RotateCounterClockwise:
0211             m_realColumn += m_row - row;
0212             break;
0213         case HomeScreenState::RotateUpsideDown:
0214             m_realRow += m_row - row;
0215             break;
0216         }
0217 
0218         setRowOnly(row);
0219     }
0220 }
0221 
0222 void FolioPageDelegate::setRowOnly(int row)
0223 {
0224     if (m_row != row) {
0225         m_row = row;
0226         Q_EMIT rowChanged();
0227     }
0228 }
0229 
0230 int FolioPageDelegate::column()
0231 {
0232     return m_column;
0233 }
0234 
0235 void FolioPageDelegate::setColumn(int column)
0236 {
0237     if (m_column != column) {
0238         // adjust stored data too
0239         switch (HomeScreenState::self()->pageOrientation()) {
0240         case HomeScreenState::RegularPosition:
0241             m_realColumn = column;
0242             break;
0243         case HomeScreenState::RotateClockwise:
0244             m_realRow += m_column - column;
0245             break;
0246         case HomeScreenState::RotateCounterClockwise:
0247             m_realRow += column - m_column;
0248             break;
0249         case HomeScreenState::RotateUpsideDown:
0250             m_realColumn += m_column - column;
0251             break;
0252         }
0253 
0254         setColumnOnly(column);
0255     }
0256 }
0257 
0258 void FolioPageDelegate::setColumnOnly(int column)
0259 {
0260     if (m_column != column) {
0261         m_column = column;
0262         Q_EMIT columnChanged();
0263     }
0264 }
0265 
0266 PageModel::PageModel(QList<FolioPageDelegate *> delegates, QObject *parent)
0267     : QAbstractListModel{parent}
0268     , m_delegates{delegates}
0269 {
0270     connect(WidgetsManager::self(), &WidgetsManager::widgetRemoved, this, [this](Plasma::Applet *applet) {
0271         if (applet) {
0272             // delete any instance of this widget
0273             for (int i = 0; i < m_delegates.size(); i++) {
0274                 auto *delegate = m_delegates[i];
0275                 if (delegate->type() == FolioDelegate::Widget && delegate->widget()->applet() == applet) {
0276                     removeDelegate(i);
0277                     break;
0278                 }
0279             }
0280         }
0281     });
0282 }
0283 
0284 PageModel::~PageModel() = default;
0285 
0286 PageModel *PageModel::fromJson(QJsonArray &arr, QObject *parent)
0287 {
0288     QList<FolioPageDelegate *> delegates;
0289 
0290     for (QJsonValueRef r : arr) {
0291         QJsonObject obj = r.toObject();
0292 
0293         FolioPageDelegate *delegate = FolioPageDelegate::fromJson(obj, parent);
0294         if (delegate) {
0295             delegates.append(delegate);
0296         }
0297     }
0298 
0299     PageModel *model = new PageModel{delegates, parent};
0300 
0301     // ensure delegates can request saves
0302     for (auto *delegate : delegates) {
0303         model->connectSaveRequests(delegate);
0304     }
0305 
0306     return model;
0307 }
0308 
0309 QJsonArray PageModel::toJson() const
0310 {
0311     QJsonArray arr;
0312 
0313     for (FolioPageDelegate *delegate : m_delegates) {
0314         if (!delegate) {
0315             continue;
0316         }
0317 
0318         arr.append(delegate->toJson());
0319     }
0320 
0321     return arr;
0322 }
0323 
0324 int PageModel::rowCount(const QModelIndex &parent) const
0325 {
0326     Q_UNUSED(parent)
0327     return m_delegates.size();
0328 }
0329 
0330 QVariant PageModel::data(const QModelIndex &index, int role) const
0331 {
0332     if (!index.isValid()) {
0333         return QVariant();
0334     }
0335 
0336     switch (role) {
0337     case DelegateRole:
0338         return QVariant::fromValue(m_delegates.at(index.row()));
0339     }
0340 
0341     return QVariant();
0342 }
0343 
0344 QHash<int, QByteArray> PageModel::roleNames() const
0345 {
0346     return {{DelegateRole, "delegate"}};
0347 }
0348 
0349 void PageModel::removeDelegate(int row, int col)
0350 {
0351     for (int i = 0; i < m_delegates.size(); ++i) {
0352         if (m_delegates[i]->row() == row && m_delegates[i]->column() == col) {
0353             removeDelegate(i);
0354             break;
0355         }
0356     }
0357 }
0358 
0359 void PageModel::removeDelegate(int index)
0360 {
0361     if (index < 0 || index >= m_delegates.size()) {
0362         return;
0363     }
0364 
0365     beginRemoveRows(QModelIndex(), index, index);
0366     // HACK: do not deleteLater(), because the delegate might still be used somewhere else
0367     m_delegates.removeAt(index);
0368     endRemoveRows();
0369 
0370     save();
0371 }
0372 
0373 bool PageModel::canAddDelegate(int row, int column, FolioDelegate *delegate)
0374 {
0375     if (row < 0 || row >= HomeScreenState::self()->pageRows() || column < 0 || column >= HomeScreenState::self()->pageColumns()) {
0376         return false;
0377     }
0378 
0379     if (delegate->type() == FolioDelegate::Widget) {
0380         // inserting a widget...
0381 
0382         // bounds of widget
0383         int maxRow = row + delegate->widget()->gridHeight() - 1;
0384         int maxColumn = column + delegate->widget()->gridWidth() - 1;
0385 
0386         // check bounds
0387         if ((row < 0 || row >= HomeScreenState::self()->pageRows()) || (maxRow < 0 || maxRow >= HomeScreenState::self()->pageRows())
0388             || (column < 0 || column >= HomeScreenState::self()->pageColumns()) || (maxColumn < 0 || maxColumn >= HomeScreenState::self()->pageColumns())) {
0389             return false;
0390         }
0391 
0392         // check if any delegate exists at any of the spots where the widget is being added
0393         for (FolioPageDelegate *d : m_delegates) {
0394             if (delegate->widget()->isInBounds(row, column, d->row(), d->column())) {
0395                 return false;
0396             } else if (d->type() == FolioDelegate::Widget) {
0397                 // 2 widgets overlapping scenario
0398                 if (d->widget()->overlapsWidget(d->row(), d->column(), delegate->widget(), row, column)) {
0399                     return false;
0400                 }
0401             }
0402         }
0403 
0404     } else {
0405         // inserting app or folder...
0406 
0407         // check if there already exists a delegate in this space
0408         for (FolioPageDelegate *d : m_delegates) {
0409             if (d->row() == row && d->column() == column) {
0410                 return false;
0411             } else if (d->type() == FolioDelegate::Widget && d->widget()->isInBounds(d->row(), d->column(), row, column)) {
0412                 return false;
0413             }
0414         }
0415     }
0416 
0417     return true;
0418 }
0419 
0420 bool PageModel::addDelegate(FolioPageDelegate *delegate)
0421 {
0422     if (!canAddDelegate(delegate->row(), delegate->column(), delegate)) {
0423         return false;
0424     }
0425 
0426     beginInsertRows(QModelIndex(), m_delegates.size(), m_delegates.size());
0427     m_delegates.append(delegate);
0428     endInsertRows();
0429 
0430     // ensure the delegate requests saves
0431     connectSaveRequests(delegate);
0432     save();
0433 
0434     return true;
0435 }
0436 
0437 FolioPageDelegate *PageModel::getDelegate(int row, int col)
0438 {
0439     for (FolioPageDelegate *d : m_delegates) {
0440         if (d->row() == row && d->column() == col) {
0441             return d;
0442         }
0443 
0444         // check if this is in a widget's space
0445         if (d->type() == FolioDelegate::Widget) {
0446             if (d->widget()->isInBounds(d->row(), d->column(), row, col)) {
0447                 return d;
0448             }
0449         }
0450     }
0451     return nullptr;
0452 }
0453 
0454 void PageModel::moveAndResizeWidgetDelegate(FolioPageDelegate *delegate, int newRow, int newColumn, int newGridWidth, int newGridHeight)
0455 {
0456     if (delegate->type() != FolioDelegate::Widget) {
0457         return;
0458     }
0459 
0460     if (newGridWidth < 1 || newGridHeight < 1) {
0461         return;
0462     }
0463 
0464     // test if we can add the delegate with new size and position
0465     FolioWidget *testWidget = new FolioWidget(this, 0, 0, 0);
0466     // we have to use setGridWidth and setGridHeight since it takes into account the page orientation
0467     testWidget->setGridWidth(newGridWidth);
0468     testWidget->setGridHeight(newGridHeight);
0469     FolioDelegate *testDelegate = new FolioDelegate(testWidget, this);
0470 
0471     // NOT THREAD SAFE!
0472     // which is fine, because the GUI isn't multithreaded
0473     int index = m_delegates.indexOf(delegate);
0474     m_delegates.remove(index); // remove the delegate temporarily, since we don't want it to check overlapping of itself
0475     bool canAdd = canAddDelegate(newRow, newColumn, testDelegate);
0476     m_delegates.insert(index, delegate); // add it back
0477 
0478     // cleanup test delegate
0479     testDelegate->deleteLater();
0480     testWidget->deleteLater();
0481 
0482     if (!canAdd) {
0483         return;
0484     }
0485 
0486     delegate->setRow(newRow);
0487     delegate->setColumn(newColumn);
0488     delegate->widget()->setGridWidth(newGridWidth);
0489     delegate->widget()->setGridHeight(newGridHeight);
0490 }
0491 
0492 bool PageModel::isPageEmpty()
0493 {
0494     return m_delegates.size() == 0;
0495 }
0496 
0497 void PageModel::connectSaveRequests(FolioDelegate *delegate)
0498 {
0499     if (delegate->type() == FolioDelegate::Folder && delegate->folder()) {
0500         connect(delegate->folder(), &FolioApplicationFolder::saveRequested, this, &PageModel::save);
0501     } else if (delegate->type() == FolioDelegate::Widget && delegate->widget()) {
0502         connect(delegate->widget(), &FolioWidget::saveRequested, this, &PageModel::save);
0503     }
0504 }
0505 
0506 void PageModel::save()
0507 {
0508     Q_EMIT saveRequested();
0509 }