shift-shell/containments/homescreens/folio/folioapplicationfolder.cpp
Micah Stanley 3a6a2f78da homescreen/folio: fix icon drop position misalignment within folder and outside folder view check not accounting for size differences
This fixes an issue with folder app icon drop positions being misaligned. Noticeably causing issues when placing apps within folders when using gesture only mode.
Also, this fixes an issue with the outsideView calculation not accounting for folder icon size differences. Causing an issues with some apps still being interactive when outside the current page.
2024-11-20 03:20:01 +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 *FolioApplicationFolder::fromJson(QJsonObject &obj, HomeScreen *parent)
{
QString name = obj[QStringLiteral("name")].toString();
QList<FolioApplication *> apps;
for (auto storageId : obj[QStringLiteral("apps")].toArray()) {
if (KService::Ptr service = KService::serviceByStorageId(storageId.toString())) {
apps.append(new FolioApplication(parent, service));
}
}
FolioApplicationFolder *folder = new 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());
}
return previews;
}
ApplicationFolderModel *FolioApplicationFolder::applications()
{
return m_applicationFolderModel;
}
void FolioApplicationFolder::setApplications(QList<FolioApplication *> applications)
{
if (m_applicationFolderModel) {
m_applicationFolderModel->deleteLater();
}
m_delegates.clear();
for (auto *app : applications) {
m_delegates.append({new FolioDelegate{app, m_homeScreen}, 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 *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 *folder)
: QAbstractListModel{folder}
, m_folder{folder}
{
HomeScreenState *homeScreenState = 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);
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 *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 *delegate, int index)
{
if (index < 0 || index > m_folder->m_delegates.size()) {
return false;
}
if (!delegate) {
return false;
}
return true;
}
bool ApplicationFolderModel::addDelegate(FolioDelegate *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});
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});
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 *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 = new FolioDelegate{m_folder->m_homeScreen};
}
// add empty delegate at new position
addDelegate(ghost, index);
}
void ApplicationFolderModel::replaceGhostEntry(FolioDelegate *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;
}