mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-28 14:43:09 +00:00
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.
484 lines
15 KiB
C++
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;
|
|
}
|